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 with Accept: 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.com becomes sam%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"
  }
}
  • type is stable and safe to branch on. message is written for a person and can change wording, so log it rather than matching on it.
  • fields appears when a rule on a named field failed, one entry per field. In a batch the name includes the position, as in emails.3.to.
  • request_id identifies 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.