Reference

API reference

Endpoints, parameters, and responses. Base URL https://api.sendara.dev.

Send every request over HTTPS to https://api.sendara.dev. Authenticate it with a Bearer key. Sendara returns every timestamp in RFC 3339 form, and every monetary amount in micro-dollars (1,000,000 = $1.00). This page shows a path parameter in {braces}.

Send

Send one email, or send many emails in one call.

Send a message

POST/v1/sendsend scope

Send one email. Email is the only send channel that Sendara runs today.

Body parameters
channelstringRequired
The send channel. Set it to email.
idempotency_keystringRequired
A unique key for this request. An empty key returns 400. The Sendara SDKs generate a key when you omit it. A key that you reuse with a different body returns 409 idempotency_key_reused.
destinationobjectRequired
The recipient: { email }.
payloadobjectOptional
The email content: { subject, body_html, body_text }. Required unless you provide template_id.
message_typestringOptional
transactional. This is the default.
metadataobjectOptional
The per-send options. { from_email } sets the sender. Your own domain requires it, and an omitted from_email returns 422 from_required. The address must sit on a verified domain. Give a bare address, or put the display name first: Acme <[email protected]>.
template_idstringOptional
Render a stored template instead of an inline payload.
template_varsobjectOptional
The values that Sendara puts into the template.
scheduled_atstringOptional
RFC 3339 dispatch time, up to 90 days ahead. Times more than one minute ahead return scheduled; nearer times enter the queue immediately.
validate_recipientbooleanOptional
Check recipient syntax and domain mail records before accepting the send. Defaults to false.
store_payloadbooleanOptional
Retain rendered content after dispatch (the default), or set false to redact the stored payload after safe dispatch.
test_sendbooleanOptional
Send real email free to a verified test recipient, subject to the 10-per-recipient daily cap. Defaults to false.
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": "[email protected]" },
    "payload": {
      "subject": "Welcome to Acme",
      "body_html": "<h1>Welcome 🎉</h1>"
    },
    "metadata": { "from_email": "[email protected]" }
  }'
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. Sendara processes each item on its own. The response keeps the order of the request, and each item carries a success flag or an error. A 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": "[email protected]" },
      "payload": { "subject": "Hi", "body_html": "<p>Hi</p>" } },
    { "channel": "email", "idempotency_key": "b2",
      "destination": { "email": "[email protected]" },
      "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 } }
]

Validate an email address

POST/v1/validatesend scope

Check one address before you send to it. Sendara checks the syntax and the MX records of the domain, and sends no mail. The endpoint returns validation_not_configured (503) when the check is not available.

Body parameters
emailstringRequired
The address to check.
Example request
curl https://api.sendara.dev/v1/validate \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]" }'
Response · 200
{ "email": "[email protected]", "valid": true, "reason": "" }

Messages

Read sent messages and their event timeline.

List messages

GET/v1/messagesread scope

Return a list of message summaries. Filter the list by the channel, the status and the time range. Pass an idempotency_key to get the one message that you sent with that key. Sendara then returns that message with its full event timeline, and not a list.

Query parameters
idempotency_keystringOptional
Return the one message that you sent with this key, plus its full event timeline. The key is unique for the account. An unused key returns 404. This parameter overrides every other filter.
searchstringOptional
Search the text of the recipient address, the From address, the subject and the body. A local part such as alice matches [email protected]. This parameter works with the other filters.
channelstringOptional
Filter the list by the channel.
statusstringOptional
Filter the list by the status, for example delivered, bounced, delivery_delayed or suppressed.
fromstringOptional
The lower bound of the time range, in RFC 3339 format. The bound is inclusive.
tostringOptional
The upper bound of the time range, in RFC 3339 format. The bound is inclusive.
limitintegerOptional
The page size, from 1 to 100. The default is 50.
cursorstringOptional
The opaque keyset cursor. Take it from the next_cursor field of the previous response.
Response · 200
{
  "messages": [
    { "id": "msg_a1b2c3", "channel": "email", "status": "delivered",
      "message_type": "transactional", "created_at": "2026-06-14T10:00:00Z" }
  ],
  "next_cursor": null
}

Get a message

GET/v1/messages/{id}read scope

Return one message and 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": "evt_1", "type": "sent", "occurred_at": "2026-06-14T10:00:00Z" },
    { "id": "evt_2", "type": "delivered", "occurred_at": "2026-06-14T10:00:12Z" }
  ]
}

Cancel a scheduled message

POST/v1/messages/{id}/cancelsend scope

Cancel a message that you scheduled for a future send and that Sendara did not yet dispatch. The endpoint returns not_found (404) when no pending scheduled message carries that id, because Sendara already sent it, you already canceled it, or the id is unknown.

Response · 200
{ "id": "msg_a1b2c3", "status": "canceled" }

Inbound

Read the email that you receive, and forward it. These deployment-gated routes exist only when inbound email is enabled; otherwise they return 404. You also enable inbound mail on each verified domain. A route forwards matching mail to a signed webhook or to another inbox. The email.received webhooks of the account still fire.

List inbound emails

GET/v1/inboundread scope

Return the emails that you received. The newest email comes first. The list omits the text body and the html body, so the payload stays small. Get a single email to read the full content.

Query parameters
limitintegerOptional
The largest number of emails to return.
Example request
curl "https://api.sendara.dev/v1/inbound?limit=30" \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "emails": [
    {
      "id": "inb_9f21",
      "account_id": "acc_abc123",
      "domain": "inbound.acme.com",
      "recipient": "[email protected]",
      "inbox_id": "inbox_a1b2c3",
      "from": "Alice <[email protected]>",
      "to": ["[email protected]"],
      "cc": [],
      "subject": "Need help with my order",
      "spf_verdict": "PASS",
      "dkim_verdict": "PASS",
      "dmarc_verdict": "PASS",
      "attachments": [
        { "filename": "receipt.pdf", "content_type": "application/pdf", "size": 84211 }
      ],
      "size_bytes": 91234,
      "received_at": "2026-07-17T10:02:00Z",
      "thread_id": "thr_5c10"
    }
  ]
}

Get an inbound email

GET/v1/inbound/{id}read scope

Return one received email and its full content: the text body, the html body, the headers, the SPF, DKIM and DMARC verdicts, the data about each attachment, and the fields that join the email to its thread.

