Reference
API reference
Base URL https://api.sendbaze.com/v1. JSON in, JSON out. Bearer authentication on every endpoint.
Request and response headers
| Field | Type | Description |
|---|---|---|
Authorizationrequired | request | Bearer sk_live_… or Bearer sk_test_…. |
Idempotency-Key | request | Up to 255 characters. Makes a POST safe to retry for 24 hours. |
x-request-id | response | Unique per request. Quote it when you contact support. |
x-ratelimit-limit | response | Requests per second allowed for your organisation. |
x-ratelimit-remaining | response | Requests left in the current second. |
Retry-After | response | Seconds to wait. Sent with 429 responses only. |
Idempotent-Replayed | response | true when the response was served from an earlier request with the same key. |
Emails
Send an email
POST/v1/emails
Validates the request, writes the message, queues it and returns the id. The email leaves through Amazon SES in the Africa (Cape Town) region within seconds. Requires the emails:send scope and a verified sending domain.
| Field | Type | Description |
|---|---|---|
fromrequired | string | A verified sender. orders@yourshop.co.za or Yourshop <orders@yourshop.co.za>. |
torequired | string | string[] | One address or up to 50. |
cc | string | string[] | Up to 50. |
bcc | string | string[] | Up to 50. |
replyTo | string | Where replies go if not the sender. |
subjectrequired | string | Up to 998 characters. |
html | string | HTML body, up to 2 MB. One of html, text or templateId is required. |
text | string | Plain-text body. Sent alongside html if both are set. |
templateId | string | A template from the dashboard. Cannot be combined with html or text. |
variables | object | String, number or boolean values for template placeholders. Requires templateId. |
headers | object | Custom headers. Standard headers such as From, To, Subject and DKIM-Signature cannot be overridden. |
tags | string[] | Up to 10 tags of 64 characters, returned on events and searchable in the logs. |
class | "transactional" | "marketing" | Defaults to transactional. Marketing mail gets List-Unsubscribe headers and requires a lawful basis for each recipient. |
attachments | object[] | Coming in the private beta. Requests with attachments are rejected until then. |
curl https://api.sendbaze.com/v1/emails \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{
"from": "Yourshop <orders@yourshop.co.za>",
"to": ["thandi@example.com"],
"replyTo": "help@yourshop.co.za",
"subject": "Your order #1042 has shipped",
"html": "<p>Tracking: <b>CG123456789ZA</b></p>",
"text": "Tracking: CG123456789ZA",
"tags": ["order-shipped"]
}'
# 200 OK
# { "id": "em_01j9x7k2r4m8q3v5n6b7c8d9e0", "status": "queued" }Send a batch of emails
POST/v1/emails/batch
Up to 1 000 emails in one request. Each item is validated and queued on its own, so a bad item fails alone. Results come back in input order. Idempotency-Key is not applied to individual items; use tags to reconcile.
curl https://api.sendbaze.com/v1/emails/batch \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"emails": [
{ "from": "orders@yourshop.co.za", "to": "a@example.com", "subject": "Hi", "text": "..." },
{ "from": "orders@yourshop.co.za", "to": "not-an-email", "subject": "Hi", "text": "..." }
]
}'
# 200 OK
# {
# "results": [
# { "index": 0, "ok": true, "data": { "id": "em_01j9...", "status": "queued" } },
# { "index": 1, "ok": false, "error": { "code": "validation_error", "message": "...",
# "fields": { "to": ["must be a valid email address"] } } }
# ]
# }Retrieve an email
GET/v1/emails/{id}
Returns the message object with its full event timeline. Requires the read scope.
Domains
Live keys may only send from domains your organisation has verified. Add a domain, publish the DNS records the response lists at your registrar, and the status moves to verified once Amazon SES confirms the DKIM and MAIL FROM records (usually minutes, at most 72 hours). Sending from a subdomain of a verified domain needs no extra set-up. Test keys skip this check because they never deliver.
Add a domain
POST/v1/domains
Creates the sending identity and returns the records to publish: three DKIM CNAMEs, an MX and an SPF TXT for the custom return-path host (mailFromSubdomain, default mail), and a suggested DMARC record. Each record carries our last DNS lookup in status and found, so a wrong value is visible rather than a silent pending. Adding a domain you already have returns it with 200. Requires domains:write.
curl https://api.sendbaze.com/v1/domains \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "name": "yourshop.co.za" }'
# 201 Created
# {
# "id": "dom_01j9x7k2r4m8q3v5n6b7c8d9e0",
# "name": "yourshop.co.za",
# "status": "pending",
# "mailFromDomain": "mail.yourshop.co.za",
# "records": [
# { "purpose": "dkim", "type": "CNAME", "name": "abc123._domainkey.yourshop.co.za",
# "value": "abc123.dkim.af-south-1.amazonses.com", "required": true, "status": "pending", "found": [] },
# ... two more DKIM CNAMEs ...
# { "purpose": "mail_from_mx", "type": "MX", "name": "mail.yourshop.co.za",
# "value": "10 feedback-smtp.af-south-1.amazonses.com", "required": true, "status": "pending", "found": [] },
# { "purpose": "mail_from_spf", "type": "TXT", "name": "mail.yourshop.co.za",
# "value": "v=spf1 include:amazonses.com ~all", "required": true, "status": "pending", "found": [] },
# { "purpose": "dmarc", "type": "TXT", "name": "_dmarc.yourshop.co.za",
# "value": "v=DMARC1; p=none; rua=mailto:dmarc@yourshop.co.za", "required": false, "status": "pending", "found": [] }
# ],
# "verifiedAt": null, "lastCheckedAt": "2026-09-23T18:40:12.000Z", "createdAt": "2026-09-23T18:40:11.000Z"
# }List and retrieve domains
GET/v1/domains
GET/v1/domains/{id}
The list is newest first under data. Statuses are re-checked every five minutes while pending and daily once verified. Requires read.
Check a domain now
POST/v1/domains/{id}/verify
Asks SES and public DNS immediately instead of waiting for the next poll, and returns the updated domain. Use it after publishing records. Requires domains:write.
Delete a domain
DELETE/v1/domains/{id}
Removes the identity. Sends from that domain fail with domain_not_verified from the next request. Returns { "id": "dom_…", "deleted": true }.
Suppressions
Addresses your organisation will not send to. A send whose to, cc or bcc includes one fails with 422 recipient_suppressed before anything is queued, so a bad address never costs you reputation twice. Entries arrive four ways: hard_bounce (the mailbox does not exist; applied for every sender on Sendbaze), complaint (the recipient marked your email as spam), unsubscribe (they used the List-Unsubscribe link on a marketing email; blocks marketing only, transactional still sends) and manual (you added it). Test keys skip the check.
class: "marketing") automatically carries List-Unsubscribe and one-click headers, so Gmail and Yahoo show an unsubscribe button. Put {{unsubscribe_url}} in your HTML or text to add a visible link too. If the complaint rate reaches 0.1% with at least 3 complaints in 24 hours, sending is paused (423 org_paused) and the owners are emailed.List suppressions
GET/v1/suppressions
Newest first under data, cursor-paged with nextCursor. Query parameters: email (substring match), limit (1–200, default 50), cursor. Requires read.
curl "https://api.sendbaze.com/v1/suppressions?limit=50" \
-H "Authorization: Bearer sk_live_..."
# 200 OK
# {
# "data": [
# {
# "id": "sup_01j9x7k2r4m8q3v5n6b7c8d9e0",
# "email": "gone@example.co.za",
# "reason": "hard_bounce",
# "messageId": "em_01j9x7k2r4m8q3v5n6b7c8d9e1",
# "note": "smtp; 550 5.1.1 user unknown",
# "createdAt": "2026-10-27T09:12:44.000Z"
# }
# ],
# "nextCursor": null
# }Add a suppression
POST/v1/suppressions
Body: email (required), reason (manual blocks all email, the default; unsubscribe blocks marketing only) and an optional note. Adding an address that is already listed returns the existing entry with 200. Requires emails:send.
curl https://api.sendbaze.com/v1/suppressions \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "email": "person@example.co.za", "reason": "manual", "note": "Asked to be removed" }'
# 201 Created (200 if it was already listed)Remove a suppression
DELETE/v1/suppressions/{id}
Removes one of your own entries so the address can receive email again. Returns { "id": "sup_…", "deleted": true }. Global hard-bounce entries are not yours to remove; contact support if a mailbox has come back to life. Requires emails:send.
SMS
Send an SMS
POST/v1/sms
Counts segments (GSM-7 or UCS-2), checks credits, applies the compliance rules for the message class, debits credits and queues the message. Credits are refunded automatically if the message never leaves our platform. Requires the sms:send scope.
| Field | Type | Description |
|---|---|---|
torequired | string | E.164, for example +27821234567. |
bodyrequired | string | Up to 1,600 characters or 10 segments. The response tells you how many segments it used. |
classrequired | "transactional" | "marketing" | Marketing messages need a consent record for the number, an opt-out footer, and are held to 08:00 to 20:00 SAST on weekdays and Saturdays. |
senderId | string | An approved alphanumeric sender of up to 11 characters, or a number you own. Defaults to your organisation's shared sender. |
scheduledAt | string | ISO 8601 timestamp up to 30 days ahead. The response status is "scheduled". |
tags | string[] | Up to 10 tags. |
curl https://api.sendbaze.com/v1/sms \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"to": "+27821234567",
"body": "Your OTP is 482 913. It expires in 5 minutes.",
"class": "transactional"
}'
# 200 OK
# { "id": "sm_01j9x7n4q6r8s0t2u4v6w8x0y2", "status": "queued", "segments": 1, "credits": 1 }scheduled.Send a batch of SMS
POST/v1/sms/batch
Same shape as the email batch with a messages array of up to 1 000 items. If credits run out part-way, the remaining items fail with insufficient_credits and the earlier ones still send.
Retrieve an SMS
GET/v1/sms/{id}
Returns the message object with segments, credits, scheduledAt and the event timeline.
Usage
GET/v1/usage
Your plan, the current billing period, how much of each allowance is used and remaining, prepaid SMS credits, and domains in use. Allowances reset at period.end. When an email allowance is used up, sends fail with plan_allowance_exceeded until the reset or an upgrade; SMS continues on prepaid credits once its allowance is gone. Test keys never count. Requires read.
curl https://api.sendbaze.com/v1/usage -H "Authorization: Bearer sk_live_..."
# 200 OK
# {
# "plan": { "id": "starter", "name": "Starter", "priceRand": 249 },
# "period": { "start": "2026-10-23T18:00:00.000Z", "end": "2026-11-23T18:00:00.000Z" },
# "emails": { "used": 4210, "included": 10000, "remaining": 5790 },
# "sms": { "used": 300, "included": 300, "remaining": 0, "credits": 1000 },
# "domains": { "used": 1, "included": 2 }
# }The message object
| Field | Type | Description |
|---|---|---|
id | string | em_… for email, sm_… for SMS. |
channel | "email" | "sms" | |
class | string | transactional or marketing. |
status | string | queued, sent, delivered, bounced, complained, failed, expired or rejected. See below. |
to | string | Recipient address or number. |
from | string | Sender address or sender ID. |
subject | string | null | Email only. |
segments | number | SMS only. |
credits | number | SMS only. Credits debited for this message. |
tags | string[] | |
error | { code, message } | null | Set once the message has failed, bounced or been rejected. |
createdAt | string | |
sentAt | string | null | |
deliveredAt | string | null | |
events | object[] | The full timeline, oldest first: { type, occurredAt, detail }. |
Statuses
queued: accepted and waiting for a worker. Usually under a second.sent: handed to Amazon SES or the mobile network.delivered: the receiving server or handset confirmed delivery.bouncedandcomplained: email only. Both suppress the address.failed: permanent failure after retries.errorsays why.expired: SMS only. The network could not deliver within its validity period.rejected: blocked before sending, for example a suppressed address or a missing consent record.
Idempotency
Send an Idempotency-Key with any POST. For 24 hours the same key and body return the original response with an Idempotent-Replayed: true header. The same key with a different body or on a different channel returns 409. If a request fails before the message is queued, the key is released so your retry goes through.
# First request
curl https://api.sendbaze.com/v1/sms -H "Idempotency-Key: otp-4821-1" ...
# 200 { "id": "sm_01j9...", ... }
# Same key, same body, within 24 hours
curl https://api.sendbaze.com/v1/sms -H "Idempotency-Key: otp-4821-1" ...
# 200 { "id": "sm_01j9...", ... } header: Idempotent-Replayed: true
# Same key, different body
# 409 { "error": { "code": "idempotency_conflict", ... } }Rate limits
100 requests per second per organisation, shared across all its keys. The batch endpoints count as one request each, so bulk sends should use them. Above the limit the API returns 429 with Retry-After.
Test mode
Keys that start with sk_test_ run every validation and compliance check and return an id, but never enqueue, never debit credits and never send. The send response carries "mode": "test" (live keys return "live"), test messages appear in the logs with a test badge, and GET lookups show status sent. Use them in CI and staging; to reach a real inbox, create a live key.
Errors
Every error shares one envelope with a stable code. See the errors reference.