Skip to content
LumoAsset Store

Asset Store REST API v1

The same endpoints the Lumo editor's Asset Store tab uses. Public reads need no authentication; publishers authenticate with a Bearer JWT and machine clients can send X-API-Key instead.

Base URL: /apiJSONBearer JWTX-API-KeyCORS enabled
MethodEndpointPurpose
GET/api/assetsList & search assets
GET/api/assets/[id]Asset detail
POST/api/auth/loginGet a JWT
POST/api/assetsPublish (multipart)
PATCH/api/admin/assets/[id]Approve / reject

Base URL

https://<your-deployment>/api

Every endpoint accepts and returns JSON unless stated otherwise. The same routes are used by this website, the Lumo editor's Asset Store tab and any machine client.

Authentication

Requests are authenticated in one of two ways — both work on every route:

Authorization: Bearer <supabase-access-token>
X-API-Key: $STORE_API_KEY

The X-API-Key header exists so the desktop app can call the API without a browser session. Treat STORE_API_KEY as a secret with admin-level power and never ship it in client code.

Public read endpoints (GET /api/assets, GET /api/assets/[id], free downloads) need no credentials at all.

CORS

Allowed origins come from the CORS_ORIGINS environment variable (comma separated). When the variable is empty, any origin may call the API. OPTIONS preflight requests are handled on every route with a 204 response.

Caching

Endpoint class Cache
Public reads s-maxage=60, stale-while-revalidate=300
Downloads no-store (counter must stay accurate)
Auth / admin no-store

Error format

Every failure uses the same envelope and a meaningful HTTP status:

{
  "error": {
    "code": "invalid_category",
    "message": "Category must be one of: 3D Models, Textures & Materials, ..."
  }
}

See Errors for the full status-code table.

Versioning

The v1 prefix is part of the path (/api/...). Breaking changes will ship under /api/v2 while v1 keeps working.

POST /api/auth/signup

Creates an account from an email and password.

Body

{
  "email": "publisher@example.com",
  "password": "correct-horse-battery",
  "username": "Ada Lovelace",
  "handle": "ada"
}

Response — 201 Created

{
  "token": "eyJhbGciOi...",
  "user": {
    "id": "9c2f...",
    "email": "publisher@example.com",
    "username": "Ada Lovelace",
    "handle": "ada",
    "role": "user",
    "banned": false,
    "createdAt": "2026-01-08T12:00:00.000Z"
  },
  "requiresConfirmation": false,
  "message": "Account created."
}

When the Supabase project requires email confirmation, token is null and requiresConfirmation is true.

Status Meaning
201 Account created
400 Invalid email, weak password or duplicate account
503 Supabase environment variables missing

POST /api/auth/login

Exchanges credentials for a token.

Body

{ "email": "publisher@example.com", "password": "correct-horse-battery" }

Response — 200 OK

{
  "token": "eyJhbGciOi...",
  "refreshToken": "...",
  "expiresAt": 1767225600,
  "user": { "id": "9c2f...", "role": "user" }
}
Status Meaning
200 Signed in
401 Wrong credentials
403 Account suspended (account_banned)

Using the token

Send it on every authenticated request:

TOKEN=$(curl -s -X POST /api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"a@b.dev","password":"secret123"}' | jq -r .token)

curl -X POST /api/assets \
  -H "Authorization: Bearer $TOKEN" \
  -F title="Sci-Fi Door" \
  -F category="3D Models" \
  -F file=@door.zip

Roles

Role Can do
user Download, publish own assets, edit own assets
admin Everything above plus moderation, user management and featuring

Roles live in the users.role column and are enforced server-side on every admin route.

GET /api/assets

Public catalogue listing with search, filters, sorting and pagination.

Query parameters

Param Type Default Notes
category string — One of the six store categories
q string — Matches title and description
price free | paid — Filter by price
sort newest | popular | rating newest
page number 1
perPage number 12 Max 48
curl "https://store.example.com/api/assets?category=Audio&sort=popular&page=1"

Response — 200 OK

{
  "items": [
    {
      "id": "6b1f...",
      "title": "Footstep Pack Vol. 1",
      "slug": "footstep-pack-vol-1",
      "author": { "id": "9c2f...", "username": "Ada Lovelace", "handle": "ada", "avatar": null },
      "category": "Audio",
      "tags": ["footsteps", "sfx"],
      "price": 0,
      "downloads": 6712,
      "thumbnail": "https://.../cover.png",
      "version": "1.2.0",
      "file": "https://.../pack.zip",
      "license": "CC0 1.0",
      "fileSize": 18432000,
      "rating": 4.5,
      "featured": true,
      "status": "approved",
      "createdAt": "2026-01-12T09:00:00.000Z"
    }
  ],
  "total": 42,
  "page": 1,
  "perPage": 12,
  "pageCount": 4
}

