All requests are made over HTTPS to https://api.sendara.dev and authenticated with a Bearer key. Timestamps are RFC 3339; monetary amounts are micro-dollars (1,000,000 = $1.00). Path parameters are shown in {braces}.
Send
Send messages across channels — one at a time or in batches.
Send a message
Send a single message on any channel. An idempotency key is required — retrying with the same key returns the original result instead of sending twice.
curl https://api.sendara.dev/v1/send \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"channel": "email",
"idempotency_key": "evt_welcome_8f3a",
"message_type": "transactional",
"destination": { "email": "user@example.com" },
"payload": {
"subject": "Welcome to Acme",
"body_html": "<h1>Welcome 🎉</h1>"
},
"metadata": { "from_email": "hello@acme.com" }
}'{
"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. Each item is processed independently; the response preserves request order with per-item success or error. 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": "a@acme.com" },
"payload": { "subject": "Hi", "body_html": "<p>Hi</p>" } },
{ "channel": "email", "idempotency_key": "b2",
"destination": { "email": "b@acme.com" },
"payload": { "subject": "Hi", "body_html": "<p>Hi</p>" } }
]'[
{ "success": true,
"response": { "id": "msg_1", "status": "queued", "channel": "email",
"idempotency_key": "b1", "created_at": "2026-06-14T10:00:00Z" } },
{ "success": false,
"error": { "code": "recipient_suppressed",
"message": "Recipient is on the suppression list", "status": 409 } }
]Broadcasts
Bulk email campaigns to an audience or an inline recipient list.
Send a bulk email
Send one email to many recipients in a single call. Provide an audience_list_id (a contact list) or an inline recipients array, plus inline content (subject + body_html) or a template_id rendered per recipient. Fans out asynchronously and returns a broadcast to poll. Suppressed and unsubscribed recipients are skipped; each recipient is idempotent.
curl https://api.sendara.dev/v1/send/bulk \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"from_email": "hello@mail.acme.com",
"subject": "News from Acme",
"body_html": "<h1>Hi {{first_name}}</h1>",
"audience_list_id": "list_9f21"
}'{
"id": "bc_a1b2c3",
"name": "Bulk send",
"status": "sending",
"from_email": "hello@mail.acme.com",
"total_recipients": 0,
"sent_count": 0,
"failed_count": 0,
"created_at": "2026-06-14T10:00:00Z"
}List broadcasts
Return the account's broadcasts, newest first.
{
"broadcasts": [
{ "id": "bc_a1b2c3", "name": "October newsletter", "status": "sent",
"from_email": "hello@mail.acme.com", "total_recipients": 1840,
"sent_count": 1840, "failed_count": 0, "created_at": "2026-06-14T10:00:00Z" }
]
}Get a broadcast
Return a broadcast with aggregate delivery stats.
{
"broadcast": { "id": "bc_a1b2c3", "status": "sent", "total_recipients": 1840 },
"stats": { "total": 1840, "sent": 1840, "delivered": 1798,
"opened": 642, "bounced": 31, "complained": 2, "failed": 0 }
}Messages
Read sent messages and their event timeline.
List messages
Return a list of message summaries, filtered by channel, status, and time range.
{
"messages": [
{ "id": "msg_a1b2c3", "channel": "email", "status": "delivered",
"message_type": "transactional", "created_at": "2026-06-14T10:00:00Z" }
]
}Get a message
Retrieve a single message with its full event timeline.
{
"id": "msg_a1b2c3",
"channel": "email",
"status": "delivered",
"message_type": "transactional",
"created_at": "2026-06-14T10:00:00Z",
"events": [
{ "id": "ev_1", "type": "accepted", "occurred_at": "2026-06-14T10:00:00Z" },
{ "id": "ev_2", "type": "delivered", "occurred_at": "2026-06-14T10:00:12Z" }
]
}Usage
Account usage and cost for a billing period.
Get usage
Return the send count and cost (in micro-dollars) for the period, broken down by channel.
{
"period": "2026-06",
"total_send_count": 18420,
"total_cost_micros": 9210000,
"channels": [
{ "channel": "email", "send_count": 18000, "cost_micros": 9000000 },
{ "channel": "sms", "send_count": 420, "cost_micros": 210000 }
]
}Domains
Add and verify sending domains. Requires the admin scope.
Add a sending domain
Register a domain and receive the DNS records to publish: three DKIM CNAMEs, a custom MAIL FROM (MX + SPF), and DMARC.
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": "s1._domainkey.mail.acme.com",
"value": "s1.dkim.sendara.dev" }
],
"mail_from_domain": "mail.acme.com",
"created_at": "2026-06-14T10:00:00Z"
}Verify a domain
Re-check DNS status and return the per-record verification result.
{
"domain": "mail.acme.com",
"fully_verified": true,
"results": [
{ "field": "dkim", "type": "CNAME", "name": "s1._domainkey.mail.acme.com",
"status": "verified", "detail": "" }
]
}API keys
Create, rotate, and revoke API keys. Requires the admin scope.
Create an API key
Create a scoped key. The plaintext secret is returned exactly once — store it securely.
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 and invalidate the old one. The new plaintext key is returned once.
{ "key": "sk_live_9b2c0a17e4d5f6a8" }Revoke an API key
Permanently revoke a key. Subsequent requests with it return 401.
# 204 No ContentSuppressions
Manage recipients that should never receive messages on a channel.
Suppress a recipient
Block all future sends to a recipient on the given channel.
curl https://api.sendara.dev/v1/suppressions \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "channel": "email", "recipient": "bounce@acme.com", "reason": "hard_bounce" }'{
"channel": "email",
"recipient": "bounce@acme.com",
"state": "suppressed",
"reason": "hard_bounce",
"updated_at": "2026-06-14T10:00:00Z"
}Remove a suppression
Un-suppress a recipient so they can receive messages again.
# 204 No ContentBilling
Read the plan and open Polar checkout or the customer portal.
Get billing state
Return the account's current plan and subscription status.
{ "plan": "pro", "subscription_status": "active" }Start a checkout
Return a hosted Polar checkout URL to subscribe to Pro.
{ "url": "https://checkout.polar.sh/..." }Open the customer portal
Return a hosted Polar customer-portal URL to manage the subscription, payment method, and invoices.
{ "url": "https://polar.sh/acme/portal/..." }Templates
Store reusable email content with mustache {{variables}}. Render a template inline on a send (template_id + template_vars) or preview it with the render endpoint.
Create a template
Store an email template. Subject and body support {{ name }} variables; each variable can declare a sample, default, and whether it is 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 account's templates, newest first.
{
"templates": [
{ "id": "tmpl_a1b2c3", "name": "Welcome", "channel": "email",
"version": 1, "is_active": true, "created_at": "2026-06-14T10:00:00Z" }
]
}Get a template
Retrieve a single template with its full body and variable metadata.
{
"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
Update content or metadata. Editing the body or variables creates a new version.
{ "id": "tmpl_a1b2c3", "name": "Welcome back", "version": 2 }Render a preview
Render the template with a set of variables and return the resulting subject and bodies — without sending. A missing required variable returns missing_variable (400).
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.
# 204 No ContentContacts & lists
Store contacts and organize them into static or dynamic lists. A list (audience) is the target of a broadcast via audience_list_id.
Create a contact
Add a contact. An email address that already exists returns duplicate_contact (409).
curl https://api.sendara.dev/v1/contacts \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"email": "ada@example.com",
"first_name": "Ada",
"tags": ["beta"],
"attributes": { "plan": "pro" }
}'{
"id": "ct_a1b2c3",
"email": "ada@example.com",
"phone_number": null,
"first_name": "Ada",
"last_name": "",
"attributes": { "plan": "pro" },
"tags": ["beta"],
"email_consent": "subscribed",
"created_at": "2026-06-14T10:00:00Z",
"updated_at": "2026-06-14T10:00:00Z"
}List contacts
Return contacts, newest first.
{
"contacts": [
{ "id": "ct_a1b2c3", "email": "ada@example.com",
"first_name": "Ada", "tags": ["beta"],
"email_consent": "subscribed", "created_at": "2026-06-14T10:00:00Z" }
]
}Get a contact
Retrieve a single contact.
{ "id": "ct_a1b2c3", "email": "ada@example.com", "first_name": "Ada" }Update a contact
Update any contact field. Only the fields present in the body are changed.
{ "id": "ct_a1b2c3", "email": "ada@example.com", "first_name": "Augusta" }Delete a contact
Permanently delete a contact and remove it from all lists.
# 204 No ContentImport contacts
Bulk-create or update contacts in one call. Existing emails are upserted.
{ "imported": 240, "updated": 12, "skipped": 0 }Create a list
Create an audience. A static list holds contacts you add explicitly; a dynamic list auto-includes contacts matching segment_rules (tags / attributes).
curl https://api.sendara.dev/v1/contacts/lists \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "name": "Beta users", "list_type": "dynamic",
"segment_rules": { "tags": ["beta"] } }'{
"id": "list_9f21",
"name": "Beta users",
"list_type": "dynamic",
"segment_rules": { "tags": ["beta"] },
"created_at": "2026-06-14T10:00:00Z",
"updated_at": "2026-06-14T10:00:00Z"
}List lists
Return the account's contact lists.
{
"lists": [
{ "id": "list_9f21", "name": "Beta users", "list_type": "dynamic",
"created_at": "2026-06-14T10:00:00Z" }
]
}Add a list member
Add a contact to a static list. Adding a contact already on the list returns duplicate_member (409).
{
"id": "lm_1",
"contact_list_id": "list_9f21",
"contact_id": "ct_a1b2c3",
"added_at": "2026-06-14T10:00:00Z"
}List list members
Return the contacts on a list.
{ "members": [ { "id": "ct_a1b2c3", "email": "ada@example.com" } ] }Remove a list member
Remove a contact from a static list.
# 204 No ContentWebhooks
Subscribe an HTTPS endpoint to delivery events (delivered, bounced, complained, opened, …). Each subscription has a signing secret used to verify the Sendara-Signature header.
Create a webhook
Register an endpoint and the event types to deliver. The signing secret is returned once — store it to verify signatures.
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": "whsec_5f3a39ab8c2d4e6f9a0b1c2d3e4f5a6b",
"is_active": true,
"created_at": "2026-06-14T10:00:00Z"
}List webhooks
Return the account's webhook subscriptions. The signing secret is not included.
{
"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
Retrieve a single webhook subscription.
{ "id": "wh_a1b2c3", "endpoint_url": "https://acme.com/hooks/sendara",
"event_types": ["delivered", "bounced"], "is_active": true }Update a webhook
Change the endpoint, event types, or active state.
{ "id": "wh_a1b2c3", "is_active": false }Rotate the signing secret
Issue a new signing secret and invalidate the old one. The new secret is returned once.
{ "signing_secret": "whsec_9b2c0a17e4d5f6a8b1c2d3e4f5a6b7c8" }List deliveries
Inspect recent delivery attempts for a subscription, including status and response code.
{
"deliveries": [
{ "id": "whd_1", "event_id": "evt_9c1f", "event_type": "delivered",
"status": "succeeded", "response_code": 200, "attempt_count": 1,
"created_at": "2026-06-14T10:00:00Z" }
]
}Delete a webhook
Permanently delete a subscription. No further events are delivered.
# 204 No ContentTest recipients
Register up to 3 of your own addresses, verify them by email, then send REAL emails to them for free (capped 10/address/day) with test_send: true — ideal for prod/UAT validation without billing.
Register a test recipient
Register an address and trigger a verification email. Registering a 4th 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": "qa@yourteam.com" }'{
"id": "tr_a1b2c3",
"email": "qa@yourteam.com",
"status": "pending",
"verified_at": null,
"created_at": "2026-06-14T10:00:00Z"
}List test recipients
Return your registered test recipients and their verification status.
{
"recipients": [
{ "id": "tr_a1b2c3", "email": "qa@yourteam.com", "status": "verified",
"verified_at": "2026-06-14T10:05:00Z", "created_at": "2026-06-14T10:00:00Z" }
]
}Resend verification
Re-send the verification email for a pending test recipient.
# 204 No ContentDelete a test recipient
Remove a registered test recipient, freeing one of your 3 slots.
# 204 No ContentUploads
Host images for use in templates and emails. Upload a PNG, JPEG, GIF, or WebP (max 2 MiB) and receive a public URL.
Upload an image
Send a multipart/form-data request with a file part. Returns a public URL to embed in HTML. Files over 2 MiB return payload_too_large (413).
curl https://api.sendara.dev/v1/uploads \
-H "Authorization: Bearer sk_live_xxx" \
-F "file=@hero.png"{
"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 | Missing or invalid API key. Check the Authorization: Bearer header. Recovery: supply a valid sk_live_/sk_test_ key. |
| forbidden | 403 | The key's scope does not permit this operation. Recovery: use a key with the send, read, or admin scope the endpoint requires. |
| invalid_request | 400 | The request body or parameters are malformed or missing a required field. Recovery: fix the field named in the message and retry. |
| not_found | 404 | The resource does not exist or belongs to another account. Recovery: check the id. |
| recipient_suppressed | 409 | The recipient is on the suppression list for this channel. Fires on send. Recovery: remove the suppression or send to a different address. |
| idempotency_key_reused | 409 | The idempotency_key was reused with a different request body. Recovery: use a new key, or replay the exact original body to get the cached result. |
| from_not_verified | 422 | The from address is not on a verified domain. Fires on send (also 403 in some paths). Recovery: verify the sending domain first. |
| missing_variable | 400 | A template render is missing a required {{variable}}. Recovery: supply every required variable in template_vars / vars. |
| invalid_template | 400 | The template syntax is invalid or the referenced template is inactive. Recovery: fix the template body or activate it. |
| invalid_token | 400 | A verification or action token is malformed or expired. Recovery: request a fresh link. |
| invalid_signature | 401 | Webhook signature verification failed (Sendara-Signature mismatch). Recovery: recompute HMAC-SHA256 over "<timestamp>.<rawBody>" with the subscription's signing secret. |
| rate_limit_exceeded | 429 | Too many requests in the window. Response carries Retry-After and X-RateLimit-* headers. Recovery: back off until X-RateLimit-Reset, then retry. |
| spend_cap_exceeded | 402 | The account's spend cap for the period has been reached. Recovery: raise the cap in billing or wait for the next period. |
| billing_not_configured | 400 | A billing-gated action was attempted before billing is set up. Recovery: complete checkout to configure billing. |
| duplicate_contact | 409 | A contact with this email already exists. Recovery: update the existing contact instead of creating a new one. |
| duplicate_member | 409 | The contact is already a member of this list. Recovery: treat as already-added; no action needed. |
| too_many_test_recipients | 422 | You already have the maximum of 3 registered test recipients. Recovery: delete one before adding another. |
| recipient_not_verified | 403 | A test_send target is not a verified test recipient for this account. Recovery: register and verify the address first. |
| test_send_daily_limit | 429 | The 10-per-address-per-day test-send cap was reached. Recovery: wait until the next UTC day or use a different verified recipient. |
| payload_too_large | 413 | The request body or upload exceeds the size limit (uploads cap at 2 MiB). Recovery: shrink the payload. |
| internal_error | 500 | An unexpected server error. Recovery: retry with backoff; if it persists, contact support. |