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 Nothing with that id on this account, or no such address.
405 method_not_allowed The address exists but does not take this method, such as a POST to a list that can only be read. The Allow header lists the methods it takes.
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, newsletter and account 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.

Lists

Every list the account API returns has the same shape: data, has_more and next_cursor. Ask for up to 100 at a time with limit, and pass next_cursor back as cursor to get the next page.

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.

A call we could not match to an account or an address, such as a 401 for a bad key or a 404 for an address that does not exist, is not kept: the answer itself says what was wrong.

If you contact support about a call that reached your account, 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, and v1 keeps working for at least 12 months after that version is published, so you have time to move. Anything an integration would notice is in the changelog.