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
Send one email. Email is the only send channel that Sendara runs today.
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]" }
}'{
"id": "msg_a1b2c3",
"status": "queued",
"channel": "email",
"idempotency_key": "evt_welcome_8f3a",
"created_at": "2026-06-14T10:00:00Z"
}Send a batch
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.
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>" } }
]'[
{ "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
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.
curl https://api.sendara.dev/v1/validate \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]" }'{ "email": "[email protected]", "valid": true, "reason": "" }Messages
Read sent messages and their event timeline.
List messages
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.
{
"messages": [
{ "id": "msg_a1b2c3", "channel": "email", "status": "delivered",
"message_type": "transactional", "created_at": "2026-06-14T10:00:00Z" }
],
"next_cursor": null
}Get a message
Return one message and its full event timeline.
{
"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
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.
{ "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
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.
curl "https://api.sendara.dev/v1/inbound?limit=30" \
-H "Authorization: Bearer sk_live_xxx"{
"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
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.
{
"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
Return the original RFC 822 message as message/rfc822. The endpoint returns not_found (404) when Sendara did not keep the raw message.
curl https://api.sendara.dev/v1/inbound/inb_9f21/raw \
-H "Authorization: Bearer sk_live_xxx" -o message.eml# Content-Type: message/rfc822 (the original .eml bytes)Download an attachment
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).
curl https://api.sendara.dev/v1/inbound/inb_9f21/attachments/0 \
-H "Authorization: Bearer sk_live_xxx" -o receipt.pdf# Content-Type: application/pdf (the attachment bytes)List inbound routes
Return the forwarding routes of the account. Sendara omits the signing secret.
{
"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
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.
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"
}'{
"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
Change the match prefix, the destination or the active state of a route. Sendara changes only the fields that the body contains.
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 }'{
"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 a forwarding route permanently. Sendara then forwards no more matching mail.
# 204 No ContentEnable inbound receiving
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.
curl https://api.sendara.dev/v1/domains/dom_1/inbound \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "enabled": true }'{
"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
Return the role inboxes that you configured on your verified inbound domains, for example [email protected] and [email protected].
curl https://api.sendara.dev/v1/mail/inboxes \
-H "Authorization: Bearer sk_live_xxx"{
"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
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.
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" }'{
"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
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
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.
curl "https://api.sendara.dev/v1/mail/threads?folder=inbox&limit=30" \
-H "Authorization: Bearer sk_live_xxx"{
"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
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.
curl "https://api.sendara.dev/v1/mail/search?q=invoice&inbox=support%40mail.acme.com" \
-H "Authorization: Bearer sk_live_xxx"{
"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
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.
curl "https://api.sendara.dev/v1/mail/export?format=json&inbox=support%40mail.acme.com&q=has%3Aattachment" \
-H "Authorization: Bearer sk_live_xxx"{
"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
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.
{
"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
Mark every message in a thread as read or unread. An omitted body or read field defaults to true.
{ "id": "thr_5c10", "read": true }Set a thread's starred state
Star or unstar every message in a thread. An omitted body or starred field defaults to true.
{ "id": "thr_5c10", "starred": true }Set a message's starred state
Star or unstar one message. Set source in the path to inbound or outbound. An omitted body or starred field defaults to true.
{ "source": "inbound", "id": "inb_9f21", "starred": true }List mail labels
Return the user-defined labels of the account, ordered by name.
{ "labels": [
{ "id": "lbl_a1b2c3", "name": "Important", "color": "#f59e0b",
"created_at": "2026-06-17T10:00:00Z" }
] }Create a mail label
Create a user-defined label. Label names are unique without regard to case.
{ "id": "lbl_a1b2c3", "name": "Important", "color": "#f59e0b",
"created_at": "2026-06-17T10:00:00Z" }Delete a mail label
Delete a user-defined label and remove it from every message that carries it.
# 204 No ContentCompose from Mail
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.
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."
}'{
"id": "msg_a1b2c3",
"status": "queued",
"channel": "email",
"idempotency_key": "mail_...",
"created_at": "2026-06-17T10:05:00Z"
}Reply to a thread
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.
{ "id": "msg_reply1", "status": "queued", "channel": "email", "idempotency_key": "mail_..." }Forward a thread
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.
{ "id": "msg_forward1", "status": "queued", "channel": "email", "idempotency_key": "mail_..." }Move a thread
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.
{ "id": "thr_5c10", "folder": "archive" }Update thread labels
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.
{ "id": "thr_5c10" }Usage
Read send counts and the internal estimated-spend ledger used for spend-cap enforcement in a billing period.
Get usage
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.
{
"period": "2026-06",
"total_send_count": 18420,
"total_cost_micros": 13815000,
"channels": [
{ "channel": "email", "send_count": 18420, "cost_micros": 13815000 }
]
}Set a spend cap
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.
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 }'{
"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
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.
curl https://api.sendara.dev/v1/account/verification \
-H "Authorization: Bearer sk_live_xxx"{
"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
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.
curl https://api.sendara.dev/v1/domains \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "domain": "mail.acme.com" }'{
"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
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.
{
"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
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.
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" }'{ "domain": "mail.acme.com", "from_name": "Acme" }Get BIMI state
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.
{
"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
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.
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" }'{
"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."
}Upload a BIMI logo
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).
curl https://api.sendara.dev/v1/domains/mail.acme.com/bimi/logo \
-H "Authorization: Bearer sk_live_xxx" \
-F "[email protected]"{
"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
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.
{
"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
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.
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; 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
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).
# 204 No ContentAPI keys
Create an API key, rotate it and revoke it. These endpoints need the admin scope.
Create an API key
Create a key with a scope. Sendara returns the plain-text secret once. Store the secret in your secret manager.
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 }'{
"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
Issue a new secret for the key. The old secret stops working at once. Sendara returns the new plain-text key one time.
{ "key": "sk_live_9b2c0a17e4d5f6a8" }Revoke an API key
Revoke a key permanently. A later request with that key returns 401.
# 204 No ContentSuppressions
Control the recipients that must receive no message on a channel.
List suppressions
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.
curl "https://api.sendara.dev/v1/suppressions?channel=email" \
-H "Authorization: Bearer sk_live_xxx"{
"suppressions": [
{ "channel": "email", "recipient": "[email protected]", "state": "suppressed",
"reason": "hard_bounce", "updated_at": "2026-06-14T10:00:00Z" }
]
}Suppress a recipient
Block every later send to a recipient on the channel that you name.
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" }'{
"channel": "email",
"recipient": "[email protected]",
"state": "suppressed",
"reason": "hard_bounce",
"updated_at": "2026-06-14T10:00:00Z"
}Bulk-import suppressions
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.
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" }
]
}'{ "imported": 2, "skipped": 0 }Remove a suppression
Remove the suppression, so that the recipient receives messages again.
# 204 No ContentBilling
Read your plan, and open the Polar checkout or the Polar customer portal.
Get billing state
Return the current transactional email plan, subscription status, and included volume.
{ "plan": "pro", "subscription_status": "active",
"included_allotment": 50000, "recurring_interval": "month" }Start a checkout
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.
curl https://api.sendara.dev/v1/billing/checkout \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "plan": "pro", "period": "year" }'{ "url": "https://checkout.polar.sh/..." }Open the customer portal
Return a Polar customer-portal URL. In the portal you change the subscription, the payment method and the invoices.
{ "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
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.
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 }
]
}'{
"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
Return the templates of the account. The newest template comes first.
{
"templates": [
{ "id": "tmpl_a1b2c3", "name": "Welcome", "channel": "email",
"version": 1, "is_active": true, "created_at": "2026-06-14T10:00:00Z" }
]
}Get a template
Return one template, its full body, and the data about each variable.
{
"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
Change the content or the name of a template. A change to the body or to the variables creates a new version.
{ "id": "tmpl_a1b2c3", "name": "Welcome back", "version": 2 }Render a preview
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).
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" } }'{
"subject": "Welcome, Ada",
"body_html": "<h1>Hi Ada</h1>",
"body_text": null
}Delete a template
Permanently delete a template.
{ "message": "Template deleted" }Preview arbitrary source
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.
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" }
}'{
"subject": "Welcome, Ada",
"html": "<h1>Hi Ada</h1><p>Acme Inc</p>",
"text": "",
"warnings": [],
"errors": []
}List gallery templates
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.
{
"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
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 }}.
{
"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
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 }}.
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>" }'{
"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
Return one snippet.
{
"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
Change the name of a snippet, or change its bodies.
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>" }'{
"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
Permanently delete a snippet.
# 204 No ContentWebhooks
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
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.
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"] }'{
"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
Return the webhook subscriptions of the account. Signing secrets are omitted.
{
"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
Return one webhook subscription. Signing secrets are omitted.
{ "id": "wh_a1b2c3", "endpoint_url": "https://acme.com/hooks/sendara",
"event_types": ["delivered", "bounced"], "is_active": true }Update a webhook
Change the endpoint, the event types or the active state of a subscription.
{ "id": "wh_a1b2c3", "is_active": false }Rotate the signing secret
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.
{ "id": "wh_a1b2c3",
"signing_secret": "9b2c0a17e4d5f6a8b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f708192a3b4c5d6e" }List deliveries
Return the recent delivery attempts for a subscription, with the status and the response code of each attempt.
{
"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 a subscription permanently. Sendara then delivers no more events to it.
{ "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
Register an address. Sendara sends a verification email to it. A fourth address returns too_many_test_recipients (422).
curl https://api.sendara.dev/v1/test-recipients \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]" }'{
"id": "tr_a1b2c3",
"email": "[email protected]",
"status": "pending",
"verified_at": null,
"created_at": "2026-06-14T10:00:00Z"
}List test recipients
Return the test recipients that you registered, and the verification status of each one.
{
"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
Send the verification email again to a test recipient that is still pending.
# 204 No ContentDelete a test recipient
Remove a registered test recipient. One of your 3 slots then becomes free.
# 204 No ContentUploads
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
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).
curl https://api.sendara.dev/v1/uploads \
-H "Authorization: Bearer sk_live_xxx" \
-F "[email protected]"{
"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
}
}| Code | Status | Description |
|---|---|---|
| unauthorized | 401 | The API key is absent or wrong. Check the Authorization: Bearer header. To recover, send a valid sk_live_ key or sk_test_ key. |
| forbidden | 403 | The 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_request | 400 | The 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_found | 404 | The resource does not exist, or it belongs to another account. To recover, check the id. |
| recipient_suppressed | 409 | The 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. |
| conflict | 409 | A 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_reused | 409 | You 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_required | 422 | Your 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_verified | 422 | The from address does not sit on a verified domain. To recover, verify the sending domain first. |
| domain_not_ready | 422 | The 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_domain | 422 | Sendara Mail found no verified sender domain. To recover, verify a sending domain, or pass a from address on a verified domain. |
| missing_variable | 400 | The template render needs a {{variable}} that you did not supply. To recover, supply every required variable in template_vars or vars. |
| invalid_template | 400 | The 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_token | 400 | A verification token or an action token is malformed, or it expired. To recover, request a new link. |
| invalid_signature | 403 | A 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_exceeded | 429 | You 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_exceeded | 402 | The 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_configured | 503 | You 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_recipients | 422 | You already registered the maximum of 3 test recipients. To recover, delete one recipient, then add the new one. |
| recipient_not_verified | 403 | The 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_limit | 429 | The 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_large | 413 | The 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_error | 500 | The server met an unexpected error. To recover, wait, then send the request again. Contact support when the error continues. |