Amba

Media

Media assets and folders — register an upload, browse, organize, delete.

Media assets register a filename + storage path and return a signed-style upload URL the client can PUT the file body to. Folders are a simple parent/child tree.

Endpoints

MethodPathDescription
POST/admin/projects/:projectId/mediaRegister an asset, get an upload_url.
GET/admin/projects/:projectId/mediaPaginated list; optional ?folder_id=.
DELETE/admin/projects/:projectId/media/:assetIdDelete an asset row.
POST/admin/projects/:projectId/media/foldersCreate a folder.
GET/admin/projects/:projectId/media/foldersList folders (optional ?parent_id=).
DELETE/admin/projects/:projectId/media/folders/:folderIdDelete a folder.
POST/admin/projects/:projectId/media/catalogsCreate a catalog.
GET/admin/projects/:projectId/media/catalogsList catalogs with item counts.
GET/admin/projects/:projectId/media/catalogs/:catalogIdGet a catalog + its ordered items.
PATCH/admin/projects/:projectId/media/catalogs/:catalogIdUpdate a catalog.
DELETE/admin/projects/:projectId/media/catalogs/:catalogIdDelete a catalog (items cascade).
POST/admin/projects/:projectId/media/catalogs/:catalogId/itemsAdd an asset to the catalog.
PATCH/admin/projects/:projectId/media/catalogs/:catalogId/items/reorderReorder items.
DELETE/admin/projects/:projectId/media/catalogs/:catalogId/items/:mediaIdRemove an asset.

POST /admin/projects/:projectId/media

Register a media asset. The storage path is <projectId>/<ms>_<id>_<name>, where <name> is a URL-safe form of filename: its last path component, with accents dropped, every character other than letters, digits, ., _ and - replaced by _, and the extension kept. The asset's filename stays exactly as sent. A missing or empty filename returns 400 INVALID_INPUT.

Request (CreateMediaAssetInput)

FieldTypeRequiredDefault
filenamestringyes—
mime_typestringyes—
size_bytesnumberfree: yesnull
folder_iduuidnonull
alt_textstringnonull
metadataobjectno{}

Response 201

{
  "data": {
    "id": "…",
    "filename": "…",
    "storage_url": "https://{projectId}.cdn.amba.host/media/…",
    "uploaded_by": "admin",
    "upload_state": "pending"
  },
  "upload_url": "https://{projectId}.cdn.amba.host/media/…?upload=true",
  "upload_url_expires_at": "2026-05-28T12:05:00Z"
}

Uploading the file

PUT the raw file bytes to the returned upload_url:

  • Method is PUT; the request body is the file bytes.
  • Content-Type MUST match the mime_type registered above — a mismatch is rejected.
  • No Authorization header on the upload PUT — upload_url is already authorized.
  • Success is 200, 201 or 204. The file is then served from storage_url.
  • upload_url is short-lived (upload_url_expires_at); if it has expired, register the asset again to get a fresh one.
  • Maximum file size for the media bucket is 50 MB.
  • On a free project size_bytes is required, the upload URL accepts at most that many bytes, and the size counts toward the 250 MB media budget: an upload past it returns 402 FREE_TIER_LIMIT_REACHED with details.limit: "media_storage".

The asset is registered with upload_state: "pending". It becomes "committed" when the upload finishes, and from then on it is listed and counts toward media storage. Until then it is left out of the list and of media usage. A pending asset whose file never arrives is deleted by the daily cleanup once it is 24 hours old (see When an upload counts).

Allowed MIME types

The default media bucket accepts the types below. A mime_type outside the list returns 400 MIME_NOT_ALLOWED.

CategoryAllowed types
Imageimage/jpeg, image/png, image/webp, image/gif
Videovideo/mp4, video/webm, video/quicktime, video/x-m4v
Audioaudio/mpeg, audio/aac, audio/ogg, audio/mp4

End-to-end example

