Amba

Media

Read media assets + upload user-owned media (avatars, user content).

Client uploads are scoped under <projectId>/user/<appUserId>/<ms>_<filename> in the storage path — they can't collide with admin-uploaded assets.

How signed URLs work

Every upload and private-download URL Amba returns carries a freshly minted, single-purpose token — not a long-lived link persisted in your database. The token is:

  • Per-request. A new one is minted each time you register an upload or request a download URL. You don't store URLs; you mint them on demand.
  • Scoped to one object + one operation. The token is bound to the exact storage object and to a single verb — an upload token can only PUT that one object, a download token can only GET it. It can't be replayed against a different file, another user's object, or the opposite operation.
  • Short-lived. Upload URLs default to ~10 minutes, download URLs to ~1 hour. Both are capped at a hard maximum of 24 hours. When a URL expires, you mint another.
  • Effectively revocable by expiry. Because URLs are short-lived and minted per request rather than persisted, no stale, indefinitely-valid link ever circulates — you don't maintain a list of live URLs to invalidate.

You can tune the lifetime per request with expires_in_seconds (see Signing an upload / download URL). The bytes are served from https://<project>.cdn.amba.host/...; storage credentials are never exposed to the client.

Endpoints

MethodPathDescription
GET/client/media/:assetIdFetch a single asset (public columns).
GET/client/mediaPaginated asset list; optional ?folder_id=.
POST/client/media/uploadRegister an upload; returns upload_url.
POST/client/media/presignMint a short-lived signed upload (PUT) or download (GET) URL for an object you own.
GET/client/media/catalogs/:slugRead a public asset catalog by slug (no session required).

GET /client/media/:assetId

Response 200

{
  "data": {
    "id": "…",
    "filename": "…",
    "mime_type": "image/png",
    "storage_url": "…",
    "thumbnail_url": null,
    "alt_text": null,
    "metadata": {}
  }
}

Errors

  • 404 NOT_FOUND.

Try it:

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

Curl:

curl -X GET '${BASE_URL}/client/media/{assetId}' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}'

GET /client/media

Query

ParamDefaultDescription
limit50
offset0
folder_idFilter.

Response 200

{
  "data": [{ "id": "…", "filename": "…", "storage_url": "…" }],
  "total": 42,
  "offset": 0,
  "limit": 50
}

Try it:

GET/client/media
client auth
curl -X GET 'https://api.amba.dev/v1/client/media'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET '${BASE_URL}/client/media' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}'

POST /client/media/upload

Request

FieldTypeRequired
filenamestringyes
mime_typestringyes
size_bytesnumberno

Response 201

{
  "data": {
    "id": "…",
    "filename": "…",
    "storage_url": "https://{projectId}.cdn.amba.host/media/…",
    "uploaded_by": "<appUserId>"
  },
  "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 in the upload call — a mismatch is rejected.
  • No Authorization or X-Api-Key header on the upload PUTupload_url is already authorized.
  • Success is 200 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 upload again to get a fresh one.
  • Maximum file size for the media bucket is 50 MB. Uploads with a mime_type outside the bucket allow-list return 400 MIME_NOT_ALLOWED.

Errors

  • 400 MIME_NOT_ALLOWED — the mime_type is not in the bucket's allow-list.
  • 404 BUCKET_NOT_FOUND — the named bucket doesn't exist.
  • 500 CREATE_FAILED.

Try it:

POST/client/media/upload
client auth
curl -X POST 'https://api.amba.dev/v1/client/media/upload'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl (end-to-end):

# 1. Register the upload
RESP=$(curl -s -X POST '${BASE_URL}/client/media/upload' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{"filename":"avatar.jpg","mime_type":"image/jpeg"}')
 
# 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/jpeg' --data-binary @avatar.jpg
 
# 4. Fetch the asset
curl -L "$STORAGE_URL" -o downloaded.jpg

POST /client/media/presign

Mint a short-lived signed URL for a single object you own — either to PUT bytes (upload) or to GET a private object (download). This is the on-demand way to get a signed URL when you already know the object's bucket + key, rather than going through the upload-registration flow.

Request

FieldTypeRequiredDescription
bucketstringyesThe bucket the object lives in.
keystringyesThe object key. For PUT, must start with user/<your-user-id>/ — you can only upload under your own prefix.
method"GET" | "PUT"yesGET mints a download URL; PUT mints an upload URL.
content_typestringno(PUT only) Pins the allowed Content-Type for the upload.
expires_in_secondsnumbernoURL lifetime. Defaults to ~600s for PUT, ~3600s for GET. Clamped to a max of 86400s (24h).

Response 200

{
  "data": {
    "url": "https://<project>.cdn.amba.host/…?token=…",
    "expires_at": "2026-05-28T12:05:00Z"
  }
}
  • GET enforces ownership against the media_assets row — a key that isn't yours returns 403 FORBIDDEN (or 404 NOT_FOUND if no matching asset exists). A client can never sign a URL for another user's object.
  • PUT requires the key to be under your user/<your-user-id>/ prefix; otherwise 403 FORBIDDEN.
  • The returned url is already authorized by the embedded token — PUT/GET it directly with no Authorization or X-Api-Key header. After expires_at it stops working; mint a fresh one.

Errors

  • 400 INVALID_METHODmethod isn't "GET" or "PUT".
  • 400 INVALID_BODYbucket / key missing or not strings.
  • 403 FORBIDDENPUT key outside your prefix, or GET on another user's object.
  • 404 BUCKET_NOT_FOUND / 404 NOT_FOUND — bucket or asset doesn't exist.
  • 503 TENANT_UNAVAILABLE — storage still provisioning.

Curl (sign a 5-minute download URL):

curl -X POST '${BASE_URL}/client/media/presign' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}' \
  -H 'Authorization: Bearer ${SESSION_TOKEN}' \
  -H 'Content-Type: application/json' \
  -d '{"bucket":"media","key":"user/<your-user-id>/avatar.jpg","method":"GET","expires_in_seconds":300}'

Catalogs

A catalog is a curated, slug-addressable bundle of media assets, configured in the admin API. Your app reads one in a single call — typically at boot — to load a fixed set of images (onboarding illustrations, a sticker pack, home-screen heroes) in a stable order.

GET /client/media/catalogs/:slug

Resolve a public catalog by slug. Requires the client API key but no end-user session — this is public, curated content. Only catalogs published with is_public: true are readable; anything else returns 404 NOT_FOUND (a draft catalog never leaks its existence). The catalog JSON is client-cacheable (Cache-Control: private, max-age=300) — private because the catalog is scoped to your project, so it's cached on the calling device, never in a shared cache. The asset bytes at each url stay CDN-cached independently.

Response 200

{
  "data": {
    "slug": "onboarding-art",
    "name": "Onboarding Illustrations",
    "description": null,
    "metadata": {},
    "updated_at": "2026-06-05T12:00:00Z",
    "items": [
      {
        "media_id": "…",
        "position": 0,
        "caption": "Welcome screen",
        "metadata": {},
        "filename": "welcome.png",
        "mime_type": "image/png",
        "url": "https://{projectId}.cdn.amba.host/media/welcome.png",
        "thumbnail_url": null,
        "alt_text": null,
        "blurhash": null
      }
    ]
  }
}

Items are ordered by position. Each url is a stable, long-cacheable public asset URL.

SDK:

const catalog = await Amba.media.catalog('onboarding-art');
catalog.items.forEach((item) => render(item.url, item.caption));

Curl:

curl -X GET '${BASE_URL}/client/media/catalogs/onboarding-art' \
  -H 'X-Api-Key: ${CLIENT_API_KEY}'

On this page