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 email. Email is the only generally available send channel 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. 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": "[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
Pre-flight check of a single address — syntax plus domain MX — without sending. Returns validation_not_configured (503) when validation is unavailable.
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": "" }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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]", "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": 18420, "cost_micros": 9210000 }
]
}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": "" }
]
}Get BIMI state
Return the domain's BIMI state: the hosted logo URL, the TXT record to publish (null if no logo is set), whether DMARC is at enforcement with a recommended policy, whether the record is live, and a note on which providers 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 served over HTTPS. Sendara validates it and returns the same shape as the GET, including the generated TXT record.
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 (SVG, max 1 MiB). Sendara validates the SVG Tiny PS profile, hosts the logo on a stable HTTPS URL, and returns the same shape as the GET. A non-compliant or oversized file returns invalid_request (400) or 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."
}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": "[email protected]", "reason": "hard_bounce" }'{
"channel": "email",
"recipient": "[email protected]",
"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 for the chosen plan and billing period. Annual billing is two months free.
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 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": "[email protected]",
"first_name": "Ada",
"tags": ["beta"],
"attributes": { "plan": "pro" }
}'{
"id": "ct_a1b2c3",
"email": "[email protected]",
"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": "[email protected]",
"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": "[email protected]", "first_name": "Ada" }Update a contact
Update any contact field. Only the fields present in the body are changed.
{ "id": "ct_a1b2c3", "email": "[email protected]", "first_name": "Augusta" }Delete a contact
Permanently delete a contact and remove it from all lists.
# 204 No ContentImport contacts from a file
Bulk-import contacts from a previously uploaded CSV or JSON file in object storage. Existing emails are upserted.
curl https://api.sendara.dev/v1/contacts/import \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "s3_key": "imports/acct_123/contacts.csv", "format": "csv" }'{
"success_count": 240,
"error_count": 2,
"errors": [
{ "row": 18, "message": "invalid email" }
]
}Import contacts inline
Import already-mapped rows directly in the request body — no file upload. Set dry_run to validate without writing, and optionally add every imported contact to a list.
curl https://api.sendara.dev/v1/contacts/import/inline \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{ "email": "[email protected]", "first_name": "Ada", "tags": ["beta"] }
],
"list_id": "list_9f21",
"dry_run": true
}'{
"summary": {
"total": 1,
"valid": 1,
"invalid": 0,
"duplicates_existing": 0,
"errors": []
},
"success_count": 0,
"error_count": 0
}Get contact usage
Return the current contact count against your marketing plan's audience limit (a limit of 0 means unlimited).
{ "count": 1840, "limit": 5000 }Bulk contact action
Apply one action to a set of contacts. Ids not owned by your account are silently skipped.
curl https://api.sendara.dev/v1/contacts/bulk \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{ "ids": ["ct_a1b2c3", "ct_d4e5f6"], "action": "add_tag", "tag": "beta" }'{ "affected": 2 }Preview a segment
Count the contacts that match a set of segment rules, without creating a list. Invalid rules return invalid_request (400).
{ "count": 312 }List a contact's lists
Return the lists a contact belongs to.
{
"lists": [
{ "id": "list_9f21", "name": "Beta users", "list_type": "dynamic" }
]
}Get a contact's activity
Return a contact's recent activity timeline (outbound sends and events, plus inbound emails), newest first.
{
"events": [
{ "type": "opened", "channel": "email", "source": "broadcast",
"subject": "October newsletter", "message_id": "msg_a1b2c3",
"occurred_at": "2026-06-14T10:04:30Z" },
{ "type": "delivered", "channel": "email", "source": "broadcast",
"subject": "October newsletter", "message_id": "msg_a1b2c3",
"occurred_at": "2026-06-14T10:00:12Z" }
]
}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": "[email protected]" } ] }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": "[email protected]" }'{
"id": "tr_a1b2c3",
"email": "[email protected]",
"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": "[email protected]", "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 "[email protected]"{
"id": "asset_a1b2c3",
"url": "https://assets.sendara.dev/v1/assets/asset_a1b2c3",
"content_type": "image/png",
"bytes": 48213
}Signup forms
Hosted and embeddable signup forms that subscribe contacts to a list, with optional double opt-in. Public submit, hosted-page, and confirm routes are unauthenticated; the CRUD below requires a key.
Create a form
Create a signup form bound to a list. Returns the generated slug, public URL, and embed code.
curl https://api.sendara.dev/v1/forms \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter signup",
"list_id": "list_9f21",
"double_opt_in": true,
"fields": { "first_name": { "enabled": true, "required": false } }
}'{
"id": "form_a1b2c3",
"slug": "newsletter-signup",
"name": "Newsletter signup",
"list_id": "list_9f21",
"double_opt_in": true,
"fields": { "first_name": { "enabled": true, "required": false },
"last_name": { "enabled": false, "required": false },
"phone": { "enabled": false, "required": false } },
"success_message": "Thanks for subscribing!",
"redirect_url": "",
"settings": { "heading": "", "description": "", "button_text": "", "branding": true },
"status": "active",
"public_url": "https://api.sendara.dev/v1/public/f/newsletter-signup",
"embed_code": "<iframe src=\"https://api.sendara.dev/v1/public/f/newsletter-signup?embed=1\"></iframe>",
"submission_count": 0,
"created_at": "2026-06-14T10:00:00Z",
"updated_at": "2026-06-14T10:00:00Z"
}List forms
Return the account's signup forms.
{
"forms": [
{ "id": "form_a1b2c3", "slug": "newsletter-signup", "name": "Newsletter signup",
"status": "active", "submission_count": 128, "created_at": "2026-06-14T10:00:00Z" }
]
}Get a form
Retrieve a single form with its full config.
{ "id": "form_a1b2c3", "slug": "newsletter-signup", "name": "Newsletter signup",
"list_id": "list_9f21", "status": "active", "submission_count": 128 }Update a form
Update any form field. Set status to active or inactive to publish or unpublish.
{ "id": "form_a1b2c3", "status": "inactive" }Delete a form
Permanently delete a form.
# 204 No ContentGet public form config
Unauthenticated. Return the render-only config for a form — deliberately omits account, list, and submission internals.
{
"name": "Newsletter signup",
"heading": "Join the list",
"description": "",
"fields": { "first_name": { "enabled": true, "required": false },
"last_name": { "enabled": false, "required": false },
"phone": { "enabled": false, "required": false } },
"button_text": "Subscribe",
"success_message": "Thanks for subscribing!",
"double_opt_in": true
}Submit a public form
Unauthenticated, honeypot-protected, IP rate-limited. Subscribe an email to the form's list. With double opt-in, returns pending until the contact confirms.
{ "status": "subscribed" }Automations
Drip sequences triggered by a list add, tag add, or form signup. An automation is inert until activated, then enrolls matching contacts and walks them through wait/email steps.
Create an automation
Create a drip automation. It starts in draft and sends nothing until you activate it.
curl https://api.sendara.dev/v1/automations \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome series",
"from_email": "[email protected]",
"trigger_type": "list_added",
"trigger_config": { "list_id": "list_9f21" },
"steps": [
{ "type": "email", "subject": "Welcome", "body_html": "<h1>Hi</h1>" },
{ "type": "wait", "hours": 24 },
{ "type": "email", "subject": "Day two", "body_html": "<p>More</p>" }
]
}'{
"id": "auto_a1b2c3",
"name": "Welcome series",
"trigger_type": "list_added",
"trigger_config": { "list_id": "list_9f21" },
"from_email": "[email protected]",
"status": "draft",
"steps": [
{ "type": "email", "subject": "Welcome", "body_html": "<h1>Hi</h1>" },
{ "type": "wait", "hours": 24 }
],
"enrollment_count": 0,
"created_at": "2026-06-14T10:00:00Z"
}List automations
Return the account's automations.
{
"automations": [
{ "id": "auto_a1b2c3", "name": "Welcome series", "trigger_type": "list_added",
"status": "active", "enrollment_count": 412, "created_at": "2026-06-14T10:00:00Z" }
]
}Get an automation
Retrieve a single automation with its full step list.
{ "id": "auto_a1b2c3", "name": "Welcome series", "status": "active",
"enrollment_count": 412 }Update an automation
Update name, sender, trigger, or steps.
{ "id": "auto_a1b2c3", "name": "Welcome series v2" }Delete an automation
Permanently delete an automation.
# 204 No ContentActivate an automation
Move an automation to active so it starts enrolling matching contacts.
{ "id": "auto_a1b2c3", "status": "active" }Pause an automation
Pause an active automation. Enrolled contacts stop advancing until it is reactivated.
{ "id": "auto_a1b2c3", "status": "paused" }Analytics
Read-only marketing aggregates: an account-wide overview, a per-broadcast report, and per-contact engagement.
Get the overview
Account-wide totals, rates, a daily timeseries, deliverability health, and top campaigns.
{
"days": 30,
"totals": { "sent": 18420, "delivered": 18100, "opened": 6420,
"clicked": 1840, "bounced": 220, "complained": 8, "unsubscribed": 42 },
"rates": { "delivery_rate": 0.9826, "open_rate": 0.3547, "click_rate": 0.1017,
"click_to_open_rate": 0.2866, "bounce_rate": 0.0119,
"complaint_rate": 0.0004, "unsubscribe_rate": 0.0023 },
"timeseries": [ { "date": "2026-06-14", "sent": 640, "opened": 228, "clicked": 65 } ],
"deliverability": { "bounce_rate": 0.0119, "complaint_rate": 0.0004,
"health": "good", "notes": [] },
"top_campaigns": [
{ "id": "bc_a1b2c3", "name": "October newsletter", "sent_at": "2026-06-14T10:00:00Z",
"sent": 1840, "delivered": 1798, "opened": 642, "clicked": 190,
"open_rate": 0.3571, "click_rate": 0.1057 }
]
}Get a broadcast report
Totals, rates, a timeseries, and top clicked links for one broadcast.
{
"broadcast_id": "bc_a1b2c3",
"totals": { "sent": 1840, "delivered": 1798, "opened": 642, "clicked": 190 },
"rates": { "delivery_rate": 0.9772, "open_rate": 0.3571, "click_rate": 0.1057 },
"timeseries": [ { "date": "2026-06-14", "sent": 1840, "opened": 642, "clicked": 190 } ],
"top_links": [ { "url": "https://acme.com/launch", "clicks": 120, "unique_clicks": 98 } ]
}Get contact engagement
A single contact's lifetime engagement and computed score.
{
"contact_id": "ct_a1b2c3",
"sent": 24,
"opened": 18,
"clicked": 6,
"open_rate": 0.75,
"click_rate": 0.25,
"last_engaged_at": "2026-06-14T10:04:30Z",
"score": 82,
"status": "engaged"
}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. |