Response · 200
{
  "id": "inb_9f21",
  "account_id": "acc_abc123",
  "domain": "inbound.acme.com",
  "recipient": "[email protected]",
  "inbox_id": "inbox_a1b2c3",
  "from": "Alice <[email protected]>",
  "to": ["[email protected]"],
  "cc": [],
  "subject": "Need help with my order",
  "text": "Hi, my order has not arrived yet.",
  "html": "<p>Hi, my order has not arrived yet.</p>",
  "spf_verdict": "PASS",
  "dkim_verdict": "PASS",
  "dmarc_verdict": "PASS",
  "attachments": [
    { "filename": "receipt.pdf", "content_type": "application/pdf", "size": 84211 }
  ],
  "size_bytes": 91234,
  "received_at": "2026-07-17T10:02:00Z",
  "rfc_message_id": "<[email protected]>",
  "thread_id": "thr_5c10"
}

Download the raw message

GET/v1/inbound/{id}/rawread scope

Return the original RFC 822 message as message/rfc822. The endpoint returns not_found (404) when Sendara did not keep the raw message.

Example request
curl https://api.sendara.dev/v1/inbound/inb_9f21/raw \
  -H "Authorization: Bearer sk_live_xxx" -o message.eml
Response · 200
# Content-Type: message/rfc822 (the original .eml bytes)

Download an attachment

GET/v1/inbound/{id}/attachments/{index}read scope

Return one attachment by its index in the attachments array of the email. The index starts at 0. The response carries the Content-Type of the attachment and a Content-Disposition filename. An index outside the array returns not_found (404).

Example request
curl https://api.sendara.dev/v1/inbound/inb_9f21/attachments/0 \
  -H "Authorization: Bearer sk_live_xxx" -o receipt.pdf
Response · 200
# Content-Type: application/pdf (the attachment bytes)

List inbound routes

GET/v1/inbound/routesread scope

Return the forwarding routes of the account. Sendara omits the signing secret.

Response · 200
{
  "routes": [
    { "id": "route_a1b2c3", "domain": "inbound.acme.com", "match_prefix": "support",
      "destination_type": "webhook", "forward_url": "https://acme.com/hooks/inbound",
      "active": true, "created_at": "2026-07-17T10:00:00Z" }
  ]
}

Create an inbound route

POST/v1/inbound/routesadmin scope

Forward the mail that arrives at a local part on a verified domain with inbound mail enabled. A webhook route posts the parsed email to an HTTPS forward_url, and signs the request with the signing_secret. An email route sends the message again to forward_address through Amazon SES. Sendara returns the signing_secret once, when you create the route.

Body parameters
domainstringRequired
The domain to match. It must be verified, outside the sandbox, and enabled for inbound mail.
destination_typestringOptional
webhook (the default) or email.
match_prefixstringOptional
The local part to match before the @ sign, for example support. An empty value or * matches every address on the domain.
forward_urlstringOptional
The HTTPS URL that receives the signed POST. A webhook destination_type requires it.
forward_addressstringOptional
The inbox that receives the forwarded message. An email destination_type requires it.
Example request
curl https://api.sendara.dev/v1/inbound/routes \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "inbound.acme.com",
    "match_prefix": "support",
    "destination_type": "webhook",
    "forward_url": "https://acme.com/hooks/inbound"
  }'
Response · 201
{
  "id": "route_a1b2c3",
  "domain": "inbound.acme.com",
  "match_prefix": "support",
  "destination_type": "webhook",
  "forward_url": "https://acme.com/hooks/inbound",
  "active": true,
  "created_at": "2026-07-17T10:00:00Z",
  "signing_secret": "whsec_5f3a39ab8c2d4e6f9a0b1c2d"
}

Update an inbound route

PUT/v1/inbound/routes/{id}admin scope

Change the match prefix, the destination or the active state of a route. Sendara changes only the fields that the body contains.

Body parameters
match_prefixstringOptional
The new local part to match.
forward_urlstringOptional
The new HTTPS webhook URL.
forward_addressstringOptional
The new inbox that receives the forwarded message.
activebooleanOptional
Pause the route, or start it again.
Example request
curl -X PUT https://api.sendara.dev/v1/inbound/routes/route_a1b2c3 \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "active": false }'
Response · 200
{
  "id": "route_a1b2c3",
  "domain": "inbound.acme.com",
  "match_prefix": "support",
  "destination_type": "webhook",
  "forward_url": "https://acme.com/hooks/inbound",
  "active": false,
  "created_at": "2026-07-17T10:00:00Z"
}

Delete an inbound route

DELETE/v1/inbound/routes/{id}admin scope

Delete a forwarding route permanently. Sendara then forwards no more matching mail.

Response · 204
# 204 No Content

Enable inbound receiving

POST/v1/domains/{id}/inboundadmin scope

Turn inbound mail on or off for a domain. The domain must be verified and outside the sandbox. Any other domain returns domain_not_ready (422). On success the endpoint returns the MX record to publish, so that your mail reaches Sendara.

Body parameters
enabledbooleanRequired
Set this to true to receive inbound mail on the domain.
Example request
curl https://api.sendara.dev/v1/domains/dom_1/inbound \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
Response · 200
{
  "enabled": true,
  "mx_record": { "type": "MX", "priority": "10", "value": "inbound-smtp.us-east-1.amazonaws.com" }
}

Mail app

Read and write the threaded inbox of Sendara Mail. These deployment-gated routes exist only when Sendara Mail is enabled; otherwise they return 404. They use the same dashboard session or API key as the rest of the API.

List mail inboxes

GET/v1/mail/inboxesread scope

Return the role inboxes that you configured on your verified inbound domains, for example [email protected] and [email protected].

Example request
curl https://api.sendara.dev/v1/mail/inboxes \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "inboxes": [
    {
      "id": "inbox_a1b2c3",
      "domain_id": "dom_abc123",
      "domain": "mail.acme.com",
      "local_part": "support",
      "address": "[email protected]",
      "active": true,
      "created_at": "2026-06-17T10:00:00Z",
      "updated_at": "2026-06-17T10:00:00Z"
    }
  ]
}

Create a mail inbox

POST/v1/mail/inboxesadmin scope

Create a separate receiving address on a domain that is verified, outside the sandbox, and enabled for inbound mail. The catch-all address on the domain stays active. Mail that matches this address exactly then carries an inbox_id that you can filter on.

Body parameters
addressstringOptional
The full address to create. Use this field instead of domain and local_part.
domainstringOptional
The verified inbound domain. An omitted address requires it.
local_partstringOptional
The mailbox name before the @ sign. An omitted address requires it.
namestringOptional
The display label for the inbox. This field is optional.
Example request
curl https://api.sendara.dev/v1/mail/inboxes \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "mail.acme.com", "local_part": "support" }'
Response · 201
{
  "id": "inbox_a1b2c3",
  "domain_id": "dom_abc123",
  "domain": "mail.acme.com",
  "local_part": "support",
  "address": "[email protected]",
  "active": true,
  "created_at": "2026-06-17T10:00:00Z",
  "updated_at": "2026-06-17T10:00:00Z"
}

Delete a mail inbox

DELETE/v1/mail/inboxes/{id}admin scope

Remove an inbox address. Each existing message keeps its recipient address and stays in its thread. Sendara detaches the message from the deleted inbox id.

List mail threads

GET/v1/mail/threadsread scope

Return a summary of each thread across the mail that you received and the mail that you sent. The thread with the newest message comes first. Pass inbox_id or inbox to show one role inbox. The q parameter takes plain text and these operators: from:, to:, subject:, has:attachment, after:YYYY-MM-DD and before:YYYY-MM-DD. A call to GET /v1/mail/threads/{id} marks the thread read.

Query parameters
folderstringOptional
Filter the threads by inbox, sent, archive, trash or spam.
labelstringOptional
Return only the threads that carry this label.
qstringOptional
The search text and the operators, for example invoice from:[email protected] has:attachment after:2026-06-01.
inbox_idstringOptional
Return only the threads in this mail inbox.
inboxstringOptional
Return only the threads for this exact recipient address, for example [email protected].
unreadbooleanOptional
Set this to true to return only the unread threads.
starredbooleanOptional
Set this to true to return the threads that hold at least one starred message.
limitintegerOptional
The page size, from 1 to 100. The default is 50.
cursorstringOptional
The opaque keyset cursor. Take it from the next_cursor field.
Example request
curl "https://api.sendara.dev/v1/mail/threads?folder=inbox&limit=30" \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "threads": [
    {
      "id": "thr_5c10",
      "subject": "Need help with my order",
      "snippet": "Hi, my order has not arrived",
      "participants": ["[email protected]", "[email protected]"],
      "last_message_at": "2026-06-17T10:02:00Z",
      "message_count": 2,
      "has_unread": true,
      "is_starred": false,
      "labels": ["Support"]
    }
  ],
  "next_cursor": "opaque"
}

Search mail

GET/v1/mail/searchread scope

Search the text of the mail that you received and the mail that you sent. A query of operators alone works, so q=from:[email protected] and q=has:attachment need no other text. Add inbox_id or inbox to q to search one role inbox.

Query parameters
qstringRequired
The text to search for.
inbox_idstringOptional
Search only this mail inbox.
inboxstringOptional
Search only this exact recipient address.
limitintegerOptional
The page size, from 1 to 100. The default is 50.
cursorstringOptional
The offset cursor. Take it from the next_cursor field.
Example request
curl "https://api.sendara.dev/v1/mail/search?q=invoice&inbox=support%40mail.acme.com" \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "results": [
    {
      "thread_id": "thr_5c10",
      "source": "inbound",
      "message_ref": "inb_9f21",
      "from": "Alice <[email protected]>",
      "subject": "Invoice question",
      "snippet": "Can you resend the invoice?",
      "occurred_at": "2026-06-17T10:02:00Z"
    }
  ]
}

Export mail

GET/v1/mail/exportread scope

Export the mail that you selected, as JSON or as mbox. The endpoint takes the same filters as /v1/mail/threads, and that includes the role inbox filters and the search operators. The JSON output names each attachment. The bytes of an attachment stay behind a download URL that requires your API key.

Query parameters
formatstringOptional
json (the default) or mbox.
folderstringOptional
Filter the threads by inbox, sent, archive, trash or spam.
labelstringOptional
Export only the threads that carry this label.
qstringOptional
The search text and these operators: from:, to:, subject:, has:attachment, after:, before:.
inbox_idstringOptional
Export only the threads in this mail inbox.
inboxstringOptional
Export only the threads for this exact recipient address.
unreadbooleanOptional
Set this to true to export only the unread threads.
starredbooleanOptional
Set this to true to export the starred threads.
limitintegerOptional
The largest number of messages to export. The default is 5000, and 5000 is also the maximum.
Example request
curl "https://api.sendara.dev/v1/mail/export?format=json&inbox=support%40mail.acme.com&q=has%3Aattachment" \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "exported_at": "2026-06-17T10:12:00Z",
  "count": 1,
  "messages": [
    {
      "id": "inb_9f21",
      "thread_id": "thr_5c10",
      "source": "inbound",
      "from": "Alice <[email protected]>",
      "to": ["[email protected]"],
      "subject": "Invoice question",
      "text_body": "Can you resend the invoice?",
      "occurred_at": "2026-06-17T10:02:00Z",
      "attachments": [
        { "filename": "invoice.pdf", "content_type": "application/pdf", "size": 84211 }
      ]
    }
  ]
}

Get a mail thread

GET/v1/mail/threads/{id}read scope

Return the full view of a thread. It carries the SPF, DKIM and DMARC verdicts of each received message, the attachments, the delivery status of each sent message, and the event timeline of each sent message. The call marks the thread read.

Response · 200
{
  "id": "thr_5c10",
  "subject": "Need help with my order",
  "participants": ["[email protected]", "[email protected]"],
  "message_count": 2,
  "is_starred": false,
  "labels": ["Support"],
  "messages": [
    {
      "id": "inb_9f21",
      "source": "inbound",
      "from": "Alice <[email protected]>",
      "to": ["[email protected]"],
      "subject": "Need help with my order",
      "text_body": "Hi, my order has not arrived",
      "spf_verdict": "PASS",
      "dkim_verdict": "PASS",
      "dmarc_verdict": "PASS",
      "occurred_at": "2026-06-17T10:02:00Z"
    }
  ]
}

Set a thread's read state

POST/v1/mail/threads/{id}/readread scope

Mark every message in a thread as read or unread. An omitted body or read field defaults to true.

Body parameters
readbooleanOptional
true to mark the thread read; false to mark it unread. Defaults to true.
Response · 200
{ "id": "thr_5c10", "read": true }

Set a thread's starred state

POST/v1/mail/threads/{id}/starread scope

Star or unstar every message in a thread. An omitted body or starred field defaults to true.

Body parameters
starredbooleanOptional
true to star the thread; false to unstar it. Defaults to true.
Response · 200
{ "id": "thr_5c10", "starred": true }

Set a message's starred state

POST/v1/mail/messages/{source}/{id}/starread scope

Star or unstar one message. Set source in the path to inbound or outbound. An omitted body or starred field defaults to true.

Body parameters
starredbooleanOptional
true to star the message; false to unstar it. Defaults to true.
Response · 200
{ "source": "inbound", "id": "inb_9f21", "starred": true }

List mail labels

GET/v1/mail/labelsread scope

Return the user-defined labels of the account, ordered by name.

