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
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. |
| GET | /client/media/catalogs/:slug | Read a public asset catalog by slug (no session required). |
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 | no |
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
200or204. The file is then served fromstorage_url. 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.404 BUCKET_NOT_FOUND— the named bucket doesn't exist.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 | no | (PUT only) Pins the allowed Content-Type for the upload. |
expires_in_seconds | number | no | URL lifetime. Defaults to ~600s for PUT, ~3600s for GET. Clamped to a max of 86400s (24h). |
Response 200
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 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.403 FORBIDDEN—PUTkey outside your prefix, orGETon 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):
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: