RELIABLE DELIVERY

CapiMail API quickstart

The transactional email API is available at /api/v1/emails. These examples describe the implemented runtime contract. Start with a cm_test_ key: a valid test request is validated without creating a message, reserving credits, or sending email.

Authentication

Send the complete API key as a Bearer token. Keys belong to one organization, can be revoked or expired, and must remain in a server-side secret manager. Never expose one in browser code or a public repository.

Authorization: Bearer cm_test_your_key
Authorization: Bearer cm_live_your_key

POST /api/v1/emails requires email:send. GET /api/v1/emails/{message_id} accepts email:read or email:send. Invalid, expired, or revoked credentials return 401 unauthorized; a valid key without a required scope returns 403 insufficient_scope.

Send an email

A live request accepts 1–50 unique recipients and requires a verified sender domain, an approved organization, sufficient credits, and either html or text content.

curl -X POST https://capimail.com/api/v1/emails \
  -H "Authorization: Bearer cm_test_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@example.com",
    "to": ["customer@example.net"],
    "subject": "Welcome!",
    "html": "<h1>Welcome</h1>",
    "idempotency_key": "welcome-customer-123"
  }'

Live-key response · HTTP 202 Accepted

HTTP/1.1 202 Accepted
Content-Type: application/json
Cache-Control: no-store

{
  "message_id": "uuid",
  "status": "queued",
  "request_id": "uuid"
}

A 202 means the send is durably persisted with a credit reservation and a queued job; it does not mean delivered. Background processing performs the provider attempt. A repeated idempotency key returns the original message instead of sending twice, and provider acceptance is not the same as confirmed delivery.

Test-key response · HTTP 200 OK

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "status": "validated",
  "delivered": false,
  "message": "Request is valid. Test keys never queue, reserve credits, or deliver email.",
  "request_id": "uuid"
}

A test-key request creates no message, consumes no credits, and never reaches the delivery worker.

Integration examples

JavaScript / TypeScript

const response = await fetch("https://capimail.com/api/v1/emails", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.CAPIMAIL_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ from: "hello@your-verified-domain.com", to: ["customer@example.com"], subject: "Account update", text: "Your account was updated.", idempotency_key: "account-update-123" })
});
if (!response.ok) throw new Error(JSON.stringify(await response.json()));

Node.js

import https from "node:https";
// Use your preferred HTTP client and keep CAPIMAIL_API_KEY server-side.
// POST the same JSON contract to /api/v1/emails with the Bearer header.

Python

import os, requests
r = requests.post("https://capimail.com/api/v1/emails",
  headers={"Authorization": f"Bearer {os.environ['CAPIMAIL_API_KEY']}"},
  json={"from":"hello@your-verified-domain.com","to":["customer@example.com"],"subject":"Account update","text":"Your account was updated.","idempotency_key":"account-update-123"})
r.raise_for_status()

Retrieve email status

curl https://capimail.com/api/v1/emails/{message_id} \
  -H "Authorization: Bearer cm_live_your_key"

Possible states are queued, processing, provider_accepted, delivered, soft_bounced, hard_bounced, complained, failed, and unknown. Only an authenticated provider delivery event means delivered.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "email": {
    "id": "uuid",
    "status": "delivered",
    "sender": "hello@your-verified-domain.com",
    "subject": "Account update",
    "recipient_count": 1,
    "provider_message_id": "provider-id",
    "created_at": "2026-10-14T10:00:00.000Z",
    "events": [
      { "event_type": "delivery", "occurred_at": "2026-10-14T10:00:03.000Z" }
    ]
  },
  "request_id": "uuid"
}

Idempotency

Every send requires an idempotency_key of 8–128 characters using letters, numbers, dots, underscores, colons, or hyphens. Keep one key for one logical email and reuse it when retrying an uncertain request.

"idempotency_key": "invoice-paid:invoice_123"

Repeating an accepted key with the identical payload returns 202, the original message_id, and idempotent: true. Reusing that key with a different sender, recipient, subject, content, headers, or tags returns 409 idempotency_conflict.

HTTP/1.1 202 Accepted
Content-Type: application/json
Cache-Control: no-store

{
  "status": "queued",
  "message_id": "original-uuid",
  "provider_message_id": null,
  "idempotent": true,
  "request_id": "uuid"
}

Errors

API errors contain a stable machine-readable code, a safe message, and a top-level request_id for support and tracing.

HTTP/1.1 422 Unprocessable Content
Content-Type: application/json
Cache-Control: no-store

{
  "status": "error",
  "error": {
    "code": "validation_error",
    "message": "Request validation failed.",
    "details": [{ "path": "to.0", "message": "Invalid email address" }]
  },
  "request_id": "uuid"
}
HTTPMachine codeEndpointMeaning
400invalid_jsonPOSTThe request body is not valid JSON.
401unauthorizedPOST / GETThe API key is missing, invalid, expired, or revoked.
402insufficient_creditsPOSTThe organization does not have enough available credits.
403insufficient_scopePOST / GETThe key does not grant a required email scope.
403organization_not_approvedPOSTThe organization is not approved for live sending.
403domain_not_verifiedPOSTThe sender domain is not verified.
404not_foundGETNo tenant-scoped message exists with that ID.
409idempotency_conflictPOSTThe idempotency key was reused with a different payload.
413payload_too_largePOSTThe request exceeds the maximum accepted payload size.
422validation_errorPOSTOne or more request fields are invalid.
422recipient_suppressedPOSTOne or more recipients are on the suppression list.
429rate_limit_exceededPOSTA key or organization recipient limit was exceeded.
500backend_errorPOST / GETA configured backend operation failed.
503backend_not_configuredPOST / GETThe email backend is unavailable or incomplete.

Every public v1 response carries its numeric result in the HTTP status line and includes a traceable request_id in the JSON body. Error responses also include a stable machine-readable identifier in error.code. Clients should branch on the HTTP status or response.ok, then use error.code for application-specific handling.

Rate limits

Limits are enforced atomically by recipient count, independently for each API key and organization. The default deployment limits are 30 recipients per key per minute, 5 recipients per organization per minute, and 20 recipients per organization per UTC day; operators can change these values through environment configuration.

When any window would be exceeded, the request returns 429 rate_limit_exceeded. Wait until the relevant minute or UTC-day window resets, then retry with the same idempotency key. Invalid requests and test-key validation do not consume a rate-limit slot or credits. The introductory allowance is 100 accepted recipient sends once per eligible organization and does not reset monthly.

Domain verification and events

Add a domain in the dashboard, publish every DKIM record exactly, configure SPF/DMARC for your policy, then request a recheck. CapiMail verifies status through AWS SES; typing a domain never marks it verified. SES notifications are signature-checked, allow-listed by topic, processed idempotently, and used to advance delivery, bounce, complaint, and suppression state.

CapiMail | Newsletters people want to read