Authentication

Project API keys for ingestion, JWT for dashboard / management, Deploy tokens for OTA. How to mint, where to use, and how to rotate each.

Sankofa uses three credential types. Each authenticates a different surface of the API — using the wrong type returns 401 Unauthorized with no recovery hint, so it's worth understanding which goes where before integrating.

The three credential types

CredentialFormatHeaderUsed for
Project API key (live)sk_live_ + 32 hex charsx-api-key: sk_live_…Production ingestion, decision handshake, Catch ingest
Project API key (test)sk_test_ + 32 hex charsx-api-key: sk_test_…Dev / staging ingestion against the test environment of the same project
User JWTStandard JWT, signed HMACAuthorization: Bearer <jwt>Dashboard + management API (flags / configs / surveys / projects / members CRUD)
Deploy tokensk_deploy_ + 64 hex charsAuthorization: Bearer sk_deploy_…Deploy / OTA release commands (sankofa release, sankofa patch)

Notes:

  • Demo project keys — demo projects expose a sentinel live key (sk_live_demo_disabled_…) that is intentionally rejected with 403 Forbidden. Demo apps must use sk_test_….
  • Region pinning — keys do not carry a region suffix. Regional projects use the same key format; routing happens at the ingress layer based on the project's stored region (eu-west-1, us-east-1, af-south-1, ap-southeast-1 for Enterprise; eu-west-1 default).

x-api-key (project API keys)

The header name is x-api-key. The engine's HTTP framework treats header names as case-insensitive, so X-API-Key, X-Api-Key, and x-api-key all work — but the canonical form is lowercase.

bash
curl -X POST https://api.sankofa.dev/api/v1/track \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_..." \
-d '{
  "event_name": "checkout_started",
  "distinct_id": "user_123"
}'

How project + environment are resolved

On every request the engine:

  1. Reads the x-api-key header

    If missing or empty: 401 Unauthorized.

  2. Looks up the key

    First as projects.api_key (live keys). If found, sets environment = "live". If not, looks up as projects.test_api_key and sets environment = "test". If neither matches: 403 Forbidden — Invalid API Key.

  3. Checks the demo-disabled sentinel

    If the resolved key has the sk_live_demo_disabled_ prefix, returns 403 Forbidden — live API key disabled for demo projects — use the test key.

  4. Validates Origin (browsers) or IP (servers)

    See "Origin and IP allowlists" below.

  5. Proceeds to the handler

    With the project + environment + tenant context attached.

The lookup is a single indexed query — typically < 1 ms. There's no Redis cache layer in the path; the database is the source of truth on every request.

Origin and IP allowlists

Each project has two optional comma-separated allow-lists you can configure from /dashboard/<project>/settings/security:

  • AuthorizedDomains — origins the project accepts (e.g. https://app.example.com,https://staging.example.com). Compared against the request's Origin header.
  • AuthorizedIPs — server IPs the project accepts. Compared against the resolved client IP when no Origin header is present.

Behavior:

Request has OriginAuthorizedDomainsResult
YesEmptyPass (no domain check)
YesNon-empty, contains originPass
YesNon-empty, missing origin403 Forbidden — Unauthorized Origin
Request has no OriginAuthorizedIPsResult
YesEmptyPass (no IP check)
YesNon-empty, contains client IPPass
YesNon-empty, missing client IP403 Forbidden — Unauthorized IP Address

Allowlists let you ship a sk_live_ key in a publicly-cacheable web bundle and still constrain where it can be used.

Authorization: Bearer (user JWTs)

Dashboard and management endpoints accept a user JWT signed with the project's auth secret:

bash
curl https://api.sankofa.dev/api/v1/switch/flags \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

The JWT carries the user ID, organization, project, and roles. It's normally minted at sign-in and stored client-side by the dashboard. For programmatic use (CI scripts that need to read or update flags), generate a long-lived JWT from /dashboard/<project>/settings/api-tokens.

JWT-authenticated endpoints return 401 Unauthorized for missing / expired / invalid signatures, and 403 Forbidden for valid JWTs whose role doesn't permit the action.

sk_deploy_ (Deploy tokens)

Deploy commands — sankofa release, sankofa patch, sankofa submit, sankofa preview, sankofa status — authenticate with a Deploy token, not a JWT. Tokens are project-scoped, optionally environment-scoped (live, test, or all), and stored hashed server-side (the cleartext value is shown once at mint time).

bash
# Mint from /dashboard/<project>/settings/deploy-tokens
export SANKOFA_DEPLOY_TOKEN=sk_deploy_...
sankofa release ios --rollout 25

Deploy tokens carry the metadata the Deploy module needs (project ID, environment scope, expiration, last-used timestamp) and cannot be used for ingestion or dashboard CRUD.

Key rotation

For project API keys (sk_live_… / sk_test_…):

  1. Mint the new key

    /dashboard/<project>/settings/api-keysRotate. The new key is shown once; the old key keeps working.

  2. Roll out the new key

    Update your apps' configs and redeploy. Both keys work simultaneously — there's no traffic gap.

  3. Wait for rollout to complete

    For mobile apps, wait for the store-rollout window (often 7–14 days for a forced upgrade).

  4. Revoke the old key

    /dashboard/<project>/settings/api-keysRevoke the old entry. Within ~1 minute, any remaining clients on the old key start getting 403 Invalid API Key.

Key rotation is non-disruptive if you do it in this order. There's no in-place rotation — the new key has a different value.

For Deploy tokens, the procedure is the same: mint a new token, swap your CI environment variable, then revoke the old token.

For user JWTs, expiration is handled at mint time (typical 24h–30 days depending on type). You don't manually rotate them.

Authentication errors

StatusHeader usedCause
401 Unauthorizedx-api-keyMissing or empty header
401 UnauthorizedAuthorizationMissing / expired / invalid JWT signature
403 Forbidden — Invalid API Keyx-api-keyKey doesn't match any project's api_key or test_api_key
403 Forbidden — live API key disabled for demo projects — use the test keyx-api-keyDemo project's sentinel live key was sent
403 Forbidden — Unauthorized Originx-api-keyOrigin not in AuthorizedDomains
403 Forbidden — Unauthorized IP Addressx-api-keyClient IP not in AuthorizedIPs
403 ForbiddenAuthorization (JWT)Valid JWT but role lacks permission

See Errors for the full status-code table.

What's next

Edit this page on GitHub