Skip to main content

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​

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-Key header. There is no body field for it.
  • Format — 1 to 64 characters: Latin letters, digits, and ., _, :, -. Any other format returns 400 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​

SituationResponse
The first request with this key is still running409 REQUEST_ALREADY_IN_PROGRESS and a Retry-After header — retry later
The first request has finishedThe 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 body409 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 the X-Quota-Remaining header. A retry returns the same data.id and recipients[].id.
  • 4xx errors — 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_REQUIRED is 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.
  • 5xx is not stored: the email isn't sent and no quota is charged — retry the request with the same key.
  • 401 and 403 are 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: wait Retry-After seconds 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.

ResponseX-Quota-Remaining value
202Remaining quota right after this email was charged
402 PAYMENT_REQUIREDCurrent remaining quota — it shows how much was missing
Replay with Idempotency-Replayed: trueRemaining 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​

HTTPerror.codeCauseAction
400INVALID_IDEMPOTENCY_KEYThe key doesn't match the formatFix the key
402PAYMENT_REQUIREDThe quota isn't enough for all recipients of the emailWait for the next billing period or change your plan, then retry with the same key
409REQUEST_ALREADY_IN_PROGRESSA request with this key is still runningWait Retry-After seconds and retry
409IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOADThe key was already used with a different bodyRetry with the same body or use a new key for a new email

Other codes are in Send error codes.

See also​