Public API Overview
Base URL: https://<host>/api/v1
All endpoints require a Bearer API key.
Authorization: Bearer sok_live_... # or sok_test_...
Content type: application/json.
Endpoints
The send + read surface, which every integration starts from:
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/emails |
Send one email |
| POST | /api/v1/emails/batch |
Send up to 100 emails |
| GET | /api/v1/emails |
List emails (cursor pagination) |
| GET | /api/v1/emails/{id} |
Retrieve one email |
| POST | /api/v1/sms |
Send one SMS |
| POST | /api/v1/sms/batch |
Send up to 100 SMS |
| GET | /api/v1/sms |
List SMS |
| GET | /api/v1/sms/{id} |
Retrieve one SMS |
The full surface — audiences, contacts, templates, domains, webhooks, tenants,
projects, keys, SMS registration, verifications, inbound, jobs, usage — is
listed in /api/openapi.json,
which is the authoritative contract.
Cross-cutting behavior
Every request passes through withApiAuth (src/lib/api/middleware.ts):
- Auth — validates the Bearer token against the
api_keystable (sha256 hash), then checks the key's scope, expiry, IP allowlist and the org's suspension status. - Rate limit — Upstash sliding 60-second windows, evaluated by
src/lib/api/rate-limit.ts. A per-plan bucket per org —free60 / min,payg600,pro600,enterprise6000 — and, only when the key hasrate_limit_per_minuteset, a per-key bucket checked first. Either one rejecting is429 RATE_LIMITED. Skipped (requests pass, noX-RateLimit-*headers) when the Upstash env vars are absent. Full detail: rate-limits.md. - Context — the handler receives
{ orgId, apiKeyId, environment, tenantId, projectId, scopes, ... }.
The nine Idempotency-Key-accepting POSTs (/emails, /sms, /emails/batch, /sms/batch, /phone-numbers, /brands, /campaigns, /verifications, /audiences/{id}/send) additionally:
- Idempotency — the header replays the stored response for 24h (
IDEMPOTENCY_TTL_HOURS, tunable to 168). Same key + different body is409 IDEMPOTENCY_MISMATCH. See idempotency.md.
Send endpoints additionally:
- Usage limit — free-tier caps are enforced in the live environment (
429 USAGE_LIMIT_EXCEEDED); metered tiers bill overage but stop at the org's spend cap (402 SPEND_CAP_EXCEEDED); the test environment never reaches a provider and is metered separately.
Response envelopes
Single resource:
{ "id": "msg_...", "channel": "email", "status": "sent", "created_at": "..." }
List:
{ "data": [...], "has_more": true, "next_cursor": "MjAyNi0wNC0yMlQxMDoxNDoyMi4xMTNafG1zZ18wMUhO..." }
next_cursor is an opaque compound key — base64url("{created_at ISO}|{id}"),
minted by src/lib/api/cursor.ts — not a message id. Pass it back verbatim as
?cursor=; it is null when has_more is false. A value that does not
decode is 422 INVALID_CURSOR, never silently page one.
Batch (/emails/batch, /sms/batch):
{
"data": [
{ "index": 0, "id": "msg_...", "status": "sent", "error": null },
{ "index": 1, "id": null, "status": "failed", "error": { "code": "SUPPRESSED", "message": "..." } }
],
"total": 2,
"succeeded": 1,
"failed": 1
}
The counter is succeeded, not successful. id is null for an item that
never produced a message row — see emails.md
for the four id / status combinations.
Error: see errors.md.
See also: authentication.md, emails.md, sms.md, rate-limits.md, idempotency.md.