.md

HTTP API

One CloudFront host. One x-api-key header. One URL tree under /v1. The key's scopes decide which routes work. The machine-readable contract is /openapi.json. Language-tab examples live on /docs/reference.

Last updated

Base URL

SendBunny is self-hosted. There is no shared API host. Your base URL is https://<cloudfront-domain>/v1, shown on the dashboard API keys page. It is a CloudFront URL, never a Lambda function URL and never an execute-api hostname.

  • POST /api and /agent/* are gone. They return 404 with { "error" } pointing at /v1.
  • JSON in, JSON out. Content-Type: application/json.
  • Browse lists use cursor + limit (default 25, max 50).
  • MCP is not in this release. When it ships it will be POST /mcp on the same CloudFront host.

Authentication and scopes

Every request needs the key in x-api-key: sb... (Authorization: Bearer sb... also works). The key is checked at the edge before anything in your account runs. Keys are created by an ADMIN. A key cannot mint another key. Treat keys as server-side secrets.

ScopeRoutes
email:sendPOST /v1/emails, GET /v1/emails, GET /v1/emails/{id}, inbox compose
inbox:readconversations, messages, bodies, attachments, receiving status
inbox:replyPOST /v1/messages/{id}/reply
inbox:manageinbox addresses, receiving, trash/move/read-unread
campaigns:read / campaigns:writecampaign list, get, create, patch, start, cancel
audience:read / audience:writelists, contacts, imports, contact properties
templates:read / templates:writetemplate CRUD
domains:read / domains:writesender identities and domain status
suppression:writelist, add, and remove suppression

Existing keys with no scopes array are treated as email:send only. Default for a new key is also send-only unless the admin checks more boxes.

Errors

StatusMeaning
401Always { "error": "Invalid API key" } for missing, unknown, or revoked keys.
403Wrong scope, wrong domain, or unverified From.
400Bad input. The message names the field.
404Missing resource. Inbox items outside the key stay 404, not 403.
422Recipient suppressed, or a reply envelope cannot be derived.
409Campaign is not editable in its current status.
500Unexpected failure. Always { "error": "Internal error" }.
502SES rejected the send. Always { "error": "Send failed" }.

Send email

POST /v1/emails requires email:send. to is always an array. A string is a 400. Optional cc is also an array. Raw body (subject + html/text) or a template (templateId or templateAlias + optional data). Do not mix the two.

curl
curl -X POST 'https://YOUR-DISTRIBUTION.cloudfront.net/v1/emails' \
  -H 'x-api-key: sbYOUR_KEY_HERE' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "hello@yourdomain.com",
    "to": ["success@simulator.amazonses.com"],
    "subject": "Hello from SendBunny",
    "html": "<p>It works!</p>"
  }'

200: { "messageId": "<ses-id>", "id": "<ledger-id>" }. Look it up later with GET /v1/emails/{id} (ledger id or SES messageId). GET /v1/emails?root= lists the 90-day transactional log for one domain. These rows are not Inbox Sent.

Template merge data

data is a JSON object. Nested objects flatten to dot paths ({user:{name}} fills {{user.name}}). Numbers and booleans are stringified. Arrays and null are rejected (400 with invalid[]). {{email}} defaults to the first to address. A variable without an inline {{key|fallback}} must be present or the send is 400 with missing[].

Inbox

  • GET /v1/conversations?root=&folder= — browse. inbox:read.
  • GET /v1/conversations/{threadId} — messages in a thread.
  • GET /v1/messages/{id} and /body and /attachments/{n}.
  • POST /v1/messages/{id}/replyinbox:reply. Recipients are derived from the thread. Do not send to. Defaults to send; pass mode: draft to preview.
  • POST /v1/inbox/messages — new mail from an InboxAddress (email:send). Optional mode: draft.
  • POST /v1/inbox/actions — trash / move / read-unread. Requires inbox:manage.
  • GET|POST /v1/inbox/receiving — enable or disable inbound mail for a root.
  • GET|POST /v1/inbox/addresses and DELETE /v1/inbox/addresses/{address} — manage requires inbox:manage.

Campaigns, audience, templates, domains

These map 1:1 to the dashboard. Writes go through the same Lambda cores the UI uses. Campaign start and cancel require campaigns:write. Destructive audience deletes require audience:write and a root query so a key cannot wipe another domain.

  • Campaigns: GET|POST /v1/campaigns, GET|PATCH|DELETE /v1/campaigns/{id}, POST .../test, POST .../start, POST .../cancel.
  • Lists: GET|POST /v1/lists, GET|PATCH|DELETE /v1/lists/{id}, POST /v1/lists/{id}/imports/presign, GET|POST /v1/lists/{id}/imports.
  • Contacts: GET /v1/contacts?root= or ?listId=, POST /v1/contacts, DELETE /v1/contacts/{email}?root=.
  • Templates: GET|POST /v1/templates, GET|PATCH|DELETE /v1/templates/{id}. Aliases resolve this domain, then Shared.
  • Identities: GET|POST /v1/identities (create/get/delete only), PATCH /v1/identities/{identity}/tracking, POST .../refresh, DELETE /v1/identities/{identity}, GET /v1/domains/{root}/status.
  • Suppression: GET|POST /v1/suppression, DELETE /v1/suppression/{email} (suppression:write, account-wide).
  • Stats: GET /v1/stats/domain?root=&range= (14d / 1m / 3m / 1y / 5y).

Not in the API

  • Invite users, mint or revoke API keys, one-click Updates, onboarding wizard.
  • Realtime inbox websocket events (dashboard AppSync only).
  • MCP (POST /mcp) — later, same CloudFront host.