The Sendook API is a single Cloudflare Worker. Every route is JSON-in / JSON-out, every request authenticates with a Bearer token, and there are exactly two kinds of token: the master key and the per-agent API key.
The Worker is reached at whatever route is configured in wrangler.jsonc. For the hosted deployment that's:
https://api.sendook.com
For local development:
http://localhost:8787
Every protected route reads Authorization: Bearer <token>. There are two acceptable token kinds:
| Token kind | Where it comes from | What it can do |
|---|---|---|
| Master key | The MASTER_KEY Worker secret. | POST /agents, GET /agents, DELETE /agents/:id, plus everything the per-agent key can do (for any agent). |
| Per-agent API key | Returned exactly once by POST /agents. | All /agents/:id/* routes for its own agent. Cannot create or list agents. |
The agents directory is keyed on a SHA-256 HMAC of the per-agent key (peppered with API_KEY_HASH_PEPPER). The plain key is never stored.
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /health | none | { ok: true, version } for liveness probes. |
GET | /me | per-agent | Returns the agent identified by the bearer token. |
POST | /agents | master | Create a new agent. Returns the per-agent API key once. |
GET | /agents | master | List every non-tombstoned agent. |
GET | /agents/:id | master / agent's own key | Get a single agent. |
DELETE | /agents/:id | master | Tombstone the agent and purge its DO storage. |
POST | /agents/:id/messages/send | master / agent's own key | Send an email from the agent. |
GET | /agents/:id/messages | master / agent's own key | List the agent's messages (newest first). |
GET | /agents/:id/threads/:threadId | master / agent's own key | Get one thread plus its messages in order. |
POST | /agents/:id/webhooks | master / agent's own key | Create a webhook subscription. Returns the secret once. |
GET | /agents/:id/webhooks | master / agent's own key | List the agent's webhooks (no secrets). |
DELETE | /agents/:id/webhooks/:webhookId | master / agent's own key | Delete one webhook. |
GET | /agents/:id/webhooks/:webhookId/attempts | master / agent's own key | List delivery attempts for one webhook (last 100). |
Agent ids are 12 lowercase alphanumeric chars. UUIDs in webhook ids and thread ids use the standard 8-4-4-4-12 form.
Content-Type: application/json is required for POST/DELETE with a body.POST /agents, 4 KiB for webhook create, 1 MiB for POST .../messages/send.POST .../messages/send body validates with Zod: addresses are real email addresses, subject ≤ 998 chars, text/html non-empty when present, recipients ≤ 50 total, attachments ≤ 10 with each data ≤ ~5 MiB raw and a strict base64 character set.200 OK (read), 201 Created (create), 202 Accepted (send queued / message persisted), 204 No Content (delete).502 Bad Gateway is returned for POST .../messages/send when every recipient was rejected (the per-recipient outcomes are still in the body).application/json with { "error": "<message>" } and, for Zod failures, an additional details array of issues.400 for validation, 401 for missing/bad bearer, 403 for cross-agent access, 404 for unknown id (or tombstoned agent), 500 for unexpected.{
"error": "invalid request body",
"details": [
{ "code": "invalid_type", "expected": "string", "path": ["subject"] }
]
}
OPTIONS preflights and the Access-Control-Allow-Origin header are emitted only when the request Origin matches one of the comma-separated values in the ALLOWED_ORIGINS Worker var. Anything else is silently denied — the browser sees no CORS headers and rejects the request. Set ALLOWED_ORIGINS=* to allow any origin (only do this for self-hosted deployments behind another auth layer).