Response · 200
{ "labels": [
  { "id": "lbl_a1b2c3", "name": "Important", "color": "#f59e0b",
    "created_at": "2026-06-17T10:00:00Z" }
] }

Create a mail label

POST/v1/mail/labelsread scope

Create a user-defined label. Label names are unique without regard to case.

Body parameters
namestringRequired
The label name.
colorstringOptional
An optional UI color value.
Response · 201
{ "id": "lbl_a1b2c3", "name": "Important", "color": "#f59e0b",
  "created_at": "2026-06-17T10:00:00Z" }

Delete a mail label

DELETE/v1/mail/labels/{id}read scope

Delete a user-defined label and remove it from every message that carries it.

Response · 204
# 204 No Content

Compose from Mail

POST/v1/mail/composesend scope

Send a new email from the Mail app. Mail sends only from a domain of your account that is verified and outside the sandbox. When you omit from, Sendara picks a verified domain for the account.

Body parameters
tostringRequired
The email address of the recipient.
fromstringOptional
The sender address on a verified domain. This field is optional. Give a bare address, or put the display name first.
subjectstringOptional
The subject line of the message.
body_htmlstringOptional
The HTML body. The request requires body_html or body_text.
body_textstringOptional
The plain-text body. Sendara turns it into simple HTML when you omit body_html.
Example request
curl https://api.sendara.dev/v1/mail/compose \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "[email protected]",
    "from": "Support <[email protected]>",
    "subject": "Following up",
    "body_text": "Thanks for reaching out."
  }'
Response · 201
{
  "id": "msg_a1b2c3",
  "status": "queued",
  "channel": "email",
  "idempotency_key": "mail_...",
  "created_at": "2026-06-17T10:05:00Z"
}

Reply to a thread

POST/v1/mail/threads/{id}/replysend scope

Send a reply. Sendara joins the reply to the newest received message in the thread through the In-Reply-To header, the References header and thread_id. When you omit to, Sendara replies to the first sender.

Body parameters
tostringOptional
The recipient that replaces the default. The default is the sender of the newest received message.
fromstringOptional
The sender address on a verified domain. This field is optional.
subjectstringOptional
The subject line. This field is optional. The default is Re: <thread subject>.
body_htmlstringOptional
The HTML body. The request requires body_html or body_text.
body_textstringOptional
The plain-text body.
Response · 201
{ "id": "msg_reply1", "status": "queued", "channel": "email", "idempotency_key": "mail_..." }

Forward a thread

POST/v1/mail/threads/{id}/forwardsend scope

Send a thread to a new recipient. Sendara keeps the RFC threading headers when the thread has a received parent. The request requires to and either body_html or body_text. An omitted subject defaults to Fwd: followed by the newest received subject.

Body parameters
tostringRequired
The new recipient.
fromstringOptional
The sender address on a verified domain. This field is optional.
subjectstringOptional
The subject line. This field is optional.
body_htmlstringOptional
The HTML body. The request requires body_html or body_text.
body_textstringOptional
The plain-text body.
Response · 201
{ "id": "msg_forward1", "status": "queued", "channel": "email", "idempotency_key": "mail_..." }

Move a thread

POST/v1/mail/threads/{id}/moveread scope

Move every message in a thread into inbox, archive, trash or spam. Sendara computes the sent folder from the messages that you sent, so you cannot move a thread into it.

Body parameters
folderstringRequired
inbox, archive, trash or spam.
Response · 200
{ "id": "thr_5c10", "folder": "archive" }

Update thread labels

POST/v1/mail/threads/{id}/labelsread scope

Add labels to a thread, or remove labels from it. Sendara ignores a label name that does not exist. Create your labels first with POST /v1/mail/labels.

Body parameters
addarrayOptional
The label names to add.
removearrayOptional
The label names to remove.
Response · 200
{ "id": "thr_5c10" }

Usage

Read send counts and the internal estimated-spend ledger used for spend-cap enforcement in a billing period.

Get usage

GET/v1/usageread scope

Return transactional send counts and Sendara's internal spend-cap estimate for the period, in micro-dollars. cost_micros is an enforcement ledger, not an invoice or the plan-specific overage price.

Query parameters
periodstringOptional
The billing period, in YYYY-MM format. The default is the current period.
Response · 200
{
  "period": "2026-06",
  "total_send_count": 18420,
  "total_cost_micros": 13815000,
  "channels": [
    { "channel": "email", "send_count": 18420, "cost_micros": 13815000 }
  ]
}

Set a spend cap

PUT/v1/spend-capsadmin scope

Set the spend cap of the account, in micro-dollars. A hard cap blocks a send with spend_cap_exceeded (402) once the cost of the period reaches the cap. Set a hard cap to bound the spend of an agent. A soft cap only sends an alert. Omit key_id to cap the whole account. Pass key_id to cap one API key that you own. A negative limit returns 400.

Body parameters
hard_limit_microsintegerOptional
The hard cap, in micro-dollars. Sendara blocks a send once the cost of the period reaches the cap.
soft_limit_microsintegerOptional
The soft cap, in micro-dollars. Sendara sends an alert and blocks nothing.
key_idstringOptional
The API key to cap, instead of the whole account. A key outside your account returns 404.
Example request
curl -X PUT https://api.sendara.dev/v1/spend-caps \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "hard_limit_micros": 50000000 }'
Response · 200
{
  "id": "cap_a1b2c3",
  "account_id": "acc_abc123",
  "key_id": null,
  "soft_limit_micros": null,
  "hard_limit_micros": 50000000
}

Account

Read the configuration of the account. An agent can call this group before it sends, to learn whether the account sends from its own domain or still uses the sandbox sender.

Get verification status

GET/v1/account/verification

Return the readiness of each channel, and the sandbox_mode flag of the account. Any valid API key can call this endpoint, and it needs no particular scope. sandbox_mode is true when the account holds no fully verified sending domain outside the sandbox. Sendara then sends email from shared_email_sender only, and only to the address of the account or to a verified test recipient. Each channel entry carries a status (verified, pending, sandbox, registered, ready or not_configured) and a verified boolean.

Example request
curl https://api.sendara.dev/v1/account/verification \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "account_id": "acc_abc123",
  "sandbox_mode": true,
  "shared_email_sender": "[email protected]",
  "channels": [
    { "channel": "email", "status": "sandbox", "verified": false },
    { "channel": "sms", "status": "not_configured", "verified": false },
    { "channel": "push", "status": "not_configured", "verified": false },
    { "channel": "voice", "status": "not_configured", "verified": false },
    { "channel": "webhook", "status": "ready", "verified": true }
  ]
}

Domains

