Errors
Every error has the shape { statusCode, message, name, docs_url }. Branch on name; message says what to do next, and X-Request-Id identifies the request.
| Code | Status | Cause | Resolution |
|---|---|---|---|
validation_error | 400 | A field has the wrong type or format or is over its size limit, the body is not valid JSON, or the body was not sent as JSON | Read message for the specific problem and how to fix it, then send the request again |
invalid_idempotency_key | 400 | The Idempotency-Key header is empty or longer than 256 characters | Send a key of 1 to 256 characters, such as a UUID, or leave the header out |
missing_api_key | 401 | The request has no Authorization: Bearer <key> header | Send the agent key or the reviewer key as Authorization: Bearer <key> |
invalid_api_key | 401 | The key in Authorization is not one this service issued | Check the key for typos, then use the agent key or the reviewer key you were given |
invalid_permission | 403 | The key is valid but not allowed here: agent keys never approve or send, reviewer keys never change drafts | Do not retry with another key. Agents poll GET /drafts/{id} until status is rejected, sent or failed; only the agent key changes drafts |
not_found | 404 | No route matches the method and path, or no resource has the id in the path | Check the method and path against the API reference, and the id against the one the create response returned |
missing_required_field | 422 | The body is missing one or more required fields | Add the fields named in message and send the request again |
missing_required_parameter | 422 | The query string is missing one or more required parameters | Add the parameters named in message and send the request again |
invalid_parameter | 422 | A path or query parameter has the wrong format, such as a draft id that is not a UUID | Use the format message gives for the path or query parameter it names, then send the request again |
invalid_from_address | 422 | from is not an email address, or it is not on the domain this service sends from | Leave from out to send from the default address, or use an address on the domain message names |
revision_stale | 409 | The revision the request names is not the current revision of the draft, usually because another revision was saved first | Fetch the draft, apply your change to current_revision, and send it again with its id |
content_hash_mismatch | 409 | The content_hash sent with an approval is not the hash of the revision it names, so what the reviewer saw is not what would be sent | Reload the draft, review current_revision again, and approve it with its own content_hash |
draft_locked | 409 | The draft is approved, sending or sent, so it can no longer change | Poll GET /drafts/{id} until it is sent or failed, then follow next_step. Create a new draft only for a different email |
invalid_state_transition | 409 | The draft is not in a status this action accepts, such as submitting a draft that was approved | Fetch the draft, read status and next_step, and do what next_step says |
resource_locked | 409 | Other requests kept changing the draft while this one was being saved | Wait a moment, fetch the draft, and send the request again |
invalid_idempotent_request | 409 | The Idempotency-Key was used in the last 24 hours for a request with a different body | Use a new key for a new draft. To get the first response again, send the original body with the same key |
concurrent_idempotent_requests | 409 | Another request with the same Idempotency-Key is still in progress | Wait a second and send the same request again with the same key: once the first one finishes, the retry returns its response |
missing_webhook_signature | 401 | A request to POST /webhooks/resend has no svix-id, svix-timestamp or svix-signature header, so it is not a Resend webhook | Only Resend calls this route; agents never do. Resend signs every event, so check that nothing between Resend and the service drops the svix-* headers |
invalid_webhook_signature | 401 | The svix-signature of a webhook does not match its body and RESEND_WEBHOOK_SECRET, or its svix-timestamp is more than 5 minutes from now | Nothing was recorded. Set RESEND_WEBHOOK_SECRET to the signing secret of this webhook in the Resend dashboard and redeploy; Resend retries the event |
webhook_not_configured | 503 | RESEND_WEBHOOK_SECRET is not set, so the service cannot verify Resend webhooks and records no delivery events | The operator sets RESEND_WEBHOOK_SECRET to the signing secret of the webhook in the Resend dashboard and redeploys; Resend retries the events it could not deliver |
internal_server_error | 500 | Something failed inside the service | Retry a GET, or POST /drafts with the same Idempotency-Key and body. Otherwise fetch the resource to check whether the change was saved before you retry. If it keeps failing, report the X-Request-Id |
Send failures
After approval the service sends the email through Resend, always with the Idempotency-Key send-email/<revision_id>, so a retry can never deliver it twice. Each failed attempt is recorded with one of these reasons. Retried reasons put the send back in the queue until its next attempt (the Retry-After Resend sent, or a backoff from 30 seconds to 30 minutes), and the draft stays locked; when the 10 Inngest retries run out, recover-stalled-sends starts a new run. Any other 4xx, a daily or monthly quota error included, ends the send and moves the draft to failed, with failure_reason rejected when no earlier attempt could have delivered the email, and outcome_unknown when one could.
| Reason | Retried | Cause | Resolution |
|---|---|---|---|
rate_limited | Yes | Resend answered 429 without a quota error name, usually rate_limit_exceeded: too many requests per second | Nothing: the send waits for the delay in Retry-After when Resend sends one, otherwise backs off exponentially, and tries again |
server_error | Yes | Resend, or the network in front of it, answered with a 5xx status | Nothing: the send backs off and tries again with the same key |
in_progress | Yes | Resend is still handling an earlier request with the same key (concurrent_idempotent_requests or resource_locked) | Nothing: the send tries again shortly |
network | Yes | The request timed out or never reached Resend, so the email may or may not have been sent | Nothing: the retry carries the same key, so Resend sends it at most once |
unknown_response | Yes | Resend answered without an error name or a status | Nothing: the send tries again with the same key |
rejected | No | Resend refused the email with a 4xx, such as validation_error, invalid_from_address, an unverified domain, or the 429 daily_quota_exceeded or monthly_quota_exceeded, and no earlier attempt could have delivered it | If the error is about the email (an address, the subject or a body), fix it in a new revision. If it is about the sending domain or the API key, the operator fixes that first. After a quota error, wait for the reset instead: a daily quota resets at midnight UTC and a monthly one at the start of the month, and neither needs the operator. Nothing was delivered, so a new revision with the same content can then be submitted and approved again |
outcome_unknown | No | An attempt whose outcome is unknown (a timeout, a 5xx, a concurrent request) may have delivered the email, and the send cannot be retried safely: 23 hours have passed since that attempt, and Resend forgets an idempotency key after 24, or a later attempt got a 4xx | The agent may prepare a new revision, but a person looks for the email in the Resend dashboard before approving it. If the last error is about the sending domain or the API key, the operator fixes that first; after a quota error, wait for the quota to reset; otherwise the new revision fails the same way |