API Authentication

API keys

Format: sok_live_<32-char-nanoid> or sok_test_<32-char-nanoid>.

Generated in src/lib/auth/api-key.ts:

const random = nanoid(32);
const fullKey = `${prefix}${random}`;
const keyHash = sha256(fullKey);   // stored
const lastFour = random.slice(-4); // stored for display

Storage: the full key is never persisted — only key_hash (sha256) + last_four + key_prefix.

Create a key

From the dashboard: /overview/api-keys. Internally calls POST /api/internal/api-keys.

{ "name": "Production server", "environment": "live" }

Response — full key is returned once and never shown again:

{ "id": "key_...", "key": "sok_live_...", "name": "...", "environment": "live" }

Usage

curl -H "Authorization: Bearer sok_live_..." https://host/api/v1/emails

On every request, validateApiKey():

  1. Rejects keys without sok_live_ / sok_test_ prefix.
  2. Computes sha256 of incoming key, looks up api_keys where key_hash matches AND revoked_at IS NULL.
  3. Updates last_used_at fire-and-forget.

Environments

Prefix Behavior
sok_live_ Real AWS provider call, usage limit enforced, metered to billing period
sok_test_ Provider call skipped (providerMessageId = "test_msg_..."), usage limit bypassed

What test mode does — and doesn't — simulate

Test mode is send-suppression, not a separate sandbox. A sok_test_ send:

  • Skips the provider. No SES/SNS call; provider_response is { "simulated": true }. Nothing reaches a real inbox or phone — see it in the dashboard Test inbox.
  • Writes a real message row with environment: "test". Test and live data are strictly isolated: a test key can never read live messages, and vice versa.
  • Fires your webhooks (message.sent etc.) signed with your real secret — by design, so you can integration-test your receiver end-to-end. Fetch the message by id if your handler needs to distinguish test from live.
  • Respects suppressions, templates, scheduling, and validation exactly like live — a payload that 422s in test 422s in live.
  • Never touches billing. Test usage is metered separately (GET /v1/usage with a test key shows it), bypasses plan limits, and is excluded from overage reporting.

What test mode can't catch: provider-side rejections (SES content policies, carrier filtering, DNS problems). Verify a domain and send one live message before launch.

Revoking

Dashboard → delete → soft-delete via revoked_at timestamp. Revoked keys fail the isNull(revoked_at) filter in validateApiKey.

Auth failure

Missing/malformed Bearer or invalid key → 401 authentication_error:

{ "error": { "type": "authentication_error", "message": "Invalid or missing API key", "code": "UNAUTHORIZED" } }