Shukr Docs
Shukr Public Media Service

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_id claim, 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 readsdownload-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:

HTTPerror_codeWhen
400BAD_REQUESTMissing/invalid input — e.g. a service call without owner_user_id, or a disallowed mime_type.
401UNAUTHORIZEDNo credentials, malformed token, wrong secret, refresh token used, or bad service key.
403FORBIDDENAuthenticated, 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.
404NOT_FOUNDAsset (or route) does not exist.
500INTERNAL_ERRORUnexpected 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"
}

On this page