Errors, limits & paging
The rules every endpoint shares. Base URL https://zapsmtp.xyz/v1, JSON in and out, timestamps in RFC 3339 (UTC), ids are time-ordered UUIDv7 strings.
API keys
Send the key as Authorization: Bearer <key>. The full key is shown once, when you create it; we store only a hash.
zap_…live | Delivers email. The From domain must be verified in your account. |
|---|---|
zap_test_…test | Full validation, email log, events and webhooks, but nothing is delivered and the monthly quota is not charged (the per-second limit still applies). Any From domain is accepted. Events carry test: true; list them with GET /emails?mode=test. |
full_accesspermission | Every endpoint. |
sending_accesspermission | Only sending: POST /emails, /emails/batch, /emails/spam-check, rescheduling and canceling. Can also be limited to one domain and one stream. |
Errors
Errors use HTTP status codes and always carry both a flat shape (statusCode, name, message) and a nested one. name equals error.code. Every response has an X-Request-Id header; include it when you contact support.
{
"statusCode": 422,
"name": "missing_required_field",
"message": "subject is required",
"error": {
"code": "missing_required_field",
"message": "subject is required",
"type": "invalid_request_error"
}
}validation_errorHTTP 400 | Malformed JSON or a field the endpoint does not know (unknown fields are rejected, so typos never pass silently). |
|---|---|
invalid_idempotency_keyHTTP 400 | Idempotency-Key longer than 256 bytes. |
missing_api_keyHTTP 401 | No Authorization header. |
invalid_api_keyHTTP 401 | Unknown, revoked or expired key. |
restricted_api_keyHTTP 403 | The key has sending access only, or is limited to another domain or stream. |
domain_not_verifiedHTTP 403 | Live key sending from a domain that is not verified. |
account_suspendedHTTP 403 | The account is suspended. |
plan_limit_reachedHTTP 403 | Your plan’s limit for domains or SMTP credentials is reached. |
test_key_restrictedHTTP 403 | A test key (zap_test_…) tried to change the account: test keys may send test email, render templates and read. Use a live key. |
key_restrictedHTTP 403 | A restricted key creating or changing keys tried to give them more than it has: an expiry later than its own, or another stream. |
email_unverifiedHTTP 403 | Confirm your account email address first; we sent a link to it. |
session_requiredHTTP 403 | Team and account management needs a dashboard session, not an API key. |
not_foundHTTP 404 | No such resource in your account (or outside your plan’s log retention). |
method_not_allowedHTTP 405 | The path exists, but not with this HTTP method. |
idempotency_key_reusedHTTP 409 | The Idempotency-Key was used with a different body. |
idempotency_request_in_progressHTTP 409 | The first request with this key is still running. |
not_cancellableHTTP 409 | The scheduled email was already dispatched or canceled. |
conflictHTTP 409 | The resource already exists. |
not_readyHTTP 409 | The received email is still being stored; retry the erasure in a few seconds. |
link_expiredHTTP 410 | The download link has expired; request a new one from the API. |
payload_too_largeHTTP 413 | Body over 40 MB. |
misdirected_cellHTTP 421 | Your account is served by another cell of ZapSMTP. The message names the cell and its URL, and the Zap-Cell header names the cell. Send to that URL; retrying here fails the same way. |
missing_required_fieldHTTP 422 | A required field is missing. |
invalid_parameterHTTP 422 | A field has an invalid value; the message says which. |
invalid_attachmentHTTP 422 | An attachment could not be decoded or fetched. |
template_render_errorHTTP 422 | A template variable is missing, the template is invalid, or (on save) a tag is not a variable name. |
template_variables_missingHTTP 422 | A send with a template lacks required variables that have no default; the message lists them (see Variables). |
template_variables_invalidHTTP 422 | A send with a template gives variables of the wrong type for the template’s variable schema; the message lists them. |
reserved_domainHTTP 422 | The domain is reserved and cannot be added. |
invalid_logoHTTP 422 | The brand logo SVG could not be used (PUT /domains/{id}/brand); the message says why. |
invalid_certificateHTTP 422 | The brand certificate (VMC) PEM is not valid (PUT /domains/{id}/brand). |
automated_messageHTTP 422 | The email you reply to is automated (an auto-reply, a bounce, a list); answering it can start a mail loop. Pass allow_automated: true to reply anyway. |
limit_exceededHTTP 422 | A per-account limit is reached, such as 20 forward destinations. |
quota_exceededHTTP 422 | The image library is full (2,000 images or 1 GB): delete unused images first. |
invalid_imageHTTP 422 | An uploaded image could not be used; the message says why. |
rejectedHTTP 422 | The MTA refused the message; the message gives its reason. Not retryable as is. |
rate_limit_exceededHTTP 429 | Over the per-second limit (Retry-After: 1), or too many failed authentications. |
monthly_quota_exceededHTTP 429 | Free plan or spending cap reached for this month. |
daily_quota_exceededHTTP 429 | Daily limit reached (Free plan: 100 emails a day, resets at 00:00 UTC). |
overloadedHTTP 429 | The server is at capacity (Retry-After: 1), or a send could not be taken because the event pipeline is backed up (Retry-After: 5). Nothing was sent; retry after Retry-After with the same Idempotency-Key. |
thread_reply_limitHTTP 429 | Too many replies in one conversation in the last hour (a mail loop?). Retry-After: 3600. |
internal_errorHTTP 500 | Something failed on our side. Safe to retry with the same Idempotency-Key. |
service_unavailableHTTP 503 | Temporarily unable to accept, for example while a backing service fails over after a node loss (Retry-After: 2), or while the MTA is unavailable, the suppression list is loading or too many large bodies are in flight (Retry-After: 5). Safe to retry with the same Idempotency-Key. |
scanner_unavailableHTTP 503 | The spam checkers are temporarily unavailable (spam checks); try again. |
storage_unavailableHTTP 503 | Object storage is not configured on this deployment, so images cannot be uploaded. |
inbound_not_configuredHTTP 503 | Inbound email is not enabled on this deployment. |
Rate limits and quotas
Sending responses report your budget in headers. Back off on 429 and honor Retry-After.
RateLimit-Limit | Recipients per second for your account. |
|---|---|
RateLimit-Remaining | Recipients left in the current second. |
RateLimit-Reset | Seconds until the budget refills (always 1). |
Zap-Quota-Monthly-Limit | Emails included this month, or unlimited on paid plans without a spending cap (extra email is billed as overage). |
Zap-Quota-Monthly-Remaining | Emails left this month; omitted when the limit is unlimited. |
| Recipients per email | 50 across to, cc and bcc |
|---|---|
| Emails per batch | 100 |
| Request size | 40 MB, attachments included |
| Free plan | 3,000 emails a month, at most 100 a day |
| Spam checks | 30 a minute per account |
Idempotency
Send Idempotency-Key: <1–256 bytes> with POST /emails and POST /emails/batch to retry without sending twice. Keys belong to the API key and are kept for 24 hours together with a fingerprint of the request.
| Same key, same body, finished | The original response is returned with Idempotent-Replayed: true. |
|---|---|
| Same key, different body | 409 idempotency_key_reused |
| Same key, first request still running | 409 idempotency_request_in_progress |
| First attempt got 429 or 5xx | The key is released; retry with the same key. |
Pagination
Lists return {"object": "list", "has_more": false, "data": [...]}, newest first.
GET /emailsandGET /emails/receiving:limit1–100 (default 20); pass the last id (or an RFC 3339 time) asbeforefor older items,afterfor newer ones.GET /suppressions:limit1–1000 (default 100); passnext_cursorback asafterwhilehas_moreis true.- Other lists return every item in one response.
Email statuses
An email’s status is the latest event: queued, delivered, delivery_delayed, bounced, complained, failed, opened, clicked, replied (reply tracking) or suppressed. Scheduled emails show scheduled until they are sent, or canceled.
Streams
transactional (default) and broadcast leave from separate IP pools, so a newsletter never slows down a password reset. Broadcast emails to one recipient get RFC 8058 one-click unsubscribe headers automatically; an unsubscribe adds the address to your suppressions and stops broadcast mail to it, while transactional mail still goes out. Bounces, complaints and manual suppressions stop both streams.