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, and scopes every
list/read/delete to that user.
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
For single-asset operations (GET, download-url, playback, DELETE, complete, …) a user
caller may only act on assets they own; anything else returns 403 FORBIDDEN. A service caller
is trusted and skips this check — 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 the asset belongs to another user. |
| 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"
}