Reference

API reference

Every endpoint, parameter, and response. Base URL https://api.sendara.dev.

All requests are made over HTTPS to https://api.sendara.dev and authenticated with a Bearer key. Timestamps are RFC 3339; monetary amounts are micro-dollars (1,000,000 = $1.00). Path parameters are shown in {braces}.

Send

Send messages across channels — one at a time or in batches.

Send a message

POST/v1/sendsend scope

Send a single message on any channel. An idempotency key is required — retrying with the same key returns the original result instead of sending twice.

Body parameters
channelstringRequired
One of email, sms, push, voice, webhook.
idempotency_keystringRequired
Unique key per logical send. Retries with the same key are deduplicated.
destinationobjectRequired
Channel-specific recipient. email → { email }, sms/voice → { phone_number }, push → { device_token }, webhook → { url }.
payloadobjectRequired
Channel-specific content. email → { subject, body_html, body_text }, sms → { body }, push → { title, body }.
message_typestringOptional
transactional (default) or marketing.
metadataobjectOptional
Per-send options, e.g. email { from_email }, sms { sender_id }.
template_idstringOptional
Render a stored template instead of an inline payload.
template_varsobjectOptional
Variables interpolated into the template.
Example request
curl https://api.sendara.dev/v1/send \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "email",
    "idempotency_key": "evt_welcome_8f3a",
    "message_type": "transactional",
    "destination": { "email": "user@example.com" },
    "payload": {
      "subject": "Welcome to Acme",
      "body_html": "<h1>Welcome 🎉</h1>"
    },
    "metadata": { "from_email": "hello@acme.com" }
  }'
Response · 201
{
  "id": "msg_a1b2c3",
  "status": "queued",
  "channel": "email",
  "idempotency_key": "evt_welcome_8f3a",
  "created_at": "2026-06-14T10:00:00Z"
}

Send a batch

POST/v1/send/batchsend scope

Send many messages in one call. Each item is processed independently; the response preserves request order with per-item success or error. Partial success is normal.

Example request
curl https://api.sendara.dev/v1/send/batch \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '[
    { "channel": "email", "idempotency_key": "b1",
      "destination": { "email": "a@acme.com" },
      "payload": { "subject": "Hi", "body_html": "<p>Hi</p>" } },
    { "channel": "email", "idempotency_key": "b2",
      "destination": { "email": "b@acme.com" },
      "payload": { "subject": "Hi", "body_html": "<p>Hi</p>" } }
  ]'
Response · 200
[
  { "success": true,
    "response": { "id": "msg_1", "status": "queued", "channel": "email",
      "idempotency_key": "b1", "created_at": "2026-06-14T10:00:00Z" } },
  { "success": false,
    "error": { "code": "recipient_suppressed",
      "message": "Recipient is on the suppression list", "status": 409 } }
]

Broadcasts

Bulk email campaigns to an audience or an inline recipient list.

Send a bulk email

POST/v1/send/bulksend scope

Send one email to many recipients in a single call. Provide an audience_list_id (a contact list) or an inline recipients array, plus inline content (subject + body_html) or a template_id rendered per recipient. Fans out asynchronously and returns a broadcast to poll. Suppressed and unsubscribed recipients are skipped; each recipient is idempotent.

Body parameters
from_emailstringRequired
Verified sending address.
subjectstringOptional
Inline subject (or use template_id).
body_htmlstringOptional
Inline HTML body; supports {{variables}}.
template_idstringOptional
Render a stored template per recipient instead of inline content.
audience_list_idstringOptional
A contact list (static or dynamic) to send to.
recipientsarrayOptional
Inline recipients [{ email, data }] — an alternative to audience_list_id.
message_typestringOptional
marketing (default) or transactional.
Example request
curl https://api.sendara.dev/v1/send/bulk \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "from_email": "hello@mail.acme.com",
    "subject": "News from Acme",
    "body_html": "<h1>Hi {{first_name}}</h1>",
    "audience_list_id": "list_9f21"
  }'
Response · 202
{
  "id": "bc_a1b2c3",
  "name": "Bulk send",
  "status": "sending",
  "from_email": "hello@mail.acme.com",
  "total_recipients": 0,
  "sent_count": 0,
  "failed_count": 0,
  "created_at": "2026-06-14T10:00:00Z"
}

List broadcasts

GET/v1/broadcastsread scope

Return the account's broadcasts, newest first.