# 1. Register the asset with the file's exact size in bytes
SIZE=$(wc -c < hero.png | tr -d ' ')
RESP=$(curl -s -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/media" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d "{\"filename\":\"hero.png\",\"mime_type\":\"image/png\",\"size_bytes\":${SIZE}}")
 
# 2. Extract upload + storage URLs
UPLOAD_URL=$(echo "$RESP" | jq -r '.upload_url')
STORAGE_URL=$(echo "$RESP" | jq -r '.data.storage_url')
 
# 3. PUT the bytes (Content-Type matches mime_type; no auth header)
curl -X PUT "$UPLOAD_URL" -H 'Content-Type: image/png' --data-binary @hero.png
 
# 4. Fetch the asset
curl -L "$STORAGE_URL" -o downloaded.png

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/media
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/media' \
  -H 'Content-Type: application/json' \
  -d '{
  "filename": "logo.png",
  "mime_type": "image/png",
  "size_bytes": 20480
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/media" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"filename":"logo.png","mime_type":"image/png","size_bytes":20480}'

GET /admin/projects/:projectId/media

Lists live assets whose upload finished (upload_state: "committed"). A deleted asset is omitted, including every asset of a deleted user whose stored file is still being purged (Cascade on user delete), and so is an asset whose upload has not finished.

Query

ParamDefaultDescription
limit50
offset0
folder_id—Restrict to a folder.

Response 200

{
  "data": [{ "id": "…", "filename": "…", "mime_type": "image/png" }],
  "total": 100,
  "offset": 0,
  "limit": 50
}

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/media
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/media'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/media" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

DELETE /admin/projects/:projectId/media/:assetId

{ "data": { "deleted": true } }

Try it:

DELETE/admin/projects/%7B%7BprojectId%7D%7D/media/%7B%7BassetId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/media/%7B%7BassetId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE "${BASE_URL}/admin/projects/${PROJECT_ID}/media/{assetId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

POST /admin/projects/:projectId/media/folders

Request (CreateMediaFolderInput)

FieldTypeRequired
namestringyes
parent_iduuidno

Response 201

{ "data": { "id": "…", "name": "…", "parent_id": null } }

Try it:

POST/admin/projects/%7B%7BprojectId%7D%7D/media/folders
developer auth
curl -X POST 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/media/folders' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "avatars"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/admin/projects/${PROJECT_ID}/media/folders" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"name":"avatars"}'

GET /admin/projects/:projectId/media/folders

Query

ParamDescription
parent_idIf omitted, returns only root folders (parent_id IS NULL).

Try it:

GET/admin/projects/%7B%7BprojectId%7D%7D/media/folders
developer auth
curl -X GET 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/media/folders'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/admin/projects/${PROJECT_ID}/media/folders" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

DELETE /admin/projects/:projectId/media/folders/:folderId

{ "data": { "deleted": true } }

Try it:

DELETE/admin/projects/%7B%7BprojectId%7D%7D/media/folders/%7B%7BfolderId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/admin/projects/%7B%7BprojectId%7D%7D/media/folders/%7B%7BfolderId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE "${BASE_URL}/admin/projects/${PROJECT_ID}/media/folders/{folderId}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

Catalogs

A catalog is a named, slug-addressable bundle of existing media assets your app reads in one call (Amba.media.catalog(slug) → client read). It curates assets by reference — the item URLs are the same public asset URLs every other media read returns, not copies.

POST /admin/projects/:projectId/media/catalogs

FieldTypeRequiredDefaultNotes
slugstringyes—/^[a-z0-9][a-z0-9-]{0,63}$/, unique per project
namestringyes—
descriptionstringnonull
is_publicbooleannotrueOnly public catalogs are readable client-side.
metadataobjectno{}

A duplicate slug returns 409 SLUG_TAKEN. Response 201 echoes the catalog row.

POST /admin/projects/:projectId/media/catalogs/:catalogId/items

FieldTypeRequiredDefaultNotes
media_iduuidyes—Must be an existing media asset.
positionnumberno0Lower sorts first.
captionstringnonullPer-catalog label for this asset.
metadataobjectno{}

Adding the same asset to a catalog twice returns 409 ALREADY_IN_CATALOG.

PATCH /admin/projects/:projectId/media/catalogs/:catalogId/items/reorder

Body { "items": [{ "media_id": "…", "position": 0 }, …] }. Each provided pair sets that item's position; assets not listed keep their current position.