Add and verify a sending domain. An account holds up to 100 domain records, and every verification status counts toward that limit. The write endpoints need the admin scope, and the read endpoints need the read scope.

Add a sending domain

POST/v1/domainsadmin scope

Register a domain. Sendara returns the 6 DNS records that you publish: 3 DKIM CNAMEs, an MX record and an SPF record on the MAIL FROM subdomain, and a starter DMARC record at p=none.

Body parameters
domainstringRequired
The domain or the subdomain to send from, for example 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": "abc123._domainkey.mail.acme.com",
      "value": "abc123.dkim.amazonses.com" },
    { "type": "CNAME", "name": "def456._domainkey.mail.acme.com",
      "value": "def456.dkim.amazonses.com" },
    { "type": "CNAME", "name": "ghi789._domainkey.mail.acme.com",
      "value": "ghi789.dkim.amazonses.com" },
    { "type": "MX", "name": "mail.mail.acme.com",
      "value": "10 feedback-smtp.us-east-1.amazonses.com" },
    { "type": "TXT", "name": "mail.mail.acme.com",
      "value": "v=spf1 include:amazonses.com ~all" },
    { "type": "TXT", "name": "_dmarc.mail.acme.com",
      "value": "v=DMARC1; p=none; rua=mailto:[email protected]" }
  ],
  "mail_from_domain": "mail.mail.acme.com",
  "created_at": "2026-06-14T10:00:00Z"
}

Verify a domain

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

Check the DNS records again, and return the result for each DKIM, SPF and DMARC record. The MAIL FROM MX record carries no result, because SES reports its state separately.

Response · 200
{
  "domain": "mail.acme.com",
  "fully_verified": true,
  "results": [
    { "field": "dkim_status", "type": "CNAME", "name": "abc123._domainkey.mail.acme.com",
      "status": "verified" },
    { "field": "dkim_status", "type": "CNAME", "name": "def456._domainkey.mail.acme.com",
      "status": "verified" },
    { "field": "dkim_status", "type": "CNAME", "name": "ghi789._domainkey.mail.acme.com",
      "status": "verified" },
    { "field": "spf_status", "type": "TXT", "name": "mail.mail.acme.com",
      "status": "verified" },
    { "field": "dmarc_status", "type": "TXT", "name": "_dmarc.mail.acme.com",
      "status": "verified" }
  ]
}

Set the default sender name

PUT/v1/domains/{domain}/from-nameadmin scope

Set a default From name for a verified domain. Every send from that domain then shows the name (Acme <[email protected]>). A display name in metadata.from_email overrides the default for one send.

Body parameters
from_namestringRequired
The display name to show on mail from this domain. An empty string removes the name.
Example request
curl -X PUT https://api.sendara.dev/v1/domains/mail.acme.com/from-name \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "from_name": "Acme" }'
Response · 200
{ "domain": "mail.acme.com", "from_name": "Acme" }

Get BIMI state

GET/v1/domains/{domain}/bimiread scope

Return the BIMI state of the domain: the URL of the hosted logo, the TXT record to publish (null when you set no logo), whether your DMARC policy is strict enough, a recommended policy, whether the record is live, and a note about the providers that require a VMC.

Response · 200
{
  "logo_url": "https://assets.sendara.dev/v1/bimi/mail.acme.com/logo.svg",
  "record": {
    "type": "TXT",
    "name": "default._bimi.mail.acme.com",
    "value": "v=BIMI1; l=https://assets.sendara.dev/v1/bimi/mail.acme.com/logo.svg;"
  },
  "dmarc": {
    "at_enforcement": false,
    "recommended": "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
  },
  "bimi_published": false,
  "vmc_note": "Gmail and Apple Mail require a paid VMC tied to a registered trademark; Yahoo and Fastmail display the logo without one."
}

Set the BIMI logo URL

PUT/v1/domains/{domain}/bimiadmin scope

Point BIMI at a square SVG Tiny PS logo that you serve over HTTPS. Sendara checks the logo. The response has the same shape as the GET, and it carries the TXT record that Sendara built.

Body parameters
logo_urlstringRequired
The public HTTPS URL of a square SVG Tiny PS logo.
Example request
curl -X PUT https://api.sendara.dev/v1/domains/mail.acme.com/bimi \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "logo_url": "https://cdn.acme.com/brand/logo-tiny-ps.svg" }'
Response · 200
{
  "logo_url": "https://cdn.acme.com/brand/logo-tiny-ps.svg",
  "record": {
    "type": "TXT",
    "name": "default._bimi.mail.acme.com",
    "value": "v=BIMI1; l=https://cdn.acme.com/brand/logo-tiny-ps.svg;"
  },
  "dmarc": { "at_enforcement": true, "recommended": "v=DMARC1; p=reject; rua=mailto:[email protected]" },
  "bimi_published": false,
  "vmc_note": "Gmail and Apple Mail require a paid VMC tied to a registered trademark; Yahoo and Fastmail display the logo without one."
}
POST/v1/domains/{domain}/bimi/logoadmin scope

Send a multipart/form-data request with a file part. The file is an SVG of 1 MiB or less. Sendara checks the file against the SVG Tiny PS profile, and serves the logo from a stable HTTPS URL. The response has the same shape as the GET. A file that fails the profile returns invalid_request (400), and a larger file returns payload_too_large (413).

Example request
curl https://api.sendara.dev/v1/domains/mail.acme.com/bimi/logo \
  -H "Authorization: Bearer sk_live_xxx" \
  -F "[email protected]"
Response · 200
{
  "logo_url": "https://assets.sendara.dev/v1/bimi/mail.acme.com/logo.svg",
  "record": {
    "type": "TXT",
    "name": "default._bimi.mail.acme.com",
    "value": "v=BIMI1; l=https://assets.sendara.dev/v1/bimi/mail.acme.com/logo.svg;"
  },
  "dmarc": { "at_enforcement": true, "recommended": "v=DMARC1; p=reject; rua=mailto:[email protected]" },
  "bimi_published": false,
  "vmc_note": "Gmail and Apple Mail require a paid VMC tied to a registered trademark; Yahoo and Fastmail display the logo without one."
}

Get the DNS setup guide

GET/v1/domains/{domain}/dns-setupread scope

Return the DNS records of the domain, the DNS provider that Sendara detected, and guidance for that provider. The dashboard uses this data to show where to go and what to paste. Each record carries a host field, which is the name relative to the zone apex, for a provider that adds the zone itself. The response also carries the verification status of each record.

