Build
Build with the Box Ox API
Send application mail, manage templates, and verify events from your own installation.
On this page
openapi.json is the OpenAPI 3.1 spec for /dev/v1/*. A running install also serves it at GET /dev/v1/openapi.json. This endpoint needs no key.
The inbox install on Cloudflare Workers Free supports incoming-mail access and webhooks. Outbound requests return feature_unavailable.
Direct SDK and CLI downloads#
Box Ox self-publishes packages at https://boxox.dev/downloads/#developer-tools. Use the downloaded packages directly. Download boxox-mail-0.2.0.tgz, verify it against the published SHA256SUMS.txt, then run npm install ./boxox-mail-0.2.0.tgz (or the equivalent in your package manager).
The installed package name is @boxox/mail. The imports in this guide stay the same. The CLI package boxox-cli-0.2.0.tgz bundles the SDK and WebSocket client. Install the local file or run the standalone boxox-cli-0.2.0.mjs with Node.js 22+.
Successful API sends appear in Sent and Developer logs. A received reply brings the conversation into Inbox. Mailbox copies have separate storage. Deleting one does not remove the API log.
A copy binds to an existing matching sender mailbox and follows its read grants. Without that mailbox, only the installation owner can read the copy.
Keys#
Send Authorization: Bearer <key>.
- A domain key (
bxk_…) acts on exactly one domain, within its permissions:send,read_log,read_received,templates,webhooks. - A management key (
bxm_…) can only use/dev/v1/keys. It mints, lists, rotates and revokes domain keys on its own domains.
Errors and pagination#
Every error has the form {"error": {"code", "message", "param"?}}. A rejected batch also returns errors: [{index, code, message, param?}].
Lists return {data, nextCursor}. To get the next page, pass nextCursor back as ?cursor=. nextCursor is null on the last page.
SDK#
import { BoxOx } from "@boxox/mail";
const mail = new BoxOx({ key: process.env.BOXOX_KEY, url: process.env.BOXOX_URL });
const { id } = await mail.messages.send(
{ from: "noreply@app1.com", to: "user@example.com", template: "welcome", variables: { name: "Ada" } },
{ idempotencyKey: "signup-123" },
);
for await (const m of mail.paginate(mail.messages.list, { status: "bounced" })) console.log(m.id);mail.request(operationId, {path, query, body}) calls any operation in the spec by its operationId.
Webhooks#
Box Ox signs deliveries in the Standard Webhooks format. Each delivery carries three headers: webhook-id, webhook-timestamp and webhook-signature. The signature is v1,<base64 HMAC-SHA256 of "id.timestamp.body">. During a secret rotation the header holds two space-separated signatures, one per secret.
To verify a delivery, pass the raw body, before any JSON parsing:
import { verifyWebhookRequest, WebhookVerificationError } from "@boxox/mail/webhooks";
export default {
async fetch(request: Request, env: { BOXOX_WEBHOOK_SECRET: string }) {
try {
const event = await verifyWebhookRequest(request, env.BOXOX_WEBHOOK_SECRET);
if (event.type === "message.received") {
// event.data is a ReceivedEmail
}
return new Response(null, { status: 204 });
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response("bad signature", { status: 401 });
throw err;
}
},
};By default, the verifier rejects a timestamp more than 5 minutes from now. Change the limit with toleranceSeconds.
Several deliveries can carry the same webhook-id, for example a replay. Use it to deduplicate.
Template design#
Template agents must complete the email design review before publishing. The current email skill includes the review and an HTML/plain-text invoice example.
For contributors: maintain the API contract
Maintain the API contract#
- The routes in
apps/worker/src/dev/*-routes.tsvalidate with Zod. apps/worker/src/dev/openapi.tsbuilds the spec from those same schemas, plus the response shapes.pnpm --filter @boxox/mail generatewritesdocs/api/openapi.jsonand the SDK types inpackages/sdk/src/schema.ts.- Run
pnpm --filter @boxox/mail run check-openapito check freshness, route coverage, and SDK types.
The check fails if either generated file is out of date. It also fails for a missing mounted /dev/v1 route or incompatible SDK types.
Do not edit openapi.json or schema.ts by hand. Change the route or openapi.ts, then run generate.
API endpoints#
Expand an operation for parameters and response codes. These 37 operations come from the same OpenAPI contract as the SDK.
messages
POST /dev/v1/messagesSend or schedule one message (needs `send`)
| Name | Location | Required | Description |
|---|---|---|---|
Idempotency-Key | header | Optional | Replays within 24 h return the original response; a different body gets 409. |
Response codes: 202, 400, 401, 403, 404, 409, 422, 429.
Use operation sendMessage with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/messagesMessage log, newest first (needs `read_log`)
| Name | Location | Required | Description |
|---|---|---|---|
status | query | Optional | |
tag | query | Optional | |
recipient | query | Optional | |
cursor | query | Optional | |
limit | query | Optional |
Response codes: 200, 400, 401, 403, 404.
Use operation listMessages with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/messages/batchSend up to 100 messages, all-or-nothing or partial (needs `send`)
| Name | Location | Required | Description |
|---|---|---|---|
Idempotency-Key | header | Optional | Replays within 24 h return the original response; a different body gets 409. |
Response codes: 202, 400, 401, 403, 404, 409, 422, 429.
Use operation sendBatch with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/messages/{id}One message with its status timeline and rendered body (needs `read_log`)
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation getMessage with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
PATCH /dev/v1/messages/{id}Move a scheduled message's send time (needs `send`)
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404, 409.
Use operation rescheduleMessage with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/messages/{id}/cancelCancel a scheduled message (needs `send`)
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404, 409.
Use operation cancelMessage with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
keys
GET /dev/v1/keysDomain keys on the management key's domains
Response codes: 200, 400, 401, 403, 404.
Use operation listKeys with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/keysCreate a domain key (management keys only)
Response codes: 201, 400, 401, 403, 404.
Use operation createKey with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/keys/{id}/rotateIssue a new secret; the old one works for the overlap window
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation rotateKey with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/keys/{id}/revokeRevoke a domain key at once
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation revokeKey with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
templates
GET /dev/v1/templatesLatest version of each template (needs `templates`)
| Name | Location | Required | Description |
|---|---|---|---|
limit | query | Optional | |
cursor | query | Optional |
Response codes: 200, 400, 401, 403, 404.
Use operation listTemplates with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/templatesCreate a template at version 1
Response codes: 201, 400, 401, 403, 404, 409.
Use operation createTemplate with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/templates/{name}The latest version, or ?version=
| Name | Location | Required | Description |
|---|---|---|---|
name | path | Required | |
version | query | Optional |
Response codes: 200, 400, 401, 403, 404.
Use operation getTemplate with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
PUT /dev/v1/templates/{name}Save a new version
| Name | Location | Required | Description |
|---|---|---|---|
name | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation updateTemplate with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
DELETE /dev/v1/templates/{name}Delete every version
| Name | Location | Required | Description |
|---|---|---|---|
name | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation deleteTemplate with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/templates/{name}/versionsEvery version, newest first
| Name | Location | Required | Description |
|---|---|---|---|
name | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation listTemplateVersions with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/templates/{name}/previewRender with sample variables
| Name | Location | Required | Description |
|---|---|---|---|
name | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation previewTemplate with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
webhooks
POST /dev/v1/webhooksAdd an HTTPS endpoint (needs `webhooks`)
Response codes: 201, 400, 401, 403, 404, 422.
Use operation createWebhook with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/webhooksEndpoints on the key's domain
Response codes: 200, 400, 401, 403, 404.
Use operation listWebhooks with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/webhooks/listenLive events on the key's domain over a WebSocket, for local development
Response codes: 101, 400, 401, 403, 404, 426.
Use operation listenWebhooks with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/webhooks/{id}One endpoint
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation getWebhook with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
PATCH /dev/v1/webhooks/{id}Change the URL or events, or enable/disable
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404, 422.
Use operation updateWebhook with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
DELETE /dev/v1/webhooks/{id}Delete an endpoint
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation deleteWebhook with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/webhooks/{id}/rotateNew signing secret; both sign during the overlap
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation rotateWebhookSecret with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/webhooks/{id}/testSend a webhook.test event
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 202, 400, 401, 403, 404, 409.
Use operation testWebhook with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/webhooks/{id}/deliveriesDelivery log, newest first
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required | |
status | query | Optional | |
cursor | query | Optional | |
limit | query | Optional |
Response codes: 200, 400, 401, 403, 404.
Use operation listWebhookDeliveries with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/webhooks/{id}/deliveries/{deliveryId}One delivery with its attempt log and payload
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required | |
deliveryId | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation getWebhookDelivery with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/webhooks/{id}/deliveries/{deliveryId}/replaySend a finished delivery again
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required | |
deliveryId | path | Required |
Response codes: 202, 400, 401, 403, 404, 409.
Use operation replayWebhookDelivery with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
received
GET /dev/v1/receivedMail received on the domain's app inboxes (needs `read_received`)
| Name | Location | Required | Description |
|---|---|---|---|
to | query | Optional | |
cursor | query | Optional | |
limit | query | Optional |
Response codes: 200, 400, 401, 403, 404.
Use operation listReceived with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/received/{id}Parsed fields, authentication, bodies and attachments
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation getReceived with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/received/{id}/rawA signed, expiring link to the original .eml
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required | |
expiresIn | query | Optional |
Response codes: 200, 400, 401, 403, 404, 409.
Use operation getReceivedRawUrl with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/received/{id}/attachments/{attachmentId}Download one attachment
| Name | Location | Required | Description |
|---|---|---|---|
id | path | Required | |
attachmentId | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation getReceivedAttachment with the SDK’s mail.download(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/received/raw/{token}The signed .eml link itself; no API key, the token is the credential
| Name | Location | Required | Description |
|---|---|---|---|
token | path | Required |
Response codes: 200, 404.
Use operation downloadReceivedRaw with the SDK’s mail.download(). Full request and response schemas are in the downloadable OpenAPI contract.
suppressions
GET /dev/v1/suppressionsThe domain's suppression list (needs `read_log`)
| Name | Location | Required | Description |
|---|---|---|---|
reason | query | Optional | |
cursor | query | Optional | |
limit | query | Optional |
Response codes: 200, 400, 401, 403, 404.
Use operation listSuppressions with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
POST /dev/v1/suppressionsSuppress an address (needs `send`)
Response codes: 201, 400, 401, 403, 404.
Use operation addSuppression with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
GET /dev/v1/suppressions/{email}Whether one address is suppressed
| Name | Location | Required | Description |
|---|---|---|---|
email | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation getSuppression with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
DELETE /dev/v1/suppressions/{email}Remove an address from our list (Cloudflare's still applies)
| Name | Location | Required | Description |
|---|---|---|---|
email | path | Required |
Response codes: 200, 400, 401, 403, 404.
Use operation removeSuppression with the SDK’s mail.request(). Full request and response schemas are in the downloadable OpenAPI contract.
Download the API contract#
Use the machine-readable specification to generate a client or import the Postman collection for your own installation.