Response · 200
{
  "broadcasts": [
    { "id": "bc_a1b2c3", "name": "October newsletter", "status": "sent",
      "from_email": "hello@mail.acme.com", "total_recipients": 1840,
      "sent_count": 1840, "failed_count": 0, "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Get a broadcast

GET/v1/broadcasts/{id}read scope

Return a broadcast with aggregate delivery stats.

Response · 200
{
  "broadcast": { "id": "bc_a1b2c3", "status": "sent", "total_recipients": 1840 },
  "stats": { "total": 1840, "sent": 1840, "delivered": 1798,
             "opened": 642, "bounced": 31, "complained": 2, "failed": 0 }
}

Messages

Read sent messages and their event timeline.

List messages

GET/v1/messagesread scope

Return a list of message summaries, filtered by channel, status, and time range.

Query parameters
channelstringOptional
Filter by channel.
statusstringOptional
Filter by status, e.g. delivered or bounced.
fromstringOptional
RFC 3339 lower bound (inclusive).
tostringOptional
RFC 3339 upper bound (inclusive).
Response · 200
{
  "messages": [
    { "id": "msg_a1b2c3", "channel": "email", "status": "delivered",
      "message_type": "transactional", "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Get a message

GET/v1/messages/{id}read scope

Retrieve a single message with its full event timeline.

Response · 200
{
  "id": "msg_a1b2c3",
  "channel": "email",
  "status": "delivered",
  "message_type": "transactional",
  "created_at": "2026-06-14T10:00:00Z",
  "events": [
    { "id": "ev_1", "type": "accepted", "occurred_at": "2026-06-14T10:00:00Z" },
    { "id": "ev_2", "type": "delivered", "occurred_at": "2026-06-14T10:00:12Z" }
  ]
}

Usage

Account usage and cost for a billing period.

Get usage

GET/v1/usageread scope

Return the send count and cost (in micro-dollars) for the period, broken down by channel.

Query parameters
periodstringOptional
Billing period as YYYY-MM. Defaults to the current period.
Response · 200
{
  "period": "2026-06",
  "total_send_count": 18420,
  "total_cost_micros": 9210000,
  "channels": [
    { "channel": "email", "send_count": 18000, "cost_micros": 9000000 },
    { "channel": "sms", "send_count": 420, "cost_micros": 210000 }
  ]
}

Domains

Add and verify sending domains. Requires the admin scope.

Add a sending domain

POST/v1/domainsadmin scope

Register a domain and receive the DNS records to publish: three DKIM CNAMEs, a custom MAIL FROM (MX + SPF), and DMARC.

Body parameters
domainstringRequired
The domain or subdomain to send from, e.g. mail.acme.com.
Example request
curl https://api.sendara.dev/v1/domains \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "mail.acme.com" }'
Response · 201
{
  "id": "dom_1",
  "domain": "mail.acme.com",
  "dkim_status": "pending",
  "spf_status": "pending",
  "dmarc_status": "pending",
  "dns_records": [
    { "type": "CNAME", "name": "s1._domainkey.mail.acme.com",
      "value": "s1.dkim.sendara.dev" }
  ],
  "mail_from_domain": "mail.acme.com",
  "created_at": "2026-06-14T10:00:00Z"
}

Verify a domain

POST/v1/domains/{domain}/verifyadmin scope

Re-check DNS status and return the per-record verification result.

Response · 200
{
  "domain": "mail.acme.com",
  "fully_verified": true,
  "results": [
    { "field": "dkim", "type": "CNAME", "name": "s1._domainkey.mail.acme.com",
      "status": "verified", "detail": "" }
  ]
}

API keys

Create, rotate, and revoke API keys. Requires the admin scope.

Create an API key

POST/v1/keysadmin scope

Create a scoped key. The plaintext secret is returned exactly once — store it securely.

Body parameters
scopestringOptional
send, read, or admin.
test_modebooleanOptional
Issue a test-mode key (sk_test_…). Defaults to false.
Example request
curl https://api.sendara.dev/v1/keys \
  -H "Authorization: Bearer sk_live_admin_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "scope": "send", "test_mode": false }'
Response · 201
{
  "id": "key_1",
  "key": "sk_live_5f3a39ab8c2d4e6f",
  "key_prefix": "sk_live_5f3a",
  "scope": "send",
  "test_mode": false,
  "created_at": "2026-06-14T10:00:00Z"
}

Rotate an API key

POST/v1/keys/{id}/rotateadmin scope

Issue a new secret for the key and invalidate the old one. The new plaintext key is returned once.

Response · 200
{ "key": "sk_live_9b2c0a17e4d5f6a8" }

Revoke an API key

DELETE/v1/keys/{id}admin scope

Permanently revoke a key. Subsequent requests with it return 401.

Response · 204
# 204 No Content

Suppressions

Manage recipients that should never receive messages on a channel.

Suppress a recipient

POST/v1/suppressionssend scope

Block all future sends to a recipient on the given channel.

Body parameters
channelstringRequired
Channel to suppress on.
recipientstringRequired
Email address or phone number.
reasonstringOptional
Optional reason, e.g. hard_bounce or complaint.
Example request
curl https://api.sendara.dev/v1/suppressions \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "email", "recipient": "bounce@acme.com", "reason": "hard_bounce" }'
Response · 201
{
  "channel": "email",
  "recipient": "bounce@acme.com",
  "state": "suppressed",
  "reason": "hard_bounce",
  "updated_at": "2026-06-14T10:00:00Z"
}

Remove a suppression

DELETE/v1/suppressionssend scope

Un-suppress a recipient so they can receive messages again.

Query parameters
channelstringRequired
Channel the suppression is on.
recipientstringRequired
The suppressed recipient to remove.
Response · 204
# 204 No Content

Billing

Read the plan and open Polar checkout or the customer portal.

Get billing state

GET/v1/billingread scope

Return the account's current plan and subscription status.

Response · 200
{ "plan": "pro", "subscription_status": "active" }

Start a checkout

POST/v1/billing/checkoutadmin scope

Return a hosted Polar checkout URL to subscribe to Pro.

Response · 200
{ "url": "https://checkout.polar.sh/..." }

Open the customer portal

POST/v1/billing/portaladmin scope

Return a hosted Polar customer-portal URL to manage the subscription, payment method, and invoices.

Response · 200
{ "url": "https://polar.sh/acme/portal/..." }

Templates

Store reusable email content with mustache {{variables}}. Render a template inline on a send (template_id + template_vars) or preview it with the render endpoint.

Create a template

POST/v1/templatessend scope

Store an email template. Subject and body support {{ name }} variables; each variable can declare a sample, default, and whether it is required.

Body parameters
namestringRequired
Human-readable template name.
channelstringRequired
email (the supported channel today).
subjectstringOptional
Subject line; supports {{variables}}.
body_htmlstringOptional
HTML body; supports {{variables}}.
body_textstringOptional
Optional plain-text alternative.
body_jsonobjectOptional
Optional design JSON from the visual builder.
variablesarrayOptional
Variable metadata [{ name, sample, default, required }].
Example request
curl https://api.sendara.dev/v1/templates \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Welcome",
    "channel": "email",
    "subject": "Welcome, {{ first_name }}",
    "body_html": "<h1>Hi {{ first_name }}</h1>",
    "variables": [
      { "name": "first_name", "sample": "Ada", "required": true }
    ]
  }'
Response · 201
{
  "id": "tmpl_a1b2c3",
  "name": "Welcome",
  "channel": "email",
  "subject": "Welcome, {{ first_name }}",
  "body_html": "<h1>Hi {{ first_name }}</h1>",
  "body_text": null,
  "variables": [
    { "name": "first_name", "sample": "Ada", "required": true }
  ],
  "version": 1,
  "is_active": true,
  "created_at": "2026-06-14T10:00:00Z",
  "updated_at": "2026-06-14T10:00:00Z"
}

List templates

GET/v1/templatesread scope

Return the account's templates, newest first.

Response · 200
{
  "templates": [
    { "id": "tmpl_a1b2c3", "name": "Welcome", "channel": "email",
      "version": 1, "is_active": true, "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Get a template

GET/v1/templates/{id}read scope

Retrieve a single template with its full body and variable metadata.

Response · 200
{
  "id": "tmpl_a1b2c3",
  "name": "Welcome",
  "channel": "email",
  "subject": "Welcome, {{ first_name }}",
  "body_html": "<h1>Hi {{ first_name }}</h1>",
  "variables": [{ "name": "first_name", "sample": "Ada", "required": true }],
  "version": 1,
  "is_active": true
}

Update a template

PUT/v1/templates/{id}send scope

Update content or metadata. Editing the body or variables creates a new version.

Body parameters
namestringOptional
New name.
subjectstringOptional
New subject.
body_htmlstringOptional
New HTML body.
body_textstringOptional
New plain-text body.
body_jsonobjectOptional
New design JSON.
variablesarrayOptional
Replacement variable metadata.
is_activebooleanOptional
Toggle whether the template can be used in sends.
Response · 200
{ "id": "tmpl_a1b2c3", "name": "Welcome back", "version": 2 }

Render a preview

POST/v1/templates/{id}/renderread scope

Render the template with a set of variables and return the resulting subject and bodies — without sending. A missing required variable returns missing_variable (400).

Body parameters
varsobjectRequired
Variable values to interpolate, e.g. { "first_name": "Ada" }.
Example request
curl https://api.sendara.dev/v1/templates/tmpl_a1b2c3/render \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "vars": { "first_name": "Ada" } }'
Response · 200
{
  "subject": "Welcome, Ada",
  "body_html": "<h1>Hi Ada</h1>",
  "body_text": null
}

Delete a template

DELETE/v1/templates/{id}send scope

Permanently delete a template.

Response · 204
# 204 No Content

Contacts & lists

Store contacts and organize them into static or dynamic lists. A list (audience) is the target of a broadcast via audience_list_id.

Create a contact

POST/v1/contactssend scope

Add a contact. An email address that already exists returns duplicate_contact (409).

Body parameters
emailstringOptional
Contact email address.
phone_numberstringOptional
E.164 phone number.
first_namestringOptional
Given name.
last_namestringOptional
Family name.
attributesobjectOptional
Arbitrary key/value data usable in {{variables}}.
tagsarrayOptional
String tags for segmentation.
email_consentstringOptional
subscribed, unsubscribed, or unknown.
Example request
curl https://api.sendara.dev/v1/contacts \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ada@example.com",
    "first_name": "Ada",
    "tags": ["beta"],
    "attributes": { "plan": "pro" }
  }'
Response · 201
{
  "id": "ct_a1b2c3",
  "email": "ada@example.com",
  "phone_number": null,
  "first_name": "Ada",
  "last_name": "",
  "attributes": { "plan": "pro" },
  "tags": ["beta"],
  "email_consent": "subscribed",
  "created_at": "2026-06-14T10:00:00Z",
  "updated_at": "2026-06-14T10:00:00Z"
}

List contacts

GET/v1/contactsread scope

Return contacts, newest first.

Response · 200
{
  "contacts": [
    { "id": "ct_a1b2c3", "email": "ada@example.com",
      "first_name": "Ada", "tags": ["beta"],
      "email_consent": "subscribed", "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Get a contact

GET/v1/contacts/{id}read scope

Retrieve a single contact.

Response · 200
{ "id": "ct_a1b2c3", "email": "ada@example.com", "first_name": "Ada" }

Update a contact

PUT/v1/contacts/{id}send scope

Update any contact field. Only the fields present in the body are changed.

Body parameters
emailstringOptional
New email address.
first_namestringOptional
New given name.
attributesobjectOptional
Replacement attributes.
tagsarrayOptional
Replacement tags.
email_consentstringOptional
subscribed, unsubscribed, or unknown.
Response · 200
{ "id": "ct_a1b2c3", "email": "ada@example.com", "first_name": "Augusta" }

Delete a contact

DELETE/v1/contacts/{id}send scope

Permanently delete a contact and remove it from all lists.

Response · 204
# 204 No Content

Import contacts

POST/v1/contacts/importsend scope

Bulk-create or update contacts in one call. Existing emails are upserted.

Body parameters
contactsarrayRequired
Array of contact objects to import.
Response · 200
{ "imported": 240, "updated": 12, "skipped": 0 }

Create a list

POST/v1/contacts/listssend scope

Create an audience. A static list holds contacts you add explicitly; a dynamic list auto-includes contacts matching segment_rules (tags / attributes).

Body parameters
namestringRequired
List name.
list_typestringRequired
static or dynamic.
segment_rulesobjectOptional
For dynamic lists: { tags, attributes } a contact must match.
Example request
curl https://api.sendara.dev/v1/contacts/lists \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Beta users", "list_type": "dynamic",
        "segment_rules": { "tags": ["beta"] } }'
Response · 201
{
  "id": "list_9f21",
  "name": "Beta users",
  "list_type": "dynamic",
  "segment_rules": { "tags": ["beta"] },
  "created_at": "2026-06-14T10:00:00Z",
  "updated_at": "2026-06-14T10:00:00Z"
}

List lists

GET/v1/contacts/listsread scope

Return the account's contact lists.

Response · 200
{
  "lists": [
    { "id": "list_9f21", "name": "Beta users", "list_type": "dynamic",
      "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Add a list member

POST/v1/contacts/lists/{id}/memberssend scope

Add a contact to a static list. Adding a contact already on the list returns duplicate_member (409).

Body parameters
contact_idstringRequired
The contact to add.
Response · 201
{
  "id": "lm_1",
  "contact_list_id": "list_9f21",
  "contact_id": "ct_a1b2c3",
  "added_at": "2026-06-14T10:00:00Z"
}

List list members

GET/v1/contacts/lists/{id}/membersread scope

Return the contacts on a list.

Response · 200
{ "members": [ { "id": "ct_a1b2c3", "email": "ada@example.com" } ] }

Remove a list member

DELETE/v1/contacts/lists/{id}/members/{contactId}send scope

Remove a contact from a static list.

Response · 204
# 204 No Content

Webhooks

Subscribe an HTTPS endpoint to delivery events (delivered, bounced, complained, opened, …). Each subscription has a signing secret used to verify the Sendara-Signature header.

Create a webhook

POST/v1/webhooksadmin scope

Register an endpoint and the event types to deliver. The signing secret is returned once — store it to verify signatures.

Body parameters
endpoint_urlstringRequired
HTTPS URL that receives POSTed events.
event_typesarrayOptional
Event types to deliver, e.g. ["delivered", "bounced"]. Omit for all.
Example request
curl https://api.sendara.dev/v1/webhooks \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "endpoint_url": "https://acme.com/hooks/sendara",
        "event_types": ["delivered", "bounced", "complained"] }'
Response · 201
{
  "id": "wh_a1b2c3",
  "endpoint_url": "https://acme.com/hooks/sendara",
  "event_types": ["delivered", "bounced", "complained"],
  "signing_secret": "whsec_5f3a39ab8c2d4e6f9a0b1c2d3e4f5a6b",
  "is_active": true,
  "created_at": "2026-06-14T10:00:00Z"
}

List webhooks

GET/v1/webhooksread scope

Return the account's webhook subscriptions. The signing secret is not included.

Response · 200
{
  "webhooks": [
    { "id": "wh_a1b2c3", "endpoint_url": "https://acme.com/hooks/sendara",
      "event_types": ["delivered", "bounced"], "is_active": true,
      "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Get a webhook

GET/v1/webhooks/{id}read scope

Retrieve a single webhook subscription.

Response · 200
{ "id": "wh_a1b2c3", "endpoint_url": "https://acme.com/hooks/sendara",
  "event_types": ["delivered", "bounced"], "is_active": true }

Update a webhook

PUT/v1/webhooks/{id}admin scope

Change the endpoint, event types, or active state.

Body parameters
endpoint_urlstringOptional
New endpoint URL.
event_typesarrayOptional
New event-type filter.
is_activebooleanOptional
Pause or resume deliveries.
Response · 200
{ "id": "wh_a1b2c3", "is_active": false }

Rotate the signing secret

POST/v1/webhooks/{id}/rotate-secretadmin scope

Issue a new signing secret and invalidate the old one. The new secret is returned once.

Response · 200
{ "signing_secret": "whsec_9b2c0a17e4d5f6a8b1c2d3e4f5a6b7c8" }

List deliveries

GET/v1/webhooks/{id}/deliveriesread scope

Inspect recent delivery attempts for a subscription, including status and response code.

Response · 200
{
  "deliveries": [
    { "id": "whd_1", "event_id": "evt_9c1f", "event_type": "delivered",
      "status": "succeeded", "response_code": 200, "attempt_count": 1,
      "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Delete a webhook

DELETE/v1/webhooks/{id}admin scope

Permanently delete a subscription. No further events are delivered.

Response · 204
# 204 No Content

Test recipients

Register up to 3 of your own addresses, verify them by email, then send REAL emails to them for free (capped 10/address/day) with test_send: true — ideal for prod/UAT validation without billing.

Register a test recipient

POST/v1/test-recipientssend scope

Register an address and trigger a verification email. Registering a 4th address returns too_many_test_recipients (422).

Body parameters
emailstringRequired
An address you control. A verification link is emailed to it.
Example request
curl https://api.sendara.dev/v1/test-recipients \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "email": "qa@yourteam.com" }'
Response · 201
{
  "id": "tr_a1b2c3",
  "email": "qa@yourteam.com",
  "status": "pending",
  "verified_at": null,
  "created_at": "2026-06-14T10:00:00Z"
}

List test recipients

GET/v1/test-recipientsread scope

Return your registered test recipients and their verification status.

Response · 200
{
  "recipients": [
    { "id": "tr_a1b2c3", "email": "qa@yourteam.com", "status": "verified",
      "verified_at": "2026-06-14T10:05:00Z", "created_at": "2026-06-14T10:00:00Z" }
  ]
}

Resend verification

POST/v1/test-recipients/{id}/resendsend scope

Re-send the verification email for a pending test recipient.

Response · 204
# 204 No Content

Delete a test recipient

DELETE/v1/test-recipients/{id}send scope

Remove a registered test recipient, freeing one of your 3 slots.

Response · 204
# 204 No Content

Uploads

Host images for use in templates and emails. Upload a PNG, JPEG, GIF, or WebP (max 2 MiB) and receive a public URL.

Upload an image

POST/v1/uploadssend scope

Send a multipart/form-data request with a file part. Returns a public URL to embed in HTML. Files over 2 MiB return payload_too_large (413).

Example request
curl https://api.sendara.dev/v1/uploads \
  -H "Authorization: Bearer sk_live_xxx" \
  -F "file=@hero.png"
Response · 201
{
  "id": "asset_a1b2c3",
  "url": "https://assets.sendara.dev/v1/assets/asset_a1b2c3",
  "content_type": "image/png",
  "bytes": 48213
}

Errors

Every error returns a consistent envelope: an HTTP status, a stable code you can branch on, and a human-readable message.

{
  "error": {
    "code": "from_not_verified",
    "message": "The from address is not on a verified domain",
    "status": 422
  }
}
Error codes
CodeStatusDescription
unauthorized401Missing or invalid API key. Check the Authorization: Bearer header. Recovery: supply a valid sk_live_/sk_test_ key.
forbidden403The key's scope does not permit this operation. Recovery: use a key with the send, read, or admin scope the endpoint requires.
invalid_request400The request body or parameters are malformed or missing a required field. Recovery: fix the field named in the message and retry.
not_found404The resource does not exist or belongs to another account. Recovery: check the id.
recipient_suppressed409The recipient is on the suppression list for this channel. Fires on send. Recovery: remove the suppression or send to a different address.
idempotency_key_reused409The idempotency_key was reused with a different request body. Recovery: use a new key, or replay the exact original body to get the cached result.
from_not_verified422The from address is not on a verified domain. Fires on send (also 403 in some paths). Recovery: verify the sending domain first.
missing_variable400A template render is missing a required {{variable}}. Recovery: supply every required variable in template_vars / vars.
invalid_template400The template syntax is invalid or the referenced template is inactive. Recovery: fix the template body or activate it.
invalid_token400A verification or action token is malformed or expired. Recovery: request a fresh link.
invalid_signature401Webhook signature verification failed (Sendara-Signature mismatch). Recovery: recompute HMAC-SHA256 over "<timestamp>.<rawBody>" with the subscription's signing secret.
rate_limit_exceeded429Too many requests in the window. Response carries Retry-After and X-RateLimit-* headers. Recovery: back off until X-RateLimit-Reset, then retry.
spend_cap_exceeded402The account's spend cap for the period has been reached. Recovery: raise the cap in billing or wait for the next period.
billing_not_configured400A billing-gated action was attempted before billing is set up. Recovery: complete checkout to configure billing.
duplicate_contact409A contact with this email already exists. Recovery: update the existing contact instead of creating a new one.
duplicate_member409The contact is already a member of this list. Recovery: treat as already-added; no action needed.
too_many_test_recipients422You already have the maximum of 3 registered test recipients. Recovery: delete one before adding another.
recipient_not_verified403A test_send target is not a verified test recipient for this account. Recovery: register and verify the address first.
test_send_daily_limit429The 10-per-address-per-day test-send cap was reached. Recovery: wait until the next UTC day or use a different verified recipient.
payload_too_large413The request body or upload exceeds the size limit (uploads cap at 2 MiB). Recovery: shrink the payload.
internal_error500An unexpected server error. Recovery: retry with backoff; if it persists, contact support.