Response · 200
{
  "domain": "mail.acme.com",
  "zone_apex": "acme.com",
  "provider": {
    "id": "cloudflare",
    "name": "Cloudflare",
    "docs_url": "https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records/",
    "dashboard_url": "https://dash.cloudflare.com/?to=/:account/:zone/dns",
    "supports_api": true,
    "unproxy_hint": true,
    "record_name_label": "Name",
    "record_value_label": "Content"
  },
  "records": [
    { "type": "CNAME", "name": "abc123._domainkey.mail.acme.com",
      "host": "abc123._domainkey.mail", "value": "abc123.dkim.amazonses.com" },
    { "type": "CNAME", "name": "def456._domainkey.mail.acme.com",
      "host": "def456._domainkey.mail", "value": "def456.dkim.amazonses.com" },
    { "type": "CNAME", "name": "ghi789._domainkey.mail.acme.com",
      "host": "ghi789._domainkey.mail", "value": "ghi789.dkim.amazonses.com" },
    { "type": "MX", "name": "mail.mail.acme.com",
      "host": "mail.mail", "value": "10 feedback-smtp.us-east-1.amazonses.com" },
    { "type": "TXT", "name": "mail.mail.acme.com",
      "host": "mail.mail", "value": "v=spf1 include:amazonses.com ~all" },
    { "type": "TXT", "name": "_dmarc.mail.acme.com",
      "host": "_dmarc.mail", "value": "v=DMARC1; p=none; rua=mailto:[email protected]" }
  ],
  "mail_from_domain": "mail.mail.acme.com",
  "dkim_status": "pending",
  "spf_status": "pending",
  "dmarc_status": "pending",
  "txt_status": "pending"
}

Download the zone file

GET/v1/domains/{domain}/dns-setup/zonefileread scope

Return the DNS records of the domain as a zone fragment in BIND format (text/plain). You can then import every record at once, instead of copying one field at a time.

Example request
curl https://api.sendara.dev/v1/domains/mail.acme.com/dns-setup/zonefile \
  -H "Authorization: Bearer sk_live_xxx" -o mail.acme.com.zone.txt
Response · 200
; Sendara sending DNS records for mail.acme.com
; Add these records at your DNS provider, then click Verify in the dashboard.

abc123._domainkey.mail.acme.com.	3600	IN	CNAME	abc123.dkim.amazonses.com.
def456._domainkey.mail.acme.com.	3600	IN	CNAME	def456.dkim.amazonses.com.
ghi789._domainkey.mail.acme.com.	3600	IN	CNAME	ghi789.dkim.amazonses.com.
mail.mail.acme.com.	3600	IN	MX	10 feedback-smtp.us-east-1.amazonses.com.
mail.mail.acme.com.	3600	IN	TXT	"v=spf1 include:amazonses.com ~all"
_dmarc.mail.acme.com.	3600	IN	TXT	"v=DMARC1; p=none; rua=mailto:[email protected]"

Delete a domain

DELETE/v1/domains/{domain}admin scope

Remove a sending domain permanently, and delete its Amazon SES identity. Sendara refuses to delete the built-in sandbox domain, and returns cannot_delete_sandbox (422).

Response · 204
# 204 No Content

API keys

Create an API key, rotate it and revoke it. These endpoints need the admin scope.

Create an API key

POST/v1/keysadmin scope

Create a key with a scope. Sendara returns the plain-text secret once. Store the secret in your secret manager.

Body parameters
scopestringOptional
send, read or admin.
test_modebooleanOptional
Set this to true for a test-mode key (sk_test_…). The default is 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. The old secret stops working at once. Sendara returns the new plain-text key one time.

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

Revoke an API key

DELETE/v1/keys/{id}admin scope

Revoke a key permanently. A later request with that key returns 401.

Response · 204
# 204 No Content

Suppressions

Control the recipients that must receive no message on a channel.

List suppressions

GET/v1/suppressionsread scope

Return the suppressed recipients of the account. Pass channel to see one channel. Omit channel to see every channel. Sendara returns only the entries that suppress a recipient now.

Query parameters
channelstringOptional
Return only this channel, for example email. Omit it for every channel.
Example request
curl "https://api.sendara.dev/v1/suppressions?channel=email" \
  -H "Authorization: Bearer sk_live_xxx"
Response · 200
{
  "suppressions": [
    { "channel": "email", "recipient": "[email protected]", "state": "suppressed",
      "reason": "hard_bounce", "updated_at": "2026-06-14T10:00:00Z" }
  ]
}

Suppress a recipient

POST/v1/suppressionsadmin scope

Block every later send to a recipient on the channel that you name.

Body parameters
channelstringRequired
The channel to suppress the recipient on.
recipientstringRequired
The email address or the phone number.
reasonstringOptional
The reason for the suppression, for example hard_bounce or complaint. This field is optional.
Example request
curl https://api.sendara.dev/v1/suppressions \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "email", "recipient": "[email protected]", "reason": "hard_bounce" }'
Response · 201
{
  "channel": "email",
  "recipient": "[email protected]",
  "state": "suppressed",
  "reason": "hard_bounce",
  "updated_at": "2026-06-14T10:00:00Z"
}

Bulk-import suppressions

POST/v1/suppressions/importadmin scope

Add many suppressed recipients at once when moving from another provider. The default channel is email. Every imported address is blocked from transactional sending. Sendara normalizes addresses, removes duplicates, and accepts up to 10000 entries.

Body parameters
entriesarrayRequired
Recipients shaped as { email, reason?, state? }. state is suppressed (default) or unsubscribed.
channelstringOptional
The channel to suppress the recipients on. The default is email.
Example request
curl https://api.sendara.dev/v1/suppressions/import \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "email",
    "entries": [
      { "email": "[email protected]", "reason": "hard_bounce" },
      { "email": "[email protected]", "reason": "complaint" }
    ]
  }'
Response · 200
{ "imported": 2, "skipped": 0 }

Remove a suppression

DELETE/v1/suppressionsadmin scope

Remove the suppression, so that the recipient receives messages again.

Query parameters
channelstringRequired
The channel that carries the suppression.
recipientstringRequired
The suppressed recipient to remove.
Response · 204
# 204 No Content

Billing

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

Get billing state

GET/v1/billingread scope

Return the current transactional email plan, subscription status, and included volume.

Response · 200
{ "plan": "pro", "subscription_status": "active",
  "included_allotment": 50000, "recurring_interval": "month" }

Start a checkout

POST/v1/billing/checkoutadmin scope

Start a Polar-hosted checkout for a transactional email plan. If an active subscription already exists, Sendara changes it in place. The current promotional annual prices are $59 Starter, $160 Pro, $340 Growth, and $790 Scale; each grants 12 times the monthly email volume as one annual billing-cycle pool, not monthly resets. A new checkout returns a url; an in-place change returns updated=true with the plan.

