Errors, limits and headers
The error shape every Reggio Digital API call shares, the status codes, rate limits, idempotency keys, response headers and the versioning promise.
Requests
- Send JSON with
Content-Type: application/json, and ask for JSON back withAccept: application/json. - Call us over
https. - Times are ISO 8601 with a timezone, for example
2026-10-05T09:00:00-04:00. Times we send back are in UTC. - An id in a path is used exactly as we gave it to you. An email address in a path is URL encoded, so
sam@example.combecomessam%40example.com.
Errors
Every error we return from /api/v1 has the same shape, so your code only needs one handler:
{
"error": {
"type": "invalid_request",
"message": "The to field must be an array.",
"fields": {
"to": ["The to field must be an array."]
},
"request_id": "0f5b6c1e-2d3a-4b8c-9e7f-1a2b3c4d5e6f"
}
}
typeis stable and safe to branch on.messageis written for a person and can change wording, so log it rather than matching on it.fieldsappears when a rule on a named field failed, one entry per field. In a batch the name includes the position, as inemails.3.to.request_ididentifies the call. See finding a call again.
| Status | error.type | When |
|---|---|---|
| 401 | unauthorized |
No key was sent, it is not a Reggio key, or it has been revoked. |
| 402 | no_plan |
The account does not have the product this call belongs to. |
| 403 | forbidden |
The key does not have the ability this call needs. |
| 403 | send_rejected |
The from address is on a domain this key or account cannot send from. |
| 404 | not_found |
No message, list or contact with that id on this account. |
| 409 | send_rejected |
The sending domain has not finished verifying, or sending from it is stopped. |
| 409 | idempotency_conflict |
The Idempotency-Key was already used for a different request. |
| 409 | not_cancellable |
The message has already gone out, so it cannot be cancelled. |
| 409 | not_reschedulable |
The message has already gone out, so its time cannot change. |
| 413 | send_rejected |
The attachments add up to more than 28MB. |
| 422 | invalid_request |
A field is missing or malformed. error.fields names each one. |
| 422 | send_rejected |
The request is well formed but cannot be sent: no sending domain is set up for the from address, or every recipient is suppressed. |
| 429 | rate_limited |
More than 120 requests in a minute. Wait for Retry-After seconds. |
| 429 | send_rejected |
The account has reached its sending ceiling for now. |
A 500, 502 or 503 means something went wrong on our side or with the mail network, and a 500 may arrive without the body above. Retry those with backoff. With an Idempotency-Key, a retry of a request that did go through gets the original answer instead of a second send.
Rate limits
Each account can make 120 requests a minute across the email and newsletter APIs together. Every key on the account shares that budget.
Past the limit you get a 429 with the type rate_limited and a Retry-After header saying how many seconds to wait. Successful responses carry X-RateLimit-Limit and X-RateLimit-Remaining, so a busy integration can slow down before it hits the ceiling.
If you need to send a large number of similar messages, use a batch: up to 100 messages count as one request.
Idempotency
Networks drop. When your code retries a request it is not sure went through, send an Idempotency-Key header with a value unique to that one piece of work, such as an order number:
Idempotency-Key: receipt-1042
For 24 hours, the same key with the same request body gets the original answer back, marked with Idempotent-Replayed: true, and nothing is sent or added twice. The same key with a different body gets a 409 with idempotency_conflict, because that is almost always a bug in the caller. Keys are at most 256 characters and are scoped to the API key that sent them.
Idempotency keys work on POST /emails, POST /emails/batch and POST /newsletter/contacts.
Response headers
| Header | What it tells you |
|---|---|
X-Reggio-Request-Id |
A unique id for this call. Also inside error.request_id on a failure. Quote it to support. |
X-Reggio-Monthly-Quota |
Your monthly email allowance. On email sends only. |
X-Reggio-Monthly-Remaining |
How much of that allowance is left this month. On email sends only. |
Idempotent-Replayed |
Set to true when the answer is a replay of an earlier request with the same Idempotency-Key. |
X-RateLimit-Limit |
Requests allowed per minute for your account. |
X-RateLimit-Remaining |
Requests left in the current minute. |
Retry-After |
On a 429, the number of seconds to wait before trying again. |
Finding a call again
Every response carries X-Reggio-Request-Id, and every error repeats it as error.request_id. Keep it in your logs.
Paste one into the API log in the dashboard and you get that call back: the address you called, the status we answered with, and the fields we could not accept. It works for calls that never became a message, which is exactly when you need it. We keep this for 30 days. We never keep request bodies, attachments or keys in it.
If you contact support about a call, quoting the request id is the fastest way for us to find it.
Versioning
Everything in these docs is under /api/v1. Within v1:
- we can add endpoints, optional request fields, response fields, headers, error types and webhook event types;
- we will not remove or rename a field, change its type, or make an optional field required.
Write code that ignores fields it does not recognise and treats an unknown error type like a generic failure for its status code. A change that would break that code ships under a new version instead. Anything an integration would notice is in the changelog.