SendAPI.email API
Send transactional email from your own domain, follow each email from queued to delivered, and keep your suppression list in sync.
https://sendapi.email/v1Bearer API keyJSONIntroduction
The SendAPI.email API sends transactional email. It takes and returns JSON, so send bodies with Content-Type: application/json. Every path starts with /v1.
Before you send
- Add your domain in the panel and add the DNS records it shows.
- Wait until the domain shows Verified.
- Create an API key on the API keys page.
https://sendapi.email
Authentication
Send your API key in the Authorization header on every request. Keys start with ae_live_.
The full key is shown once, when you create it; we keep only its first characters and a hash. A missing, wrong or revoked key gets 401 invalid_api_key.
Authorization: Bearer ae_live_...
Errors
Errors use HTTP status codes and return a JSON body with a code you can match on and a message you can show or log.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A field, header or query parameter is missing or wrong. The message names it. |
| 401 | invalid_api_key | The API key is missing, wrong or revoked. |
| 402 | out_of_credits | The account has fewer credits than the email has recipients. Buy more on the Billing page. |
| 403 | domain_not_found | The domain of from is not in your account. |
| 403 | domain_not_verified | The domain is in your account but isn't verified yet. |
| 404 | not_found | No email with this id, the address is not on the list, or no such path. |
| 409 | idempotency_conflict | This Idempotency-Key was already used with a different request. |
| 409 | cannot_remove | The address is on the list only for spam complaints or unsubscribes, which can't be removed. |
| 413 | payload_too_large | The request body is over 10 MB. |
| 500 | internal_error | Something failed on our side. Try again. |
| 502 | provider_error | Our email provider did not take the address off its own list, so it stays on ours. Try again. |
{
"error": {
"code": "invalid_request",
"message": "subject is required"
}
}
Idempotency
Send an Idempotency-Key header with POST /v1/emails to make retries safe. Use a value that is unique to the email, such as an order id, of 1 to 255 characters.
Keys are scoped to the API key. Repeating a request with the same key and the same body returns the same email's id and its current status, and sends nothing new. The same key with a different body gets 409 idempotency_conflict.
Idempotency-Key: order-1042-receipt
Send an email
/v1/emailsQueues an email and returns 202 with its id. Sending happens right after, in the background; follow it with Get an email.
Body parameters
fromstringRequired[email protected]orYour App <[email protected]>. The domain after@must be one you added in the panel that shows Verified.tostring or arrayRequired- 1 to 50 addresses.
subjectstringRequired- One line, at most 998 characters.
htmlstring- The HTML body. Send
html,textor both. textstring- The plain-text body.
reply_tostring or array- Up to 10 addresses.
attachmentsarray- Up to 10 files. Each has
filename(at most 255 characters),content(base64) and, optionally,content_typesuch asapplication/pdf. headersobject- Up to 10 custom headers, name to value, such as
{"List-Unsubscribe": "<https://yourapp.com/u/123>"}. Printable ASCII on one line; the value is 1 to 995 characters. From, To, Cc, Bcc, Subject, Reply-To, Date, Message-ID, MIME-Version, Content-Type, Content-Disposition, Return-Path andX-SES-headers can't be set. trackboolean- Track opens and clicks. Default true. Tracking works in HTML emails only.
- Each recipient uses one credit when the email is accepted. Credits for recipients that are never sent to, because they're suppressed or the email failed, come back to the account.
- Unknown fields are refused, so a typo can't be ignored.
- The request can be at most 10 MB, attachments included.
- Recipients on your suppression list are left out when the email is sent. If none is left, the status becomes
suppressed. - A temporary sending error is retried, 5 attempts in all, waiting 30 seconds, 2 minutes, 10 minutes, 30 minutes.
- The body, attachments and header values are deleted once the email is sent, suppressed or failed. Their names stay.
curl -X POST https://sendapi.email/v1/emails \ -H "Authorization: Bearer $SENDAPI_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-receipt" \ -d '{ "from": "Your App <[email protected]>", "to": [ "[email protected]" ], "subject": "Your receipt", "html": "<p>Thanks for your order.</p>", "text": "Thanks for your order." }'
{
"id": "em_01JCB3Z4K7R2M9Q8T5V6W3X1",
"status": "queued"
}
Get an email
/v1/emails/{id}Returns an email's status, counts and every event so far, oldest first. Only emails of your account are found; any other id gets 404 not_found.
Response fields
statusstring- See Statuses and events.
suppressed_toarray- Recipients left out because they are on the suppression list.
attachments, headersarray- The names only; the content is deleted after sending.
attemptsnumber- How many times sending was tried.
last_errorstring or null- The last sending error, if any.
opens, clicksnumber- Counted opens and clicks. Events flagged as likely bots, and a click on the same link within 60 seconds of a counted one, are in events but not counted.
eventsarray- Each has
type,atanddetail, the event's details as the email provider or the API recorded them.
curl https://sendapi.email/v1/emails/em_01JCB3Z4K7R2M9Q8T5V6W3X1 \ -H "Authorization: Bearer $SENDAPI_API_KEY"
{
"id": "em_01JCB3Z4K7R2M9Q8T5V6W3X1",
"status": "delivered",
"from": "Your App <[email protected]>",
"to": [
"[email protected]"
],
"suppressed_to": [],
"reply_to": [],
"subject": "Your receipt",
"track": true,
"attachments": [],
"headers": [],
"attempts": 1,
"last_error": null,
"opens": 0,
"clicks": 0,
"created_at": "2026-11-09T10:00:00.120Z",
"sent_at": "2026-11-09T10:00:01.050Z",
"delivered_at": "2026-11-09T10:00:02.000Z",
"last_event_at": "2026-11-09T10:00:02.000Z",
"events": [
{
"type": "queued",
"at": "2026-11-09T10:00:00.120Z",
"detail": {
"api_key": "Production"
}
},
{
"type": "sent",
"at": "2026-11-09T10:00:01.050Z",
"detail": {
"provider_message_id": "0110019a2b3c4d5e-example-000000"
}
},
{
"type": "delivery",
"at": "2026-11-09T10:00:02.000Z",
"detail": {
"timestamp": "2026-11-09T10:00:02.000Z",
"smtpResponse": "250 2.6.0 Message received"
}
}
]
}
Statuses and events
Delivery events only move a status forward, so one that arrives late never takes an email back, for example from delivered to sent.
Statuses
| Status | Meaning |
|---|---|
| queued | Accepted and waiting to be sent. |
| sending | Being handed off for delivery. |
| sent | Accepted for delivery. |
| delayed | Delivery is delayed by a temporary problem. |
| delivered | The receiving server accepted it. |
| bounced | The receiving server refused it. |
| complained | The recipient marked it as spam. |
| rejected | Refused before delivery. |
| failed | Not sent: refused for good, 5 attempts failed, or the service stopped mid-send. last_error says which. |
| suppressed | Every recipient is on your suppression list, so nothing was sent. |
Event types
| Type | Meaning |
|---|---|
queued | The API accepted the email. |
sent | Accepted for delivery; detail has provider_message_id. |
retry | A temporary sending error; it will be tried again. |
failed | Sending stopped. |
suppressed | Recipients left out because they are on the suppression list. |
delivery | Delivered to the receiving server. |
delivery_delay | Delivery is delayed. |
bounce | Bounced. |
complaint | Marked as spam. |
reject | Refused before delivery. |
open | Opened (HTML emails with tracking on). |
click | A link was clicked (HTML emails with tracking on); detail has the link. |
rendering_failure | The email could not be rendered. |
subscription | A subscription event from the email provider. |
List suppression changes
/v1/suppressionsAn address that hard-bounces is not emailed again from any of your domains; one that marks an email as spam is not emailed again from the domain that sent it. You can also add addresses in the panel. This endpoint lists the changes to that list, oldest first, so your app can keep its own copy.
Start without since, then pass the next_since of the last response. An entry appears again whenever it changes, for example when it is removed.
Query parameters
sincestring- The
next_sinceof the previous response. reasonstring- Only this reason: "bounce", "complaint", "unsubscribe", "manual".
domainstring- Only entries for this domain, plus those for every domain.
limitnumber- 1 to 1000. Default 100.
Each entry
addressstring- The blocked address.
domainstring or null- The sending domain it is blocked for; null means every domain.
reasonstring- "bounce", "complaint", "unsubscribe", "manual"
email_idstring or null- The email that caused it, if any.
removed_at, removed_bystring or null- Set once the entry is removed; it no longer blocks.
has_more is true, ask again with the new next_since right away.curl "https://sendapi.email/v1/suppressions?limit=100" \ -H "Authorization: Bearer $SENDAPI_API_KEY"
{
"data": [
{
"address": "[email protected]",
"domain": null,
"reason": "bounce",
"email_id": "em_01JCB3Z4K7R2M9Q8T5V6W3X1",
"detail": {
"bounce_type": "Permanent",
"bounce_subtype": "General",
"diagnostic_code": "smtp; 550 5.1.1 user unknown"
},
"created_by": null,
"created_at": "2026-11-09T10:00:03.000Z",
"removed_at": null,
"removed_by": null
}
],
"next_since": "42",
"has_more": false
}
Remove a suppression
/v1/suppressions/{address}Lets an address receive email again. Only "bounce", "manual" entries can be removed. Spam complaints and unsubscribes stay and are listed in kept; if the address has only those, you get 409 cannot_remove.
URL-encode the address. A bounce for every domain is also taken off our email provider's own list; if the provider refuses, the address stays on your list and you get 502 provider_error.
curl -X DELETE https://sendapi.email/v1/suppressions/[email protected] \ -H "Authorization: Bearer $SENDAPI_API_KEY"
{
"address": "[email protected]",
"removed": [
{
"address": "[email protected]",
"domain": null,
"reason": "bounce",
"email_id": "em_01JCB3Z4K7R2M9Q8T5V6W3X1",
"detail": {
"bounce_type": "Permanent",
"bounce_subtype": "General",
"diagnostic_code": "smtp; 550 5.1.1 user unknown"
},
"created_by": null,
"created_at": "2026-11-09T10:00:03.000Z",
"removed_at": "2026-11-09T12:00:00.000Z",
"removed_by": "API key Production"
}
],
"kept": []
}