Body parameters
planstringOptional
The plan to subscribe to: starter, pro, growth or scale. Omit it to default to pro; an unknown value returns 400.
periodstringOptional
The billing period: month or year. The default is month. A yearly period uses one annual Polar cycle and an annual included-volume pool.
Example request
curl https://api.sendara.dev/v1/billing/checkout \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "plan": "pro", "period": "year" }'
Response · 200
{ "url": "https://checkout.polar.sh/..." }

Open the customer portal

POST/v1/billing/portaladmin scope

Return a Polar customer-portal URL. In the portal you change the subscription, the payment method and the invoices.

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

Templates

Store email content that you use again, with Handlebars {{variables}}. Render a template on a send with template_id and template_vars. Preview a template with the render endpoint.

Create a template

POST/v1/templatesadmin scope

Store an email template. The subject and the body accept {{ name }} variables. Each variable carries a sample value, a default value, and a flag that marks it required.

Body parameters
namestringRequired
The name of the template, for a person to read.
channelstringRequired
email, the only channel that Sendara runs today.
subjectstringOptional
The subject line. It accepts {{variables}}.
body_htmlstringOptional
The HTML body. It accepts {{variables}}.
body_textstringOptional
The plain-text version. This field is optional.
body_jsonobjectOptional
The design JSON from the visual builder. This field is optional.
variablesarrayOptional
The data about each variable: [{ 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 templates of the account. The newest template comes 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

Return one template, its full body, and the data about each variable.

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}admin scope

Change the content or the name of a template. A change to the body or to the variables creates a new version.

Body parameters
namestringOptional
The new name.
subjectstringOptional
The new subject line.
body_htmlstringOptional
The new HTML body.
body_textstringOptional
The new plain-text body.
body_jsonobjectOptional
The new design JSON.
variablesarrayOptional
The data that replaces the current variables.
is_activebooleanOptional
Set this to false to stop a send from using the template.
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. Sendara returns the subject and the bodies, and sends no mail. An absent required variable returns missing_variable (400).

Body parameters
varsobjectRequired
The variable values to put into the template, for example { "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}admin scope

Permanently delete a template.

Response · 200
{ "message": "Template deleted" }

Preview arbitrary source

POST/v1/templates/previewread scope

Render inline subject source and body source with a set of variables. Sendara stores no template. Your account snippets act as partials. The response carries the rendered output, the warnings (for example a variable that the source names but you did not supply), and the render errors. The request itself still succeeds.

Body parameters
subjectstringOptional
The subject source. It accepts {{variables}}.
body_htmlstringOptional
The HTML body source. It accepts {{variables}} and snippet partials.
body_textstringOptional
The plain-text body source.
variablesobjectOptional
The variable values to put into the source.
channelstringOptional
The channel to render for. The default is email.
Example request
curl https://api.sendara.dev/v1/templates/preview \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Welcome, {{ first_name }}",
    "body_html": "<h1>Hi {{ first_name }}</h1>{{> footer }}",
    "variables": { "first_name": "Ada" }
  }'
Response · 200
{
  "subject": "Welcome, Ada",
  "html": "<h1>Hi Ada</h1><p>Acme Inc</p>",
  "text": "",
  "warnings": [],
  "errors": []
}
GET/v1/templates/galleryread scope

Return the 20 templates in the built-in gallery. Each one carries a finished design and a set of sample variables that you copy into a new template.

Response · 200
{
  "templates": [
    {
      "id": "welcome-basic",
      "name": "Welcome",
      "category": "onboarding",
      "description": "A clean welcome email for new signups.",
      "subject": "Welcome to {{ company }}",
      "body_html": "<h1>Welcome, {{ first_name }}</h1>",
      "sample_variables": { "company": "Acme", "first_name": "Ada" }
    }
  ]
}

List snippets

GET/v1/templates/snippetsread scope

Return the snippets of the account. A snippet is a named block of HTML or text, for example a header or a footer that several templates share. A template calls a snippet as a partial: {{> name }}.

Response · 200
{
  "snippets": [
    {
      "id": "snip_a1b2c3",
      "account_id": "acc_abc123",
      "name": "footer",
      "body_html": "<p>Acme Inc · 123 Main St</p>",
      "body_text": "Acme Inc · 123 Main St",
      "created_at": "2026-06-14T10:00:00Z",
      "updated_at": "2026-06-14T10:00:00Z"
    }
  ]
}

Create a snippet

POST/v1/templates/snippetsadmin scope

Create a snippet that your templates share. The name must be unique for the account, and a duplicate name returns conflict (409). Call the snippet from a template body as {{> name }}.

Body parameters
namestringRequired
The unique name of the snippet. A template uses this name to call the partial.
body_htmlstringOptional
The HTML body of the snippet.
body_textstringOptional
The plain-text body of the snippet.
Example request
curl https://api.sendara.dev/v1/templates/snippets \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "name": "footer", "body_html": "<p>Acme Inc</p>" }'
Response · 201
{
  "id": "snip_a1b2c3",
  "account_id": "acc_abc123",
  "name": "footer",
  "body_html": "<p>Acme Inc</p>",
  "body_text": "",
  "created_at": "2026-06-14T10:00:00Z",
  "updated_at": "2026-06-14T10:00:00Z"
}

Get a snippet

GET/v1/templates/snippets/{id}read scope

Return one snippet.

Response · 200
{
  "id": "snip_a1b2c3",
  "account_id": "acc_abc123",
  "name": "footer",
  "body_html": "<p>Acme Inc · 123 Main St</p>",
  "body_text": "Acme Inc · 123 Main St",
  "created_at": "2026-06-14T10:00:00Z",
  "updated_at": "2026-06-14T10:00:00Z"
}

Update a snippet

PATCH/v1/templates/snippets/{id}admin scope

Change the name of a snippet, or change its bodies.

Body parameters
namestringOptional
The new name of the snippet.
body_htmlstringOptional
The new HTML body.
body_textstringOptional
The new plain-text body.
Example request
curl -X PATCH https://api.sendara.dev/v1/templates/snippets/snip_a1b2c3 \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "body_html": "<p>Acme Inc · [email protected]</p>" }'
Response · 200
{
  "id": "snip_a1b2c3",
  "account_id": "acc_abc123",
  "name": "footer",
  "body_html": "<p>Acme Inc · [email protected]</p>",
  "body_text": "Acme Inc · 123 Main St",
  "created_at": "2026-06-14T10:00:00Z",
  "updated_at": "2026-06-14T11:00:00Z"
}

Delete a snippet

DELETE/v1/templates/snippets/{id}admin scope

Permanently delete a snippet.

Response · 204
# 204 No Content

Webhooks

Subscribe an HTTPS endpoint to the delivery events, such as delivered, bounced, complained and opened. Each subscription carries a signing secret. Use the secret to check the Sendara-Signature header. Sendara retries a failed delivery up to 12 attempts in total.

Create a webhook

POST/v1/webhooksadmin scope

Register an endpoint and the event types that Sendara delivers to it. Sendara returns the signing secret once in this create response. Store it like a password on your server; list, get, and update responses omit secrets.

Body parameters
endpoint_urlstringRequired
The HTTPS URL that receives the events by POST.
event_typesarrayOptional
The event types to deliver, for example ["delivered", "bounced"]. Omit this field for every type.
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": "5f3a39ab8c2d4e6f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5",
  "is_active": true,
  "created_at": "2026-06-14T10:00:00Z"
}

