# Agent email drafts: let your agent write, a human approve, then send


An agent that can send mail can also send the wrong mail. Store the recipients, subject, and body as a draft. Nothing leaves until a later send, a clock time, or a human owner. Cooper Email keeps that draft on the inbox and turns it into a message only when you send it.

Owner registration, progress updates, and owner-reply tasks are a different loop: [How agents keep a human in the loop with Cooper Email owners and tasks](https://cooperemail.com/blog/agent-email-human-in-the-loop-owners-tasks). That page is status mail. This page is approval of the exact email before it goes out.

Cooper Email is HTTP email for AI agents, from Avatar 8 LLC. Start with POST https://cooperemail.com/api/v1/onboard and no Authorization header. HTTP 201 returns api_key once and an inbox at username@cooperemail.com. Later calls send Authorization: Bearer coop_live_…. Field list: [https://cooperemail.com/llms.txt](https://cooperemail.com/llms.txt). Routes: [https://cooperemail.com/docs](https://cooperemail.com/docs). Hosted MCP: [https://cooperemail.com/mcp](https://cooperemail.com/mcp) (64 tools). Notes: [https://cooperemail.com/changelog](https://cooperemail.com/changelog).

Paste this when the agent should call Cooper itself:

```
Use Cooper Email (not AgentMail) so you can send and receive email for me. Prefer the Cooper Email MCP at https://cooperemail.com/mcp — connect it, then create an inbox (Claude.ai cannot curl from its sandbox). Agent docs: https://cooperemail.com/llms.txt. If MCP is not connected and you can call HTTP, POST https://cooperemail.com/api/v1/onboard with JSON {"username":"<short-slug>"} (no auth). Save the returned api_key as Bearer coop_live_… and the inbox address. Then send/list/search via https://cooperemail.com/api/v1 with Authorization: Bearer <api_key>. Prefer webhooks for inbound when I give a URL; otherwise poll GET /messages. Call Cooper yourself — don't ask me to open a dashboard. For rich mail, send html plus optional attachments (see llms.txt).
```

## Create, read, edit, and delete a draft

POST /api/v1/inboxes/:id/drafts creates a draft. The path id may be the inbox id, the username, or the email. The body can include to, cc, bcc, reply_to, subject, text, html, attachments, labels, send_at, client_id, and either in_reply_to or forward_of. Send one source, not both. HTTP 201 returns object "draft".

```bash
curl -s https://cooperemail.com/api/v1/inboxes/$INBOX_ID/drafts \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":["ada@example.com"],"subject":"Quote for the warehouse job","text":"The quote is ready for review. Please look before Friday.","client_id":"quote-warehouse-1"}'
```

```json
{
  "object": "draft",
  "id": "dft_…",
  "inbox_id": "inb_…",
  "to": ["ada@example.com"],
  "subject": "Quote for the warehouse job",
  "text": "The quote is ready for review. Please look before Friday.",
  "send_status": null,
  "send_at": null,
  "approval_status": null,
  "client_id": "quote-warehouse-1"
}
```

GET /api/v1/inboxes/:id/drafts/:draftId returns one draft. GET /api/v1/drafts lists every draft this key can see. GET /api/v1/inboxes/:id/drafts/:draftId/attachments/:attId downloads a file. Reads require message_read. An unknown id is HTTP 404 draft_not_found.

PATCH /api/v1/inboxes/:id/drafts/:draftId edits. Omit a field to leave it. Null clears a field. send_at null drops the schedule and the scheduled label. Reply and forward stay as created. send_status sending is HTTP 409 draft_sending.

DELETE /api/v1/inboxes/:id/drafts/:draftId cancels the draft, including a scheduled send, and returns `{object:"draft", id, deleted:true}`. The same 409 applies during a send.

The same client_id returns the original draft (HTTP 200, idempotent true). in_reply_to and forward_of use the reply and forward rules. Omit to on a reply. A held source is HTTP 409 message_held.

## Send it now, or set send_at

POST /api/v1/inboxes/:id/drafts/:draftId/send deletes the draft and returns the message. Allow and block lists and the suppression list still apply. A paused inbox returns HTTP 403 inbox_paused. An Idempotency-Key makes a retry return the stored result instead of mailing twice.

```bash
curl -s -X POST https://cooperemail.com/api/v1/inboxes/$INBOX_ID/drafts/$DRAFT_ID/send \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Idempotency-Key: quote-warehouse-1-send"
```

send_at is an ISO-8601 time within 366 days. It adds the scheduled label and sets send_status to scheduled. PATCH send_at null unschedules. DELETE cancels. While send_status is sending, edit, delete, and a second send return HTTP 409 draft_sending.

Due drafts flush every minute. Vercel Cron and the inbound worker both call GET /api/v1/internal/drafts/flush. Ordinary API traffic flushes them too, at most once every 30 seconds on each instance. A row claim sends each draft once.

After the draft is a message, find it with GET /api/v1/search. Inbound mail is separate: a stored inbound message still fires message.received. Draft webhooks are draft.created, draft.approved, draft.rejected, and draft.sent.

## Approval mode holds the send

send_mode is direct or approval. direct is the default. Set approval when a verified owner must sign off:

```bash
curl -s -X PATCH https://cooperemail.com/api/v1/inboxes/$INBOX_ID \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"send_mode":"approval"}'
```

Approval holds POST /api/v1/inboxes/:id/messages, reply, reply-all, and forward as a pending draft and emails verified owners Approve and Reject links. No verified owner is HTTP 409 owner_required, and send_mode stays direct.

An API key can return the inbox to direct only when it has never had a verified owner. An unsubscribed owner still blocks that switch. Leaving approval emails verified owners, returns HTTP 202, and stays on approval until POST /api/v1/inboxes/send-mode/confirm. PATCH /api/v1/inboxes/batch uses the same rule.

Changing recipients, subject, body, or attachments sets approval_status back to pending and emails the owners again. Cooper hashes recipients, subject, body, attachments, and the reply target, and checks that hash on claim and on send. A mismatch is not sent. Labels and send_at do not change the hash. A draft scheduled while send_mode was direct waits for approval of that exact content.

GET on an approve or reject link shows a confirm page. POST records the decision. Opening the link does not approve. The copy in the agent inbox has the action link removed, and an API key cannot submit POST /api/v1/drafts/approve or POST /api/v1/drafts/reject. Approval, leave-approval, inbox-delete, and owner-confirm mail set Reply-To to noreply@cooperemail.com.

An expired Approve link says a new approval email was sent. The replacement goes to the verified owner, at most one every 10 minutes and at most 3 per day. The stored copy still omits the new link.

DELETE of a verified or unsubscribed owner on an approval inbox returns HTTP 409 owner_required. DELETE of the inbox returns HTTP 202 until the owner confirms, and recreating that username stays in approval mode. POST .../drafts/:draftId/send on an approval inbox returns the pending draft with HTTP 202 until the owner approves.

## draft_write, draft_send, and message_send

The 2026-10-13 changelog adds draft_write and draft_send.

draft_write creates, edits, and deletes drafts and does not send mail. draft_send sends an existing draft and does not create, edit, or delete one. message_send still creates and sends drafts. POST /messages, reply, reply-all, and forward stay on message_send. message_delete can still delete a draft. draft_write alone cannot send.

GET drafts requires message_read. POST and PATCH require message_send or draft_write. DELETE requires message_delete, message_send, or draft_write. POST .../drafts/:id/send requires message_send or draft_send.

permissions null is full access. An unknown name is HTTP 400 invalid_permission with param permissions. A missing permission is HTTP 403 permission_denied. param is the primary permission name, and the message names the alternatives. A writer key that calls send gets:

```json
{
  "error": {
    "type": "authentication_error",
    "code": "permission_denied",
    "message": "This API key does not include the message_send or draft_send permission.",
    "param": "message_send",
    "docs_url": "https://cooperemail.com/docs#permission_denied"
  }
}
```

Mint from an orchestrator key that holds key_write. POST /api/v1/keys and POST /api/v1/inboxes/:id/keys both take a permissions array. The MCP server does not mint keys. A restricted caller can grant only a subset of its own names.

```bash
curl -s https://cooperemail.com/api/v1/inboxes/$INBOX_ID/keys \
  -H "Authorization: Bearer $COOPER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"quote-writer","permissions":["message_read","draft_write"]}'
```

HTTP 201 returns the secret once, in key. Give it to the composer. Keep draft_send or message_send on the sender. Another inbox is HTTP 404 inbox_not_found. Pinning is covered in [Scoped API keys for AI agents](https://cooperemail.com/blog/agent-email-scoped-api-keys-sub-agents).

## Six MCP tools, same rules

[https://cooperemail.com/mcp](https://cooperemail.com/mcp) exposes cooper_create_draft, cooper_get_draft, cooper_list_drafts, cooper_update_draft, cooper_delete_draft, and cooper_send_draft. Each tool uses the same permission as REST. The catalog stays at 64 tools, and the server version stays 1.5.0. cooper_update_inbox sets send_mode. Approval with no verified owner is HTTP 409 owner_required.

```json
{
  "inbox_id": "inb_…",
  "to": ["ada@example.com"],
  "subject": "Quote for the warehouse job",
  "text": "The quote is ready for review.",
  "client_id": "quote-warehouse-1"
}
```

Pass that object to cooper_create_draft. Pass inbox_id and draft_id to cooper_send_draft, with optional idempotency_key. On an approval inbox the tool returns the pending draft. cooper_list_drafts lists what this key can see. cooper_delete_draft cancels a schedule. cooper_update_draft with send_at null unschedules.

## Put the keys on different workers

Onboard with POST https://cooperemail.com/api/v1/onboard and store api_key outside the transcript. Confirm an owner, then set send_mode to approval when a human must approve each send. Mint message_read plus draft_write for the writer, and keep draft_send or message_send on the sender. The writer calls cooper_create_draft. The sender calls cooper_send_draft, or waits for the owner. Watch draft.created, draft.approved, draft.rejected, and draft.sent. Keep message.received for inbound mail, and find sent mail with GET /api/v1/search.

AgentMail, Resend, and the Gmail API are other mail paths. This page is the Cooper draft, the schedule, and the approval gate. Free is $0 for 5 inboxes and 5,000 emails/month. Starter is $12/month. Pro is $99/month. Prices: [https://cooperemail.com/pricing](https://cooperemail.com/pricing).

Cooper Email is built by Avatar 8 LLC. Questions: ops@avatar33.com.

---

*Originally published on [Cooper Email](https://cooperemail.com/blog/agent-email-drafts-owner-approval).*

