Idempotency and quota
The Idempotency-Key header protects against a second email when a request is retried, and the X-Quota-Remaining response header shows how many emails are left in the quota. This article describes both for POST /api/v1/public/emails.
Before you start
- A token with the
email.sendscope and a working request from the minimal example.
Idempotency
If the HTTP client didn't get a response (timeout, dropped connection), you don't know whether the email was accepted. Retry the request with the same Idempotency-Key: you'll get the result of the first request or 409 REQUEST_ALREADY_IN_PROGRESS, but not a second email. Without a key, a retry creates a second email.
curl -X POST https://api.emailmassivo.com/api/v1/public/emails \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: idempotency-test-1" \
-d '{"data": {"from": {"email": "noreply@yourdomain.com"}, "to": {"email": "you@yourdomain.com"}, "subject": "Test", "text": "Check"}}'
The idempotency key
- Sent only in the
Idempotency-Keyheader. There is no body field for it. - Format — 1 to 64 characters: Latin letters, digits, and
.,_,:,-. Any other format returns400 INVALID_IDEMPOTENCY_KEY. - The key is optional. Without it, every request creates a new email.
- A key is valid for 24 hours from the first request and within one API key: the same idempotency key sent with another API key is an independent request.
Good keys are tied to an event in your system: order-12345-confirmation, password-reset:8f2c1d. A random UUID per attempt is useless — a retry must carry the same key as the first attempt.
What happens on a retry
| Situation | Response |
|---|---|
| The first request with this key is still running | 409 REQUEST_ALREADY_IN_PROGRESS and a Retry-After header — retry later |
| The first request has finished | The same HTTP status and the same data as the first response, an Idempotency-Replayed: true header; meta comes from the new request |
| Same key, different request body | 409 IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD |
The body is compared byte by byte: retry with exactly the same JSON you sent the first time rather than building it again — a different field order or whitespace counts as a different body.
Which responses are stored
202— stored together with theX-Quota-Remainingheader. A retry returns the samedata.idandrecipients[].id.4xxerrors — stored, including validation errors: a retry with the same key and body returns the same error. To send a corrected email, use a new key. This also applies to fixes in the account with the same body — the sending key passed moderation, you verified the domain or created the template: a retry with the previous key returns the stored error.402 PAYMENT_REQUIREDis not stored: wait for the next billing period to start or change your plan, and retry the request with the same key — it runs again.5xxis not stored: the email isn't sent and no quota is charged — retry the request with the same key.401and403are not stored: fix the cause and retry the request with the same key.429— the API key's request limit or the domain's email limit is exceeded (Rate limit) — is not stored: waitRetry-Afterseconds and retry the request with the same key.
There's no idempotency over MCP — see Sending through MCP.
Quota and X-Quota-Remaining
Quota is the monthly email limit of your plan, shared by all sending keys of the account — see Quota and rate. It is charged when an email is accepted: one email for each recipient (to, cc, bcc) the email will be sent to. Recipients with status suppressed and bounced in a 202 response are not charged.
If the quota isn't enough for all recipients, the email isn't sent — 402 PAYMENT_REQUIRED. Wait for the next billing period to start or change your plan, and retry the request with the same key.
Quota is not refunded: if an email was sent but not delivered (bounced, failed), the charge stays.
The X-Quota-Remaining header
The remaining quota comes only in the header; it isn't in the response body.
| Response | X-Quota-Remaining value |
|---|---|
202 | Remaining quota right after this email was charged |
402 PAYMENT_REQUIRED | Current remaining quota — it shows how much was missing |
Replay with Idempotency-Replayed: true | Remaining quota as of the first, original request |
This is the remaining quota at the moment this email was accepted. Parallel sends may reduce it before you read the header, so use it for monitoring and alerts, not as an exact counter.
const header = response.headers.get('X-Quota-Remaining');
if (header !== null && Number(header) < 1000) {
// notifyOps is your alerting function
notifyOps(`${header} emails left — time to change the plan`);
}
Handling errors
| HTTP | error.code | Cause | Action |
|---|---|---|---|
| 400 | INVALID_IDEMPOTENCY_KEY | The key doesn't match the format | Fix the key |
| 402 | PAYMENT_REQUIRED | The quota isn't enough for all recipients of the email | Wait for the next billing period or change your plan, then retry with the same key |
| 409 | REQUEST_ALREADY_IN_PROGRESS | A request with this key is still running | Wait Retry-After seconds and retry |
| 409 | IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD | The key was already used with a different body | Retry with the same body or use a new key for a new email |
Other codes are in Send error codes.
See also
- Send a transactional email — request fields and the
202response. - Send error codes — the full list.
- Sending keys — quota, limits, the default key.