List webhooks

GET/v1/webhooksread scope

Return the webhook subscriptions of the account. Signing secrets are omitted.

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

Return one webhook subscription. Signing secrets are omitted.

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, the event types or the active state of a subscription.

Body parameters
endpoint_urlstringOptional
The new endpoint URL.
event_typesarrayOptional
The new list of event types.
is_activebooleanOptional
Pause the deliveries, or start them again.
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 return it once. Keep your stored old value while handlers roll out the new value, and temporarily accept either at the receiver. The previous secret is never returned by the API.

Response · 200
{ "id": "wh_a1b2c3",
  "signing_secret": "9b2c0a17e4d5f6a8b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e" }

List deliveries

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

Return the recent delivery attempts for a subscription, with the status and the response code of each attempt.

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

Delete a webhook

DELETE/v1/webhooks/{id}admin scope

Delete a subscription permanently. Sendara then delivers no more events to it.

Response · 200
{ "message": "Webhook subscription deleted" }

Test recipients

Register up to 3 of your own addresses, and verify each one by email. You then send real email to them with test_send: true, and Sendara charges nothing. Each address accepts 10 test sends in a day. Use this group to check your production setup without a charge.

Register a test recipient

POST/v1/test-recipientsadmin scope

Register an address. Sendara sends a verification email to it. A fourth address returns too_many_test_recipients (422).

Body parameters
emailstringRequired
An address that you control. Sendara emails a verification link 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": "[email protected]" }'
Response · 201
{
  "id": "tr_a1b2c3",
  "email": "[email protected]",
  "status": "pending",
  "verified_at": null,
  "created_at": "2026-06-14T10:00:00Z"
}

List test recipients

GET/v1/test-recipientsread scope

Return the test recipients that you registered, and the verification status of each one.

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

Resend verification

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

Send the verification email again to a test recipient that is still pending.

Response · 204
# 204 No Content

Delete a test recipient

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

Remove a registered test recipient. One of your 3 slots then becomes free.

Response · 204
# 204 No Content

Uploads

Host the images that your templates and your emails use. Upload a PNG, a JPEG, a GIF or a WebP of 2 MiB or less. Sendara returns a public URL.

Upload an image

POST/v1/uploadsadmin scope

Send a multipart/form-data request with a file part. Sendara returns a public URL that you put in your HTML. A file above 2 MiB returns payload_too_large (413).

Example request
curl https://api.sendara.dev/v1/uploads \
  -H "Authorization: Bearer sk_live_xxx" \
  -F "[email protected]"
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
unauthorized401The API key is absent or wrong. Check the Authorization: Bearer header. To recover, send a valid sk_live_ key or sk_test_ key.
forbidden403The scope of the key does not cover this operation. To recover, use a key that holds the send, read or admin scope that the endpoint needs.
invalid_request400The request body or a parameter is malformed, or a required field is absent. To recover, correct the field that the message names, then send the request again.
not_found404The resource does not exist, or it belongs to another account. To recover, check the id.
recipient_suppressed409The recipient sits on the suppression list for this channel. The error fires on a send. To recover, remove the suppression, or send to a different address.
conflict409A resource already holds the same unique value. The error fires when you create a mail inbox address that exists. To recover, use the existing resource, or choose a different value.
idempotency_key_reused409You reused the idempotency_key with a different request body. To recover, use a new key, or send the original body again to read the stored result.
from_required422Your own domain needs metadata.from_email, and you omitted it. The error fires on a send. To recover, set from_email (the from field in the SDK) to an address on a verified domain. A sandbox account with no verified domain leaves the field empty.
from_not_verified422The from address does not sit on a verified domain. To recover, verify the sending domain first.
domain_not_ready422The domain does not yet support this feature. The error fires when you create a mail inbox on a domain that is not verified, not outside the sandbox, or not enabled for inbound mail. To recover, verify the domain, then turn inbound mail on.
no_verified_domain422Sendara Mail found no verified sender domain. To recover, verify a sending domain, or pass a from address on a verified domain.
missing_variable400The template render needs a {{variable}} that you did not supply. To recover, supply every required variable in template_vars or vars.
invalid_template400The syntax of the template is wrong, or the template that you named is inactive. To recover, correct the template body, or activate the template.
invalid_token400A verification token or an action token is malformed, or it expired. To recover, request a new link.
invalid_signature403A provider callback or a billing webhook that Sendara receives failed HMAC verification. The cause is a wrong secret, an altered body, or a stale timestamp. Sendara returns this code on the endpoints that Sendara receives on, and not on the outbound webhooks that it sends to you. To recover, compute HMAC-SHA256 again over "<timestamp>.<rawBody>" with the correct signing secret.
rate_limit_exceeded429You sent more than 10 requests in one second. The response carries a Retry-After header and the X-RateLimit-* headers. To recover, wait until the time in X-RateLimit-Reset, then send the request again.
spend_cap_exceeded402The cost of the period reached the spend cap of the account. To recover, raise the cap in billing, or wait for the next period.
billing_not_configured503You called a billing endpoint on a deployment that does not configure billing. This is a condition of the environment, not a fault of the request. To recover, configure billing, or contact support.
too_many_test_recipients422You already registered the maximum of 3 test recipients. To recover, delete one recipient, then add the new one.
recipient_not_verified403The target of a test_send is not a verified test recipient of this account. To recover, register the address, then verify it.
test_send_daily_limit429The address reached the cap of 10 test sends in a day. To recover, wait for the next UTC day, or use a different verified recipient.
payload_too_large413The request body or the upload is above the size limit. An image upload caps at 2 MiB, and a BIMI logo caps at 1 MiB. To recover, make the payload smaller.
internal_error500The server met an unexpected error. To recover, wait, then send the request again. Contact support when the error continues.