# Email approval service > Agents create and revise email drafts and submit them for review. A person approves or rejects each one, and approved revisions are sent through Resend exactly once. The API base URL is https://resend-devtools.riquetti.com.br/api, and every path below is relative to it. An agent never sends email itself and can never approve its own draft. The loop is: 1. Create a draft: POST /drafts with an agent key and an `Idempotency-Key` header that names this email, such as `create-draft/`. If it times out or gets a `5xx`, send it again with the same key and body within 24 hours: you get the first draft back, never a second one. A `4xx` is not stored: fix the request first. Keep the draft `id` and `current_revision.id` from the response. 2. To change it, POST /drafts/{id}/revisions with `base_revision_id` set to the `current_revision.id` you last read, plus the whole email: copy the email fields of `current_revision` (`from`, `to`, `cc`, `bcc`, `reply_to`, `subject`, `html`, `text`) and change what you need. A revision replaces every field. 3. Submit it: POST /drafts/{id}/submit with `revision_id` set to `current_revision.id`. 4. Poll GET /drafts/{id} until `status` is `rejected`, `sent` or `failed`. A person decides, which can take minutes or hours: poll every 10 seconds at first, then back off to once a minute. `approved` and `sending` are not final. 5. If it is `rejected`, read `latest_review.reason`, write a new revision that addresses it (step 2) and submit that one. 6. If it is `failed`, read `send.failure_reason`. `rejected`: Resend refused it and nothing was delivered. Fix an email error in a new revision; for a sending domain or API key error in `send.last_error`, ask the operator to fix it; for a quota error (`daily_quota_exceeded`, `monthly_quota_exceeded`), wait for the reset (midnight UTC for daily, the start of the month for monthly). Then write a new revision (the same content is fine) and submit it. `outcome_unknown`: it may have been delivered. Stop and tell the user: a person must check the Resend dashboard before anyone approves a new revision, which would become a second email. 7. If it is `sent`, Resend accepted it. `send.last_event` then says what Resend reported afterwards, when the operator has set up its webhook: `sent` (accepted, not yet delivered), `delivered`, `delivery_delayed` (Resend keeps trying), `bounced` (at least one recipient did not get it: tell the user and check the addresses) or `complained` (a recipient marked it as spam: tell the user). `null` means no event has arrived yet, or the webhook is not set up. A sent draft cannot be sent again; a retry is a new draft. Every draft response has a `next_step` field with the exact request to send next. Follow it. ## Authentication - [Keys](https://resend-devtools.riquetti.com.br/api/docs): Send `Authorization: Bearer ` on every request except GET /, GET /health, the docs and the Resend webhook. Agent keys start with `agent_` and create, revise, submit, list and read drafts. Reviewer keys start with `reviewer_` and belong to people: only they approve or reject. Ask the operator of this service for an agent key, keep it in an environment variable, and never put it in a prompt, a log or a file. - [Sender](https://resend-devtools.riquetti.com.br/api): The discovery document's `sender.default_from` is the address emails go out from when `from` is left out, and `sender.allowed_domain` is the only domain `from` may use. A display name in `from` must not contain @. ## Endpoints - [GET /](https://resend-devtools.riquetti.com.br/api): Describe the service as JSON: every endpoint, the key it needs, the sender and the links below. No key. - [GET /health](https://resend-devtools.riquetti.com.br/api/health): Check that the service is up. No key. - [POST /drafts](https://resend-devtools.riquetti.com.br/api/docs): Create a draft with revision 1. Agent key. `to` and `subject` are required, plus `html`, `text` or both; `from`, `cc`, `bcc` and `reply_to` are optional. An empty `cc`, `bcc` or `reply_to` list, or a null `html` or `text`, counts as left out; `to` needs at least one address. Returns 201. Send `Idempotency-Key` (1 to 256 characters), as with Resend's POST /emails: the same key and body within 24 hours return the first response with `Idempotent-Replayed: true`, and a failed request is not stored. - [GET /drafts](https://resend-devtools.riquetti.com.br/api/docs): List drafts as summaries without bodies, newest first. Agent or reviewer key. Filter with `status` (`status=pending_review` is the review queue, longest waiting first) and page with `limit` (1 to 100, default 20) and `after` or `before`. - [GET /drafts/{id}](https://resend-devtools.riquetti.com.br/api/docs): Get a draft with its current revision, its latest review, its `send` (`status`, `failure_reason`, `last_error`, `resend_email_id`, `attempts`, `sent_at`, `last_event`, `last_event_at`) once approved, and `next_step`. Agent or reviewer key. - [GET /drafts/{id}/events](https://resend-devtools.riquetti.com.br/api/docs): The draft's history, oldest first, in one list without paging: each event's `type`, `actor`, revision, the rejection `reason` on `rejected` and the `failure_reason` on `send_failed`, plus what Resend reported after sending. Agent or reviewer key. - [GET /drafts/{id}/revisions](https://resend-devtools.riquetti.com.br/api/docs): Every revision of the draft without its bodies (recipients, subject, `content_hash`), oldest first, in one list without paging. Agent or reviewer key. - [POST /drafts/{id}/revisions](https://resend-devtools.riquetti.com.br/api/docs): Write a new revision on top of `base_revision_id`. Agent key. Returns 201. Revising a draft that waits for review withdraws it from review. - [GET /drafts/{id}/revisions/{revision_id}](https://resend-devtools.riquetti.com.br/api/docs): One revision with its `html` and `text` bodies, current or not: read what an earlier, rejected revision said. Agent or reviewer key. - [POST /drafts/{id}/submit](https://resend-devtools.riquetti.com.br/api/docs): Submit `revision_id` for review. Agent key. A repeat while the draft is `pending_review` returns 200 and changes nothing; once a person has decided, it returns 409 `invalid_state_transition`. - [POST /drafts/{id}/approve](https://resend-devtools.riquetti.com.br/api/docs): Reviewer key only, never an agent. A person approves `{"revision_id":"","content_hash":""}` and exactly one send of that revision is queued. A repeat returns 200 with the current state. - [POST /drafts/{id}/reject](https://resend-devtools.riquetti.com.br/api/docs): Reviewer key only, never an agent. A person rejects `{"revision_id":"","reason":""}`, and the agent reads the reason in `latest_review.reason`. ## Webhooks - [POST /webhooks/resend](https://resend-devtools.riquetti.com.br/api/docs): Resend calls this, never an agent. It records the email.sent, email.delivered, email.delivery_delayed, email.bounced and email.complained events as `send.last_event`, verified by their Svix signature. Read the draft instead. ## Errors - [`revision_stale`](https://resend-devtools.riquetti.com.br/api/docs/errors#revision_stale): 409. Another revision was saved after the one you named. Fetch the draft, apply your change to `current_revision` and send it again with that id. - [`draft_locked`](https://resend-devtools.riquetti.com.br/api/docs/errors#draft_locked): 409. An approved, sending or sent draft cannot change. If it is approved or sending, the email is on its way: poll it until it is `sent` or `failed` (then follow `next_step`), and never create a second draft for the same email. - [`invalid_state_transition`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_state_transition): 409. The draft's status does not allow this request, such as submitting a revision a person already rejected. Fetch the draft and do what `next_step` says; after a rejection, write a new revision first. - [`resource_locked`](https://resend-devtools.riquetti.com.br/api/docs/errors#resource_locked): 409. Other requests changed the draft at the same moment. Wait a moment, fetch it and send the request again. - [`invalid_idempotent_request`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_idempotent_request): 409. The `Idempotency-Key` was used in the last 24 hours with a different body. Use a new key for a new email; resend the original body to get the original draft back. - [`concurrent_idempotent_requests`](https://resend-devtools.riquetti.com.br/api/docs/errors#concurrent_idempotent_requests): 409. The first request with this key is still running. Wait a second and send the same request again. - [`content_hash_mismatch`](https://resend-devtools.riquetti.com.br/api/docs/errors#content_hash_mismatch): 409. Reviewers only: the hash sent with an approval is not the hash of that revision. - [`invalid_from_address`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_from_address): 422. `from` is not on `sender.allowed_domain`, or its display name contains @. Leave `from` out. - [`validation_error`](https://resend-devtools.riquetti.com.br/api/docs/errors#validation_error): 400. A field has the wrong type or format, or is over its size limit. Fix what `message` names. - [`invalid_idempotency_key`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_idempotency_key): 400. The `Idempotency-Key` header is empty or longer than 256 characters. - [`missing_required_field`](https://resend-devtools.riquetti.com.br/api/docs/errors#missing_required_field): 422. Add the fields `message` names. - [`missing_required_parameter`](https://resend-devtools.riquetti.com.br/api/docs/errors#missing_required_parameter): 422. Add the query parameters `message` names. - [`invalid_parameter`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_parameter): 422. A path or query parameter has the wrong format, such as a draft id that is not a UUID. - [`missing_api_key`](https://resend-devtools.riquetti.com.br/api/docs/errors#missing_api_key): 401. Send your agent key as `Authorization: Bearer `. - [`invalid_api_key`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_api_key): 401. The key is not one this service issued. Check it, or ask the operator for a new one. - [`invalid_permission`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_permission): 403. The route needs another kind of key. Agents never approve or reject; do not retry with another key. - [`not_found`](https://resend-devtools.riquetti.com.br/api/docs/errors#not_found): 404. Check the path and the draft id. - [`missing_webhook_signature`](https://resend-devtools.riquetti.com.br/api/docs/errors#missing_webhook_signature): 401. Only for POST /webhooks/resend, which Resend calls: the request is missing a Svix signature header. - [`invalid_webhook_signature`](https://resend-devtools.riquetti.com.br/api/docs/errors#invalid_webhook_signature): 401. Only for POST /webhooks/resend: the Svix signature does not match `RESEND_WEBHOOK_SECRET`, or `svix-timestamp` is more than 5 minutes from now. - [`webhook_not_configured`](https://resend-devtools.riquetti.com.br/api/docs/errors#webhook_not_configured): 503. Only for POST /webhooks/resend: the operator has not set `RESEND_WEBHOOK_SECRET`, so no new delivery events are recorded. - [`internal_server_error`](https://resend-devtools.riquetti.com.br/api/docs/errors#internal_server_error): 500. Retry a GET, and retry POST /drafts with the same `Idempotency-Key` and body. Before retrying a POST to a draft, fetch the draft to see whether the change was saved. A POST /drafts sent without a key may have created the draft: look for it in GET /drafts before sending it again. ## Docs - [Discovery document](https://resend-devtools.riquetti.com.br/api): The same endpoints as JSON, with the scopes each one accepts and the sender. - [OpenAPI 3.1 document](https://resend-devtools.riquetti.com.br/api/openapi.json): Every route, field and response. - [API reference](https://resend-devtools.riquetti.com.br/api/docs): The OpenAPI document, rendered for people. - [Error reference](https://resend-devtools.riquetti.com.br/api/docs/errors): Every error name with its status, cause and resolution, and every send failure reason, `rejected` and `outcome_unknown` included. Every error is JSON in Resend's shape, `{ "statusCode", "message", "name", "docs_url" }`: branch on `name`, and `message` says what to do next. ## Skills - [Email approvals skill](https://resend-devtools.riquetti.com.br/api/skill/SKILL.md): This workflow as an installable skill, with its references at https://resend-devtools.riquetti.com.br/api/skill/references/workflow.md, api.md and errors.md. Save the four files under `.claude/skills/email-approvals/`, keeping `references/`. The skills index is at https://resend-devtools.riquetti.com.br/api/skill/index.json and /.well-known/agent-skills/index.json. ## Optional - [React Email](https://react.email/docs/utilities/render): Using React Email? Render it with `@react-email/render` and send the html.