Media
Read media assets + upload user-owned media (avatars, user content).
Client uploads are scoped under <projectId>/user/<appUserId>/<ms>_<id>_<name> in the storage path — they can't collide with admin-uploaded assets. <name> is a URL-safe form of the upload's filename (its last path component, accents dropped, every character other than letters, digits, ., _ and - replaced by _, the extension kept); the asset's filename stays exactly as sent.
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
PUTthat one object, a download token can onlyGETit. 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
| Method | Path | Description |
|---|---|---|
| GET | /client/media/:assetId | Fetch a single asset (public columns). |
| GET | /client/media | Paginated asset list; optional ?folder_id=. |
| POST | /client/media/upload | Register an upload; returns upload_url. |
| POST | /client/media/presign | Mint a short-lived signed upload (PUT) or download (GET) URL for an object you own. |
| POST | /client/media/commit | Confirm an upload arrived; returns the asset. |
| GET | /client/media/catalogs/:slug | Read a public asset catalog by slug (no session required). |
| DELETE | /client/media/:assetId | Delete one of your assets and its stored file. |
Reads return assets whose upload finished. An upload is pending from
registration until its file arrives; until then GET /client/media/:assetId
answers 404, the list leaves it out, a download presign answers 404, and
it counts toward no media usage. A GET by id that finds the file commits
the asset. A pending upload whose file never arrives is deleted by the daily
cleanup once it is 24 hours old. See
When an upload counts.
GET /client/media/:assetId
Response 200
Errors
404 NOT_FOUND.
Try it:
/client/media/%7B%7BassetId%7D%7Dcurl -X GET 'https://api.amba.dev/v1/client/media/%7B%7BassetId%7D%7D'Curl:
GET /client/media
Query
| Param | Default | Description |
|---|---|---|
limit | 50 | |
offset | 0 | |
folder_id | — | Filter. |
Response 200
Try it:
/client/mediacurl -X GET 'https://api.amba.dev/v1/client/media'Curl:
POST /client/media/upload
Request
| Field | Type | Required |
|---|---|---|
filename | string | yes |
mime_type | string | yes |
size_bytes | number | on free projects (the file's exact size in bytes) |
On a free project the upload URL accepts at most size_bytes bytes and the
size counts toward the 250 MB media budget.
Response 201
Uploading the file
PUT the raw file bytes to the returned upload_url:
- Method is
PUT; the request body is the file bytes. Content-TypeMUST match themime_typeregistered in the upload call — a mismatch is rejected.- No
AuthorizationorX-Api-Keyheader on the uploadPUT—upload_urlis already authorized. - Success is
200,201or204. The file is then served fromstorage_url, and the asset is listed and returned byGET. upload_urlis short-lived (upload_url_expires_at); if it has expired, register the upload again to get a fresh one.- Maximum file size for the
mediabucket is 50 MB. Uploads with amime_typeoutside the bucket allow-list return400 MIME_NOT_ALLOWED.
Errors
400 MIME_NOT_ALLOWED— themime_typeis not in the bucket's allow-list.400 SIZE_REQUIRED— a free project registered an upload withoutsize_bytes.402 FREE_TIER_LIMIT_REACHED(details.limit: "media_storage") — the upload would take a free project past its media budget. Delete media to free space, or upgrade.404 BUCKET_NOT_FOUND— the named bucket doesn't exist.404 USER_NOT_FOUND— the signed-in user was deleted; a deleted user's session registers no asset.500 CREATE_FAILED.
Try it:
/client/media/uploadcurl -X POST 'https://api.amba.dev/v1/client/media/upload'Curl (end-to-end):
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
| Field | Type | Required | Description |
|---|---|---|---|
bucket | string | yes | The bucket the object lives in. |
key | string | yes | The object key. For PUT, must start with user/<your-user-id>/ — you can only upload under your own prefix. |
method | "GET" | "PUT" | yes | GET mints a download URL; PUT mints an upload URL. |
content_type | string | PUT only | (PUT only, required) The upload's type. The upload must send this Content-Type, and the asset is served as it. |
expires_in_seconds | number | no | URL lifetime. Defaults to ~600s for PUT, ~3600s for GET. Clamped to a max of 3600s (1h) for PUT and 86400s (24h) for GET. |
Response 200
asset_id is present on a PUT.
GETenforces ownership against themedia_assetsrow — a key that isn't yours returns403 FORBIDDEN(or404 NOT_FOUNDif no matching asset exists). A client can never sign a URL for another user's object.PUTrequires thekeyto be under youruser/<your-user-id>/prefix; otherwise403 FORBIDDEN. The key must name a file, and none of its segments may be empty,.or..(plain or percent-encoded, such as%2e%2e).PUTrequirescontent_type, which must be on the bucket's MIME allow-list when the bucket has one.PUTregisters the object as your asset and returns itsasset_id. The asset is pending until the upload arrives, then it is listed byGET /client/media, readable by id, downloadable with aGETpresign, deletable withDELETE /client/media/:assetId, counted as media storage, and erased when the user is deleted, exactly like an asset from the upload-registration flow. Presigning the samekeyagain returns the sameasset_id, records the newcontent_typeand restarts the asset's retention.- On a free project,
PUTreturns400 PRESIGN_PUT_NOT_AVAILABLE: register uploads with the SDK mode of this route (bucket,filename,mime_type,size_bytes) orPOST /client/media/upload, so the size counts toward the media budget.GETworks on every plan. - The returned
urlis already authorized by the embedded token —PUT/GETit directly with noAuthorizationorX-Api-Keyheader. Afterexpires_atit stops working; mint a fresh one.
Errors
400 INVALID_METHOD—methodisn't"GET"or"PUT".400 INVALID_BODY—bucket/keymissing or not strings,content_typemissing on aPUT, or aPUTkey that names no file, has an empty,.or..segment, or contains?,#,%,\or a control character (each would make the asset's URL address a different object).400 MIME_NOT_ALLOWED—PUTwith acontent_typeoutside the bucket's MIME allow-list.403 FORBIDDEN—PUTkey outside your prefix, orGETon another user's object.404 BUCKET_NOT_FOUND/404 NOT_FOUND— bucket or asset doesn't exist.404 USER_NOT_FOUND— SDK mode andmethod: "PUT": the signed-in user was deleted; a deleted user's session gets no upload URL.409 KEY_IN_USE—PUTon a key another uploader's asset already uses.503 TENANT_UNAVAILABLE— storage still provisioning.
Curl (sign a 5-minute download URL):
POST /client/media/commit
Confirm that an upload's file arrived. The SDK calls it after its PUT
(storage.commit, and the last step of storage.upload). The asset's file
must exist with a non-zero size; the response is the asset with its stored
size_bytes. Committing an asset that is already committed answers 200
again.
Request
| Field | Type | Required | Description |
|---|---|---|---|
upload_id | string | yes | From the presign response. Equal to asset_id. |
asset_id | string | yes | The asset to commit; it must be your own upload. |
Response 200
Errors
400 INVALID_INPUT—upload_idorasset_idmissing, or different.404 NOT_FOUND— no live asset with this id belongs to the signed-in user.409 UPLOAD_NOT_FOUND— no file has arrived for this asset (or it is empty). The asset stays pending:PUTthe file to its upload URL, then commit again. Once the upload URL has expired (about 10 minutes), register the upload again.503 UPLOAD_UNVERIFIED— the file could not be checked right now. Retry the commit.
DELETE /client/media/:assetId
Deletes one of the signed-in user's assets. The stored file is deleted before the response, so the asset's URL stops serving once cached copies expire (about 5 minutes at the edge for original files). From the delete on, the asset is gone from every read and stops counting toward media usage. Its hidden record is removed by a daily cleanup 7 to 8 days after the delete.
Response 200
Errors
404 NOT_FOUND— no live asset with this id belongs to the signed-in user.409 UPLOAD_WINDOW_OPEN— free projects only: the asset's upload URL can still write, so the delete waits. Retry after theRetry-Afterseconds.503 STORAGE_DELETE_FAILED— the stored file could not be deleted right now. The asset stays as it was; retry the delete.
Curl:
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
Items are ordered by position. Each url is a stable, long-cacheable public
asset URL.
SDK:
Curl: