Swayzio Core API v1 — base path /api/swayzio/v1
Authentication

Authentication

Every /v1 product endpoint is owner‑scoped: the server resolves the caller to a single owner (an account) and keys every database read and write on that owner. There is no way to address another account’s data by supplying an ID in a body or query — the owner is taken from the verified credential, never from request input.

Auth tiers

TierUsed byMechanism
Publichealth, all /v1/public/* share surfaces, the webhook pagesnone
Clerk actorevery /v1 product routeAuthorization: Bearer <Clerk JWT>
Entitlement‑gateduploads, sharing, search, AI, catalog‑analysisClerk actor + plan/feature check
Webhook signatureinbound webhooksprovider HMAC / signature header
Internal secret/v1/internal/*, media‑intelligence callbackAuthorization: Bearer <shared secret>

The actor (requireSwayzioActor)

Authenticated routes resolve a SwayzioActorContext:

type SwayzioActorContext = {
  actorId: string;       // the authenticated user (Clerk subject)
  ownerId: string;       // the account everything is scoped to
  ownerIdSource: string; // how ownerId was resolved (telemetry)
  email?: string;
  emails: string[];
  scopes: string[];
};

Resolution order (first match wins):

  1. Dev header x-swayzio-user-idnon‑production only. Sets ownerId === actorId, scope local:dev. Never honored when NODE_ENV === "production".
  2. Bearer JWTAuthorization: Bearer <token> verified via Clerk verifyToken.
  3. Clerk session via a configured JWT template (CLERK_JWT_TEMPLATE).
  4. Clerk publicMetadata.swayzioLegacyOwnerId (cached ~5 min) — links a modern Clerk user to a legacy owner id.
  5. Raw session claimAUTH_OWNER_CLAIM (default org_id).

If no actor can be resolved, the endpoint returns 401 { "error": "unauthorized" }.

⚠️

The dev header is a deliberate local‑development convenience. It is disabled in production. The Fastify Core API enforces the same NODE_ENV !== "production" gate (requireActor).

Owner‑scoping guarantee

  • Handlers inject ownerId: actor.ownerId into request schemas — they never read an owner from the body or query.
  • Repositories enforce WHERE owner_id = $1 on every query.
  • Responses strip ownerId (and storage URIs, provider fields) via client* / public* DTO variants — see Data Contracts.
  • Shared resources (a pack shared with you, a library you collaborate on) are reachable only through an accepted grant/share, graded server‑side (canEdit / canCollaborate / canAdmin). Public share links resolve a synthetic actor { actorId: "public", ownerId: <link owner> } so signing stays owner‑scoped.

Entitlement gates

Some actions require a feature/plan entitlement on top of a valid actor. A denied entitlement returns 403:

{
  "error": "entitlement_required",
  "code": "<feature_code>",
  "feature": "<feature>",
  "current": 3,
  "limit": 3,
  "planId": "<plan>",
  "status": "<status>",
  "upgradeRequired": true
}

Gated features include packs.create, packs.share, tracks.share, search.private, collaborators.invite, upload entitlements, and paid‑plan gates on catalog analysis and the AI chat endpoints. Internal staff (verified @swayzio.com email) are exempt from all gates.

Server‑to‑server secrets

Internal endpoints authenticate with a fixed bearer secret rather than a user token. These fail closed — a missing secret env var yields 401, never an open door.

SurfaceSecret env var
POST /v1/internal/catalog-drain/*, /v1/internal/workflows/trigger-statusSWAYZIO_TRIGGER_CALLBACK_SECRET (or TRIGGER_STATUS_CALLBACK_SECRET)
GET/POST /v1/internal/audio-embeddings/drainCRON_SECRET
POST /v1/media-intelligence/callbackMEDIA_INTELLIGENCE_CALLBACK_SECRET (+ contract‑version header)

See Internal & Services and Webhooks.