GET /api/assets/[id]

Accepts an asset id (UUID) or a slug.

curl "https://store.example.com/api/assets/footstep-pack-vol-1"

Response — 200 OK — a single object with the same shape as one element of items above.

Status Meaning
200 Found
404 Missing, or not yet approved

POST /api/assets

Requires a Bearer JWT (publisher) or X-API-Key.

Send multipart/form-data:

Field Required Notes
title yes max 120 chars
category yes must match a store category
description no max 5000 chars
tags no comma separated or repeated fields
license no defaults to Lumo Asset License
version no defaults to 1.0.0
price no USD amount, 0 = free
status no draft keeps it private, otherwise pending
file yes* the asset .zip, max 50 MB
thumbnail no image, max 5 MB

file_url / thumbnail_url may be sent as plain fields instead of uploading a file (JSON bodies are supported too).

Response — 201 Created — { "asset": { ... } } with status: "pending".

PATCH /api/assets/[id]

Owner or admin only. Accepts the same fields as POST (partial payloads are merged with the current values) and may replace file / thumbnail.

Sending {"status": "draft"} unpublishes an asset; {"status": "pending"} sends it back through review. Only admins can approve.

Status Meaning
200 Updated
400 Validation failed
403 Not the owner
404 Missing asset

GET /api/assets/[id]/download

Increments the download counter and returns the file.

  • Browsers receive a 302 redirect to the file.
  • API clients (Accept: application/json, ?format=json or X-API-Key) receive JSON:
{
  "id": "6b1f...",
  "title": "Footstep Pack Vol. 1",
  "slug": "footstep-pack-vol-1",
  "url": "https://.../pack.zip",
  "filename": "footstep-pack-vol-1-v1.2.0.zip",
  "fileSize": 18432000,
  "downloads": 6713,
  "license": "CC0 1.0"
}
Status Meaning
200 URL returned
302 Browser redirect
401 Paid asset without a session (login_required)
404 Asset or file missing

All admin endpoints require an admin Bearer JWT or the X-API-Key machine credential.

GET /api/admin/queue

Returns submissions for review.

Query Values Default
status pending | approved | rejected | draft | all pending
curl "https://store.example.com/api/admin/queue?status=pending" \
  -H "X-API-Key: $STORE_API_KEY"

Response — 200 OK

{
  "items": [ { "id": "6b1f...", "title": "Sci-Fi Door", "status": "pending" } ],
  "total": 1,
  "status": "pending"
}
Status Meaning
200 Queue returned
401 No credentials
403 Caller is not an admin

PATCH /api/admin/assets/[id]

{ "action": "approve", "comment": "Looks great, published." }
Action Effect
approve status = approved, visible in the store
reject status = rejected + review_note (comment required)
feature Adds the asset to the homepage carousel
unfeature Removes it from the carousel

Response — 200 OK — { "asset": { ... }, "action": "approve" }

GET /api/admin/users

Lists every account: { "items": [ { "id", "email", "username", "handle", "role", "banned", "createdAt" } ], "total": 12 }.

PATCH /api/admin/users/[id]

{ "role": "admin" }
{ "banned": true }

Both fields are optional. You cannot demote or ban your own account.

GET /api/admin/stats

{
  "totalAssets": 12,
  "approved": 10,
  "pending": 2,
  "rejected": 0,
  "totalUsers": 2,
  "totalDownloads": 48210,
  "featured": 4
}

Envelope

Every non-2xx response uses the same shape:

{
  "error": {
    "code": "invalid_category",
    "message": "Category must be one of: 3D Models, Textures & Materials, Sprites & 2D, Audio, Plugins, Templates.",
    "details": {}
  }
}

code is stable and safe to branch on. message is human readable and may change.

Status codes

Status code examples What to do
400 invalid_title, invalid_category, invalid_price, invalid_license, missing_file, missing_comment, invalid_action Fix the payload
401 unauthorized, invalid_credentials, login_required, signup_failed Log in and retry with a fresh token
402/401 login_required Paid asset — sign in first
403 forbidden, account_banned, role_required Caller is not allowed
404 not_found Asset or user does not exist (or is not public)
409 not_approved Approve the asset before featuring it
500 internal_error Retry, then open an issue
503 not_configured, storage_unavailable Deployment is missing environment variables

Handling errors in a client

const res = await fetch("/api/assets", {
  method: "POST",
  headers: { Authorization: `Bearer ${token}` },
  body: form,
});

if (!res.ok) {
  const { error } = await res.json();
  console.error(error.code, error.message);
}

CORS failures

If your request fails with a network error instead of a JSON body, the origin is probably not listed in CORS_ORIGINS. Add your deployment origin to the variable and redeploy.

Reporting bugs

The API is part of Lumo Engine — report problems on the issue tracker.

Was this page helpful?

Tell the Lumo team what is missing.

Open an issue