Authentication
The two authentication schemes — user JWT and internal service key — plus ownership rules and error codes.
Every /api/v1/media/* endpoint is guarded. A request authenticates with either a user access
token or an internal service key — you never send both. Playback
and webhook endpoints are not behind this guard (they rely on
asset visibility and provider signatures respectively).
Scheme 1 — User JWT (BearerAuth)
Send a Shukr user access token as a bearer token:
Authorization: Bearer <access_token>The token is verified locally (HS256, no network round-trip). It must:
- be signed with the service's
ACCESS_TOKEN_SECRET, - carry a
user_idclaim, and - have
type: "access"— refresh tokens are rejected.
The resolved user_id becomes the owner of any asset created in that request. It also scopes the
asset list, single-asset GET, and all write operations (complete, abort-upload,
upload-parts, DELETE) to that user. Media reads (download-url, playback) are not
owner-scoped — any authenticated caller may read any asset.
Scheme 2 — Service key (ServiceKeyAuth)
Internal services authenticate with a shared API key header (case-insensitive):
X-Service-Key: <service_api_key>Because there is no user in the token, a service caller must name the owner explicitly by
passing owner_user_id — in the request body for writes, or as a query parameter for GET /media.
Omitting owner_user_id on a service-key request fails validation with 400 BAD_REQUEST.
Service-key callers also bypass per-asset ownership checks, so treat the key as a trusted
server-to-server secret and never ship it to a browser or mobile client.
Same call, both schemes
curl -X POST https://media.example.com/api/v1/media/upload-url \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "filename": "photo.jpg", "content_type": "place", "mime_type": "image/jpeg" }'Ownership
Write and management operations on a single asset — complete, abort-upload, upload-parts,
DELETE, and GET /media/{id} — are owner-scoped: a user caller may only act on assets they
own, and anything else returns 403 FORBIDDEN.
Media reads — download-url and playback, plus their batch forms — are not
owner-restricted. Any authenticated caller may read any asset (authentication is still required;
there is no anonymous access). Reads never return 403 for ownership.
A service caller is trusted and skips the ownership check on the write/management ops — which is
exactly why it must state the owner_user_id.
Error codes
Authentication and authorization failures use the standard error envelope.
The error.error_code field is stable and safe to switch on:
| HTTP | error_code | When |
|---|---|---|
| 400 | BAD_REQUEST | Missing/invalid input — e.g. a service call without owner_user_id, or a disallowed mime_type. |
| 401 | UNAUTHORIZED | No credentials, malformed token, wrong secret, refresh token used, or bad service key. |
| 403 | FORBIDDEN | Authenticated, but you don't own the asset — on a write/management op (complete, abort-upload, upload-parts, DELETE, GET /media/{id}). Media reads (download-url, playback) never 403 on ownership. |
| 404 | NOT_FOUND | Asset (or route) does not exist. |
| 500 | INTERNAL_ERROR | Unexpected server error. |
{
"success": false,
"message": "Missing or invalid access token",
"code": 401,
"error": { "error_code": "UNAUTHORIZED", "details": null },
"request_id": "req_01J9Z0X8Q2",
"timestamp": "2026-07-29T04:15:30.000Z"
}