API reference
Base URL https://api.avelto.dev. Every request needs an Authorization header with a bearer key, av_live_… or av_test_…. Bodies and responses are JSON with snake_case fields. Field tables on this page are generated from the same schemas the API validates with.
Authentication
Create keys in the dashboard. The prefix tells you the mode: av_live_ keys send real email, av_test_ keys run the pipeline without sending.
Scopes
A key carries a list of scopes, and a route it does not have one for answers 403 naming the scope it needed. Keys created before scopes existed hold the 7 standard ones, and a key created without naming any still does: that is what the dashboard calls Full access. Narrowing is the deliberate act, not the default.
| Scope | Covers |
|---|---|
emails:send | Send email |
emails:read | Read the email log |
domains:manage | Add and remove domains |
webhooks:manage | Manage webhook endpoints |
suppressions:manage | Change the suppression list |
templates:manage | Create and edit templates |
account:manage | Export the account's data and erase recipients |
account:settings | Change the account name and alert settings. Owner only. |
keys:manage | Create and revoke API keys, never with more access than this one. Owner only. |
Two scopes are different. account:settings and keys:manage let a key change the account’s name and alert settings and create and revoke other keys: what an AI agent setting the account up needs, and what the dashboard’s Key for an AI agent holds. Only the account owner can grant them, in the dashboard and by asking for them by name. They are never part of Full access or of a key created without a list, and a key created through the API keys routes never holds them. See Let an AI agent set up your account.
The split worth knowing is emails:send against emails:read. A key that can send and cannot read the log back is the one to put in an application server, and the dashboard offers it as a preset.
The mode is a boundary on reads as well as sends. A test key sees test emails and their events and deliveries; a live email is a 404, and ?mode=live does not change that. It also gets a 403 live_key_required on the calls that live sending depends on: removing a domain, creating, changing or deleting a template, creating, changing or deleting a webhook endpoint, reading or changing the suppression list, changing the account settings, exporting the account and erasing a recipient. It can still add and read domains, read templates and endpoints, and send with a template in test mode, and a test key with keys:manage creates test keys only. Idempotency keys are kept per mode. A live key reads both modes. See Test mode.
curl https://api.avelto.dev/v1/emails \ -H "Authorization: Bearer av_live_..."Pagination
Lists take limit (1 to 100, default 20) and cursor, and return { data, next_cursor }. Pass next_cursor back as cursor; it is null on the last page.
Rate limits
Between 10 and 50 requests a second per key depending on plan (see Plans and limits). Over that, the API returns 429 rate_limited with a Retry-After header. New accounts can send 100 emails a day for their first 7 days, each recipient across to, cc and bcc counting as one.
Emails
Send, fetch, list and cancel emails. The key's mode decides the mode of the emails it creates, and also what it can read: a test key sees test emails only, and a live email is a 404 to it.
/v1/emailsSend an emailSend an Idempotency-Key header to make retries safe. A replay returns the existing id with status 200. Keys are kept per mode, so a test send never replays a live one, and keys starting with batch: are reserved for batches.
Provide html, text or both. At most 50 recipients across to, cc and bcc.
Errors: 403 domain_not_verified, 403 sandbox_recipient_not_allowed, 403 account_paused, 422 recipient_suppressed (live sends only), 429 plan_limit, 429 rate_limited.
Request body
| Field | Type | Description |
|---|---|---|
fromrequired | string | Sender. a@b.com or Name <a@b.com>. The domain must be verified on your account, or be your sandbox domain. |
torequired | string | string[] | One recipient or a list. At most 50 recipients across to, cc and bcc. |
ccoptional | string | string[] | Carbon-copy recipients. |
bccoptional | string | string[] | Blind-carbon-copy recipients. |
reply_tooptional | string | Address replies go to. |
subjectoptional | string | Subject line. Line breaks are rejected. |
htmloptional | string | HTML body. Provide html, text or both. |
textoptional | string | Plain-text body. Provide html, text or both. |
headersoptional | object<string, string> | Extra headers. Names reserved for the envelope, MIME structure or signing are rejected. |
tagsoptional | string[] | Up to 10 tags of letters, numbers, _ - : . for filtering and reporting. |
attachmentsoptional | object[] | Up to 10 files, 7 MB in total after decoding. |
attachments[].filenamerequired | string | File name shown to the recipient. No quotes or slashes. |
attachments[].contentoptional | string | File contents, base64-encoded. |
attachments[].urloptional | string (url) | Endpoint to POST events to. Must be https in production. |
attachments[].content_typeoptional | string | MIME type, for example application/pdf. |
template_idoptional | string (uuid) | |
template_slugoptional | string | |
variablesoptional | object<string, string | number | boolean | null> | |
scheduled_atoptional | string (ISO 8601) | Send at this time instead of now. Must be in the future and within 30 days. |
unsubscribe_urloptional | string (url) |
Response 201
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
/v1/emails/batchSend up to 100 emailsEach message is validated, limited and queued exactly as a single send is. results[i] lines up with messages[i].
200 when every message was accepted, 422 when none was, 207 when it was mixed.
An Idempotency-Key makes the whole batch replayable: each message is stored with batch:<key>:<index>, a prefix a single send's key may not use. Each message counts separately against your plan and rate limit.
Request body
| Field | Type | Description |
|---|---|---|
messagesrequired | object[] | |
messages[].fromrequired | string | Sender. a@b.com or Name <a@b.com>. The domain must be verified on your account, or be your sandbox domain. |
messages[].torequired | string | string[] | One recipient or a list. At most 50 recipients across to, cc and bcc. |
messages[].ccoptional | string | string[] | Carbon-copy recipients. |
messages[].bccoptional | string | string[] | Blind-carbon-copy recipients. |
messages[].reply_tooptional | string | Address replies go to. |
messages[].subjectoptional | string | Subject line. Line breaks are rejected. |
messages[].htmloptional | string | HTML body. Provide html, text or both. |
messages[].textoptional | string | Plain-text body. Provide html, text or both. |
messages[].headersoptional | object<string, string> | Extra headers. Names reserved for the envelope, MIME structure or signing are rejected. |
messages[].tagsoptional | string[] | Up to 10 tags of letters, numbers, _ - : . for filtering and reporting. |
messages[].attachmentsoptional | object[] | Up to 10 files, 7 MB in total after decoding. |
messages[].attachments[].filenamerequired | string | File name shown to the recipient. No quotes or slashes. |
messages[].attachments[].contentoptional | string | File contents, base64-encoded. |
messages[].attachments[].urloptional | string (url) | Endpoint to POST events to. Must be https in production. |
messages[].attachments[].content_typeoptional | string | MIME type, for example application/pdf. |
messages[].template_idoptional | string (uuid) | |
messages[].template_slugoptional | string | |
messages[].variablesoptional | object<string, string | number | boolean | null> | |
messages[].scheduled_atoptional | string (ISO 8601) | Send at this time instead of now. Must be in the future and within 30 days. |
messages[].unsubscribe_urloptional | string (url) |
Response 200
| Field | Type | Description |
|---|---|---|
results | discriminatedunion[] | |
accepted | integer | |
failed | integer |
/v1/emailsList emailsNewest first. Pass next_cursor back as cursor for the next page.
Query
| Field | Type | Description |
|---|---|---|
limitoptional | integer | Page size, 1 to 100. Defaults to 20. |
cursoroptional | string | Cursor from a previous page. |
statusoptional | "queued" | "scheduled" | "sent" | "delivered" | "bounced" | "complained" | "failed" | "cancelled" | Only emails in this status. |
tagoptional | string | Only emails carrying this tag. |
modeoptional | "live" | "test" | Filter by mode. A live key defaults to live and may ask for test; a test key always gets test, whatever this says. |
qoptional | string |
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].id | string (uuid) | Unique id. |
data[].mode | "live" | "test" | live or test, taken from the API key that created the email. |
data[].from | string | Sender. a@b.com or Name <a@b.com>. The domain must be verified on your account, or be your sandbox domain. |
data[].to | string[] | One recipient or a list. At most 50 recipients across to, cc and bcc. |
data[].cc | string[] | Carbon-copy recipients. |
data[].bcc | string[] | Blind-carbon-copy recipients. |
data[].reply_to | string | null | Address replies go to. |
data[].subject | string | Subject line. Line breaks are rejected. |
data[].tags | string[] | Up to 10 tags of letters, numbers, _ - : . for filtering and reporting. |
data[].status | "queued" | "scheduled" | "sent" | "delivered" | "bounced" | "complained" | "failed" | "cancelled" | Current state. |
data[].scheduled_at | string (ISO 8601) | null | Send at this time instead of now. Must be in the future and within 30 days. |
data[].created_at | string (ISO 8601) | When the record was created. |
data[].updated_at | string (ISO 8601) | When the record last changed. |
data[].match | object | null | |
data[].match.field | "recipient" | "subject" | "message_id" | "email_id" | |
data[].match.value | string | Record value. |
next_cursor | string | null | Pass as cursor to fetch the next page. Null on the last page. |
/v1/emails/:idGet an emailIncludes the bodies, headers, attachment metadata and every event so far.
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
mode | "live" | "test" | live or test, taken from the API key that created the email. |
from | string | Sender. a@b.com or Name <a@b.com>. The domain must be verified on your account, or be your sandbox domain. |
to | string[] | One recipient or a list. At most 50 recipients across to, cc and bcc. |
cc | string[] | Carbon-copy recipients. |
bcc | string[] | Blind-carbon-copy recipients. |
reply_to | string | null | Address replies go to. |
subject | string | Subject line. Line breaks are rejected. |
tags | string[] | Up to 10 tags of letters, numbers, _ - : . for filtering and reporting. |
status | "queued" | "scheduled" | "sent" | "delivered" | "bounced" | "complained" | "failed" | "cancelled" | Current state. |
scheduled_at | string (ISO 8601) | null | Send at this time instead of now. Must be in the future and within 30 days. |
created_at | string (ISO 8601) | When the record was created. |
updated_at | string (ISO 8601) | When the record last changed. |
match | object | null | |
match.field | "recipient" | "subject" | "message_id" | "email_id" | |
match.value | string | Record value. |
html | string | null | HTML body. Provide html, text or both. |
text | string | null | Plain-text body. Provide html, text or both. |
headers | object<string, string> | Extra headers. Names reserved for the envelope, MIME structure or signing are rejected. |
unsubscribe_url | string | null | |
idempotency_key | string | null | |
source | "api" | "smtp" | |
attachments | object[] | Up to 10 files, 7 MB in total after decoding. |
attachments[].filename | string | File name shown to the recipient. No quotes or slashes. |
attachments[].content_type | string | MIME type, for example application/pdf. |
attachments[].size | integer | Decoded size in bytes. |
domain_id | string (uuid) | null | The verified domain the email was sent from, or null for the sandbox domain. |
ses_message_id | string | null | Provider message id once the email has been handed off. |
error | string | null | Failure reason, when status is failed. |
events | object[] | Every state change for this email, oldest first. |
events[].id | string (uuid) | Unique id. |
events[].type | "email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled" | Event type. |
events[].payload | object<string, any> | Event-specific details. |
events[].occurred_at | string (ISO 8601) | When the event happened. |
/v1/emails/:id/cancelCancel a scheduled email409 not_scheduled unless the email's status is scheduled.
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
status | "cancelled" | Current state. |
/v1/emails/validateValidate an address before sendingNothing is sent or stored, so call it from a signup form on every submit. Works with a test key; needs the emails:send scope.
checks.suppressed is only looked up for a live key. A test key always gets false, which is also what a test send would see: test sends are not checked against the suppression list.
undeliverable means a send would fail or be refused; risky means it may deliver but deserves a look, and suggestion carries a corrected address when the domain looks mistyped.
It cannot tell you whether the mailbox exists. The large providers accept every address and bounce later, so nobody can without sending.
Request body
| Field | Type | Description |
|---|---|---|
emailrequired | string |
Response 200
| Field | Type | Description |
|---|---|---|
email | string | |
normalized | string | null | |
result | "deliverable" | "risky" | "undeliverable" | |
reason | "invalid_syntax" | "suppressed" | "no_mail_server" | "disposable" | "possible_typo" | "role_address" | null | Why the address was suppressed. |
checks | object | |
checks.syntax | boolean | |
checks.mail_server | boolean | null | |
checks.disposable | boolean | |
checks.role | boolean | |
checks.free_provider | boolean | |
checks.suppressed | boolean | |
suggestion | string | null |
Domains
Sending domains and the DNS records that verify them.
/v1/domainsAdd a domainReturns the ownership TXT record to publish first; the DKIM records follow once it is found. A domain whose ownership is not proven within 7 days is removed.
409 conflict if the domain is already added. 409 domain_unavailable if it is part of a domain on another account, is reserved, or is already set up elsewhere. 403 plan_limit when your plan's domain limit is reached.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string | Domain name, for example mail.example.com. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | Domain name, for example mail.example.com. |
region | string | Region the domain identity lives in. |
status | "pending" | "verified" | "failed" | Current state. |
ownership_verified | boolean | Whether the ownership TXT record has been found. A domain still unproven 7 days after it was added is removed. |
dns_records | object[] | Records to publish before the domain can verify. The ownership TXT record comes first; the DKIM records appear once it is found. |
dns_records[].type | "CNAME" | "TXT" | "MX" | Event type. |
dns_records[].name | string | Domain name, for example mail.example.com. |
dns_records[].value | string | Record value. |
dns_records[].purpose | "ownership" | "dkim" | "spf" | "dmarc" | What the record is for. |
health | "unknown" | "healthy" | "action_needed" | |
health_changed_at | string (ISO 8601) | null | |
dns_checks | object[] | null | |
dns_checks[].type | "CNAME" | "TXT" | "MX" | Event type. |
dns_checks[].name | string | Domain name, for example mail.example.com. |
dns_checks[].value | string | Record value. |
dns_checks[].purpose | "ownership" | "dkim" | "spf" | "dmarc" | What the record is for. |
dns_checks[].id | string | Unique id. |
dns_checks[].state | "found" | "missing" | "mismatch" | |
dns_checks[].found | string | null | |
dns_checked_at | string (ISO 8601) | null | |
created_at | string (ISO 8601) | When the record was created. |
/v1/domainsList domainsResponse 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].id | string (uuid) | Unique id. |
data[].name | string | Domain name, for example mail.example.com. |
data[].region | string | Region the domain identity lives in. |
data[].status | "pending" | "verified" | "failed" | Current state. |
data[].ownership_verified | boolean | Whether the ownership TXT record has been found. A domain still unproven 7 days after it was added is removed. |
data[].dns_records | object[] | Records to publish before the domain can verify. The ownership TXT record comes first; the DKIM records appear once it is found. |
data[].dns_records[].type | "CNAME" | "TXT" | "MX" | Event type. |
data[].dns_records[].name | string | Domain name, for example mail.example.com. |
data[].dns_records[].value | string | Record value. |
data[].dns_records[].purpose | "ownership" | "dkim" | "spf" | "dmarc" | What the record is for. |
data[].health | "unknown" | "healthy" | "action_needed" | |
data[].health_changed_at | string (ISO 8601) | null | |
data[].dns_checks | object[] | null | |
data[].dns_checks[].type | "CNAME" | "TXT" | "MX" | Event type. |
data[].dns_checks[].name | string | Domain name, for example mail.example.com. |
data[].dns_checks[].value | string | Record value. |
data[].dns_checks[].purpose | "ownership" | "dkim" | "spf" | "dmarc" | What the record is for. |
data[].dns_checks[].id | string | Unique id. |
data[].dns_checks[].state | "found" | "missing" | "mismatch" | |
data[].dns_checks[].found | string | null | |
data[].dns_checked_at | string (ISO 8601) | null | |
data[].created_at | string (ISO 8601) | When the record was created. |
/v1/domains/:idGet a domainRe-checks verification on every call while the domain is pending: the ownership record first, then DKIM, and updates ownership_verified, dns_records and status.
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | Domain name, for example mail.example.com. |
region | string | Region the domain identity lives in. |
status | "pending" | "verified" | "failed" | Current state. |
ownership_verified | boolean | Whether the ownership TXT record has been found. A domain still unproven 7 days after it was added is removed. |
dns_records | object[] | Records to publish before the domain can verify. The ownership TXT record comes first; the DKIM records appear once it is found. |
dns_records[].type | "CNAME" | "TXT" | "MX" | Event type. |
dns_records[].name | string | Domain name, for example mail.example.com. |
dns_records[].value | string | Record value. |
dns_records[].purpose | "ownership" | "dkim" | "spf" | "dmarc" | What the record is for. |
health | "unknown" | "healthy" | "action_needed" | |
health_changed_at | string (ISO 8601) | null | |
dns_checks | object[] | null | |
dns_checks[].type | "CNAME" | "TXT" | "MX" | Event type. |
dns_checks[].name | string | Domain name, for example mail.example.com. |
dns_checks[].value | string | Record value. |
dns_checks[].purpose | "ownership" | "dkim" | "spf" | "dmarc" | What the record is for. |
dns_checks[].id | string | Unique id. |
dns_checks[].state | "found" | "missing" | "mismatch" | |
dns_checks[].found | string | null | |
dns_checked_at | string (ISO 8601) | null | |
created_at | string (ISO 8601) | When the record was created. |
/v1/domains/:idRemove a domainNeeds a live key.
Response 204
Empty body.
Templates
A stored subject and body with {{variables}} in them. Send one by passing template_id or template_slug instead of a body. Needs the templates:manage scope. A live send renders the template as it is now, so creating, changing, deleting and restoring need a live key; a test key can read templates and send them in test mode.
/v1/templatesCreate a templateThe slug is derived from the name unless you set one. 409 conflict if a slug you chose is taken.
At most 100 templates per account; past that, 403 plan_limit. Each keeps its last 50 versions.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string | Domain name, for example mail.example.com. |
subjectrequired | string | Subject line. Line breaks are rejected. |
htmloptional | string | HTML body. Provide html, text or both. |
textoptional | string | Plain-text body. Provide html, text or both. |
slugoptional | string |
Response 201
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | Domain name, for example mail.example.com. |
slug | string | |
subject | string | Subject line. Line breaks are rejected. |
html | string | null | HTML body. Provide html, text or both. |
text | string | null | Plain-text body. Provide html, text or both. |
variables | string[] | |
version | integer | |
created_at | string (ISO 8601) | When the record was created. |
updated_at | string (ISO 8601) | When the record last changed. |
/v1/templatesList templatesResponse 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].id | string (uuid) | Unique id. |
data[].name | string | Domain name, for example mail.example.com. |
data[].slug | string | |
data[].subject | string | Subject line. Line breaks are rejected. |
data[].html | string | null | HTML body. Provide html, text or both. |
data[].text | string | null | Plain-text body. Provide html, text or both. |
data[].variables | string[] | |
data[].version | integer | |
data[].created_at | string (ISO 8601) | When the record was created. |
data[].updated_at | string (ISO 8601) | When the record last changed. |
/v1/templates/:idGet a templateResponse 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | Domain name, for example mail.example.com. |
slug | string | |
subject | string | Subject line. Line breaks are rejected. |
html | string | null | HTML body. Provide html, text or both. |
text | string | null | Plain-text body. Provide html, text or both. |
variables | string[] | |
version | integer | |
created_at | string (ISO 8601) | When the record was created. |
updated_at | string (ISO 8601) | When the record last changed. |
/v1/templates/:idUpdate a templateOnly the fields you pass change. null clears a body part. Every save keeps a version.
Request body
| Field | Type | Description |
|---|---|---|
nameoptional | string | Domain name, for example mail.example.com. |
slugoptional | string | |
subjectoptional | string | Subject line. Line breaks are rejected. |
htmloptional | string | null | HTML body. Provide html, text or both. |
textoptional | string | null | Plain-text body. Provide html, text or both. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | Domain name, for example mail.example.com. |
slug | string | |
subject | string | Subject line. Line breaks are rejected. |
html | string | null | HTML body. Provide html, text or both. |
text | string | null | Plain-text body. Provide html, text or both. |
variables | string[] | |
version | integer | |
created_at | string (ISO 8601) | When the record was created. |
updated_at | string (ISO 8601) | When the record last changed. |
/v1/templates/:idDelete a templateResponse 204
Empty body.
/v1/templates/:id/versionsList versionsNewest first. Every save is kept.
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].version | integer | |
data[].name | string | Domain name, for example mail.example.com. |
data[].subject | string | Subject line. Line breaks are rejected. |
data[].html | string | null | HTML body. Provide html, text or both. |
data[].text | string | null | Plain-text body. Provide html, text or both. |
data[].created_by | string | null | |
data[].created_at | string (ISO 8601) | When the record was created. |
/v1/templates/:id/restoreRestore a versionWrites the old version forward as a new one, so the history stays append-only.
Request body
| Field | Type | Description |
|---|---|---|
versionrequired | integer |
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | Domain name, for example mail.example.com. |
slug | string | |
subject | string | Subject line. Line breaks are rejected. |
html | string | null | HTML body. Provide html, text or both. |
text | string | null | Plain-text body. Provide html, text or both. |
variables | string[] | |
version | integer | |
created_at | string (ISO 8601) | When the record was created. |
updated_at | string (ISO 8601) | When the record last changed. |
Suppressions
Addresses this account will not send live mail to. Every entry is a real recipient, so all three routes need a live key.
/v1/suppressionsList suppressionsQuery
| Field | Type | Description |
|---|---|---|
limitoptional | integer | Page size, 1 to 100. Defaults to 20. |
cursoroptional | string | Cursor from a previous page. |
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].id | string (uuid) | Unique id. |
data[].email_address | string | null | The suppressed address. |
data[].reason | "bounce" | "complaint" | "manual" | Why the address was suppressed. |
data[].created_at | string (ISO 8601) | When the record was created. |
next_cursor | string | null | Pass as cursor to fetch the next page. Null on the last page. |
/v1/suppressionsAdd a suppressionStored with reason manual. 409 conflict if already suppressed.
Request body
| Field | Type | Description |
|---|---|---|
email_addressrequired | string (email) | The suppressed address. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
email_address | string | null | The suppressed address. |
reason | "bounce" | "complaint" | "manual" | Why the address was suppressed. |
created_at | string (ISO 8601) | When the record was created. |
/v1/suppressions/:emailRemove a suppressionURL-encode the address: @ becomes %40.
Response 204
Empty body.
Webhooks
Endpoints that receive signed event POSTs, and the log of every delivery attempt.
/v1/webhooksCreate an endpointNeeds a live key: an endpoint receives live events as well as test ones.
The secret is returned only here. In production the URL must be https and public.
At most 10 endpoints per account; past that, 403 plan_limit.
Request body
| Field | Type | Description |
|---|---|---|
urlrequired | string (url) | Endpoint to POST events to. Must be https in production. |
eventsoptional | ("email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled")[] | Event types to deliver. Defaults to all eight: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.rejected, email.failed and email.cancelled. Defaults to ["email.sent","email.delivered","email.delivery_delayed","email.bounced","email.complained","email.rejected","email.failed","email.cancelled"]. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
url | string | Endpoint to POST events to. Must be https in production. |
events | ("email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled")[] | Event types to deliver. Defaults to all eight: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.rejected, email.failed and email.cancelled. |
enabled | boolean | Whether deliveries are being attempted. |
created_at | string (ISO 8601) | When the record was created. |
secret | string | Signing secret. Returned once, at creation. |
/v1/webhooksList endpointsResponse 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].id | string (uuid) | Unique id. |
data[].url | string | Endpoint to POST events to. Must be https in production. |
data[].events | ("email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled")[] | Every state change for this email, oldest first. |
data[].enabled | boolean | Whether deliveries are being attempted. |
data[].created_at | string (ISO 8601) | When the record was created. |
/v1/webhooks/:idGet an endpointResponse 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
url | string | Endpoint to POST events to. Must be https in production. |
events | ("email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled")[] | Every state change for this email, oldest first. |
enabled | boolean | Whether deliveries are being attempted. |
created_at | string (ISO 8601) | When the record was created. |
/v1/webhooks/:idChange an endpointNeeds a live key. Pass events, enabled or both; anything you leave out stays as it is.
The URL cannot be changed: a new URL is a new endpoint, with its own secret and history. Create one and delete the old one.
Request body
| Field | Type | Description |
|---|---|---|
eventsoptional | ("email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled")[] | Event types to deliver from now on. Replaces the list. |
enabledoptional | boolean | false stops deliveries to the endpoint; true starts them again. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
url | string | Endpoint to POST events to. Must be https in production. |
events | ("email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled")[] | Event types to deliver from now on. Replaces the list. |
enabled | boolean | false stops deliveries to the endpoint; true starts them again. |
created_at | string (ISO 8601) | When the record was created. |
/v1/webhooks/:idDelete an endpointNeeds a live key.
Response 204
Empty body.
/v1/webhooks/:id/deliveriesList deliveriesQuery
| Field | Type | Description |
|---|---|---|
limitoptional | integer | Page size, 1 to 100. Defaults to 20. |
cursoroptional | string | Cursor from a previous page. |
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | Items on this page. |
data[].id | string (uuid) | Unique id. |
data[].endpoint_id | string (uuid) | The webhook endpoint this delivery belongs to. |
data[].email_event_id | string (uuid) | null | The event being delivered. |
data[].event_type | "email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled" | Type of the event being delivered. |
data[].email_id | string (uuid) | null | The email the event belongs to. |
data[].status | "pending" | "delivered" | "failed" | Current state. |
data[].attempts | integer | Delivery attempts so far, up to 8. |
data[].last_error | string | null | Error from the most recent failed attempt. |
data[].response_code | integer | null | |
data[].duration_ms | integer | null | |
data[].is_test | boolean | |
data[].replay_of | string (uuid) | null | |
data[].created_at | string (ISO 8601) | When the record was created. |
next_cursor | string | null | Pass as cursor to fetch the next page. Null on the last page. |
/v1/webhooks/:id/deliveries/:deliveryId/retryRetry a deliveryQueues a failed delivery again and returns it.
Only a delivery whose status is failed: a delivered one, or one still being retried, gets 409 conflict.
At most 100 retries per endpoint per hour; past that, 429 rate_limited with retry-after.
Response 200
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
endpoint_id | string (uuid) | The webhook endpoint this delivery belongs to. |
email_event_id | string (uuid) | null | The event being delivered. |
event_type | "email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled" | Type of the event being delivered. |
email_id | string (uuid) | null | The email the event belongs to. |
status | "pending" | "delivered" | "failed" | Current state. |
attempts | integer | Delivery attempts so far, up to 8. |
last_error | string | null | Error from the most recent failed attempt. |
response_code | integer | null | |
duration_ms | integer | null | |
is_test | boolean | |
replay_of | string (uuid) | null | |
created_at | string (ISO 8601) | When the record was created. |
Account
The account as an integrator sees it: what the key can do, the sandbox and its allowed recipients, and what is set up.
/v1/accountDescribe the accountAny scope; API key only. Read this first when integrating: it says what exists and what the next step is.
Response 200
| Field | Type | Description |
|---|---|---|
account | object | |
account.id | string (uuid) | Unique id. |
account.name | string | Domain name, for example mail.example.com. |
account.plan | "free" | "pro" | "growth" | "enterprise" | |
account.plan_step | string | null | |
key | object | |
key.mode | "live" | "test" | live or test, taken from the API key that created the email. |
key.scopes | string[] | |
sandbox | object | |
sandbox.domain | string | |
sandbox.recipients | string[] | |
sandbox.simulator | object | |
sandbox.simulator.delivered | string | |
sandbox.simulator.bounced | string | |
sandbox.simulator.complained | string | |
domains | object[] | |
domains[].id | string (uuid) | Unique id. |
domains[].name | string | Domain name, for example mail.example.com. |
domains[].status | "pending" | "verified" | "failed" | Current state. |
webhooks | object[] | |
webhooks[].id | string (uuid) | Unique id. |
webhooks[].url | string | Endpoint to POST events to. Must be https in production. |
webhooks[].enabled | boolean | Whether deliveries are being attempted. |
templates | object[] | |
templates[].id | string (uuid) | Unique id. |
templates[].slug | string | |
templates[].name | string | Domain name, for example mail.example.com. |
docs_url | string |
/v1/account/settingsGet the account settingsNeeds account:settings, which only the account owner can grant (see Scopes).
Response 200
| Field | Type | Description |
|---|---|---|
name | string | The account's name. |
alerts | object | Where alert emails go and which alerts are on, as the dashboard's Alerts section shows them. |
alerts.notify_email | string | null | The address alerts go to, or null for the account owner. |
alerts.notify_email_verified | boolean | Whether that address has confirmed by the link sent to it. Until it has, alerts keep going to the owner. |
alerts.domain_health | boolean | |
alerts.bounce_rate | boolean | |
alerts.complaint_rate | boolean | |
alerts.usage_80 | boolean | |
alerts.api_key_created | boolean |
/v1/account/settingsChange the account settingsNeeds account:settings and a live key. Pass name, alerts or both.
alerts.api_key_created can be switched on but not off: that alert is how the owner hears about a key they did not make, so only the owner can turn it off, in the dashboard. Either refusal is a 403 forbidden and changes nothing. Plans, payment, the team and sign-in are not settings this route can change.
Request body
| Field | Type | Description |
|---|---|---|
nameoptional | string | A new name for the account. |
alertsoptional | object | Alert settings to change. Anything you leave out stays as it is. |
alerts.notify_emailoptional | string (email) | null | Where alerts go: null for the account owner, or the address of someone who signs in to the account. An API key cannot send the alerts to an outside address; the owner can, in the dashboard. |
alerts.domain_healthoptional | boolean | |
alerts.bounce_rateoptional | boolean | |
alerts.complaint_rateoptional | boolean | |
alerts.usage_80optional | boolean | |
alerts.api_key_createdoptional | boolean |
Response 200
| Field | Type | Description |
|---|---|---|
name | string | A new name for the account. |
alerts | object | Alert settings to change. Anything you leave out stays as it is. |
alerts.notify_email | string | null | Where alerts go: null for the account owner, or the address of someone who signs in to the account. An API key cannot send the alerts to an outside address; the owner can, in the dashboard. |
alerts.notify_email_verified | boolean | |
alerts.domain_health | boolean | |
alerts.bounce_rate | boolean | |
alerts.complaint_rate | boolean | |
alerts.usage_80 | boolean | |
alerts.api_key_created | boolean |
API keys
Keys that a key with `keys:manage` lists, creates and revokes: how an AI agent setting the account up hands your application a narrower key of its own, such as a send-only key for the production server. Never more than the key that asks: a key made here holds a subset of the caller's scopes, never `account:settings` and `keys:manage`.
/v1/api-keysList API keysNeeds keys:manage. Never returns a key itself, only its prefix. A test key sees only test keys.
Response 200
| Field | Type | Description |
|---|---|---|
data | object[] | The account's keys. |
data[].id | string (uuid) | Unique id. |
data[].name | string | The key's name, for recognising it later. |
data[].mode | "live" | "test" | live or test. The prefix of the key says the same. |
data[].prefix | string | The first characters of the key, enough to recognise it. The whole key is never returned again. |
data[].scopes | ("emails:send" | "emails:read" | "domains:manage" | "webhooks:manage" | "suppressions:manage" | "templates:manage" | "account:manage" | "account:settings" | "keys:manage")[] | What the key may do. See Scopes above. |
data[].created_by_key_id | string (uuid) | null | The key that created this one through this API, or null when a person made it in the dashboard. |
data[].last_used_at | string (ISO 8601) | null | When the key last authenticated a request, or null. |
data[].created_at | string (ISO 8601) | When the record was created. |
data[].revoked_at | string (ISO 8601) | null | When the key was revoked, or null while it works. |
/v1/api-keysCreate an API keyNeeds keys:manage. 403 forbidden when a requested scope is one the calling key does not hold, or is an owner-only scope; the message names it. 403 live_key_required when a test key asks for a live key.
The same limit as creating keys in the dashboard: 20 an hour per account, then 429 rate_limited.
The new key records which key created it in created_by_key_id, and the dashboard shows it. If the account has the new API key alert on, the owner is emailed, with the name of the key that created it.
Request body
| Field | Type | Description |
|---|---|---|
namerequired | string | The key's name, for recognising it later. |
moderequired | "live" | "test" | live or test. The prefix of the key says the same. |
scopesrequired | ("emails:send" | "emails:read" | "domains:manage" | "webhooks:manage" | "suppressions:manage" | "templates:manage" | "account:manage" | "account:settings" | "keys:manage")[] | What the key may do. See Scopes above. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Unique id. |
name | string | The key's name, for recognising it later. |
mode | "live" | "test" | live or test. The prefix of the key says the same. |
prefix | string | The first characters of the key, enough to recognise it. The whole key is never returned again. |
scopes | ("emails:send" | "emails:read" | "domains:manage" | "webhooks:manage" | "suppressions:manage" | "templates:manage" | "account:manage" | "account:settings" | "keys:manage")[] | What the key may do. See Scopes above. |
created_by_key_id | string (uuid) | null | The key that created this one through this API, or null when a person made it in the dashboard. |
last_used_at | string (ISO 8601) | null | When the key last authenticated a request, or null. |
created_at | string (ISO 8601) | When the record was created. |
revoked_at | string (ISO 8601) | null | When the key was revoked, or null while it works. |
key | string | The full key. Returned only here; store it at once. |
/v1/api-keys/:idRevoke an API keyNeeds keys:manage. Revokes a key the calling key created, or the calling key itself. Any other id is a 404, so a key cannot learn which other keys exist or switch off the ones your application uses.
Requests using the revoked key fail at once. Revoking is permanent: create a new key instead.
Response 204
Empty body.
Data rights
Answer a data subject access request or an erasure request. Both need the `account:manage` scope and a live key.
/v1/account/exportExport everything held for the accountNeeds account:manage and a live key.
One JSON document, not paginated: account, users, keys, domains, emails with bodies, events, suppressions and webhooks. It is streamed as it is read, so a large account starts downloading at once.
At most 3 exports per account per hour (429 rate_limited after that, with Retry-After), and one at a time (409 conflict while another is running).
Response 200
Empty body.
/v1/recipients/eraseErase a recipientNeeds account:manage and a live key.
Removes the address everywhere it is held: the recipients, reply-to, subject and headers of emails, their events (bounce diagnostics included), the webhook deliveries made for them and their logs, and abuse samples. Emails sent only to them are deleted, as is the stored body of every email sent to them. Keeps only a hashed suppression entry so it is never sent to again.
Request body
| Field | Type | Description |
|---|---|---|
email_addressrequired | string (email) | The suppressed address. |
Response 200
| Field | Type | Description |
|---|---|---|
recipient_hash | string | |
emails_redacted | integer | |
emails_deleted | integer | |
events_redacted | integer | |
suppressed | boolean |
Webhook payload
The body POSTed to your endpoints, signed with Avelto-Signature: t=<unix seconds>,v1=<hex> where v1 is the HMAC-SHA256 of <t>.<body>. See Webhooks.
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Event id. The same id is sent again on every retry; use it to de-duplicate. |
type | "email.queued" | "email.scheduled" | "email.sent" | "email.delivered" | "email.delivery_delayed" | "email.bounced" | "email.complained" | "email.rejected" | "email.failed" | "email.cancelled" | Event type. |
created_at | string (ISO 8601) | When the record was created. |
data | object | Items on this page. |
data.email_id | string (uuid) | The email the event belongs to. |
data.mode | "live" | "test" | live or test, taken from the API key that created the email. |
data.from | string | Sender. a@b.com or Name <a@b.com>. The domain must be verified on your account, or be your sandbox domain. |
data.to | string[] | One recipient or a list. At most 50 recipients across to, cc and bcc. |
data.subject | string | Subject line. Line breaks are rejected. |
data.tags | string[] | Up to 10 tags of letters, numbers, _ - : . for filtering and reporting. |
data.status | string | The email's status after this event. |
data.details | object<string, any> | Event-specific details such as bounce type or diagnostic code. |
Errors
Every error response has this shape. The code is stable; the message is for people.
| Field | Type | Description |
|---|---|---|
error | object | Failure reason, when status is failed. |
error.code | "validation_error" | "unauthorized" | "forbidden" | "live_key_required" | "not_found" | "conflict" | "domain_unavailable" | "rate_limited" | "internal_error" | "sandbox_recipient_not_allowed" | "domain_not_verified" | "recipient_suppressed" | "not_scheduled" | "plan_limit" | "account_paused" | "account_suspended" | One of the codes below. |
error.message | string | Human-readable explanation. |
error.details | any | Extra context, for example the failing fields of a validation error. |
| Code | Status | When |
|---|---|---|
validation_error | 400 | The body or query failed validation. details lists the issues. |
unauthorized | 401 | Missing, malformed or revoked API key. |
forbidden | 403 | A key without the scope for the call, or a key asking to create a key with a scope it does not hold or may never grant. The message names the scope. |
live_key_required | 403 | A test key on an action that live sending depends on: removing a domain, managing templates or webhook endpoints, reading or changing suppressions, changing the account settings, creating a live key, exporting the account or erasing a recipient. |
not_found | 404 | No such email, domain, template, template version, webhook, delivery, suppression or API key on this account, or an API key this key did not create. |
conflict | 409 | The resource already exists: the domain or suppression is already added, or a template slug you chose is already taken. |
domain_unavailable | 409 | The domain cannot be added: it is part of a domain on another account, is reserved, or is already set up elsewhere. Contact support if it belongs to you. |
rate_limited | 429 | More than the per-key request rate (between 10 and 50 a second depending on plan). Honour Retry-After. |
internal_error | 500 | Something went wrong on our side. Safe to retry with an idempotency key. |
sandbox_recipient_not_allowed | 403 | Sending from the sandbox domain to anyone other than the account owner's verified email. |
domain_not_verified | 403 | from uses a domain that is not added and verified on this account. |
recipient_suppressed | 422 | A recipient is on the suppression list. details.suppressed lists the addresses. |
not_scheduled | 409 | Cancel was called on an email whose status is not scheduled. |
plan_limit | 429 / 403 | Monthly email limit or first-week daily cap reached (429), or the plan's domain limit reached (403). |
account_paused | 403 | Sending is paused on the account, usually after a high bounce or complaint rate. |
account_suspended | 403 | The account has been suspended by us. The message says why, and the dashboard has a button to write to us. |