Blog ·
Agent email drafts: let your agent write, a human approve, then send
POST /api/v1/inboxes/:id/drafts saves mail without sending. POST .../drafts/:draftId/send deletes the draft and returns a message. send_at schedules it (send_status scheduled). PATCH send_mode approval holds POST /messages, reply, reply-all, and forward until a verified owner approves. draft_write creates, edits, and deletes drafts and does not send. draft_send sends an existing draft only. A missing permission is HTTP 403 permission_denied. MCP: cooper_create_draft, cooper_get_draft, cooper_list_drafts, cooper_update_draft, cooper_delete_draft, cooper_send_draft.
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. 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. Routes: https://cooperemail.com/docs. Hosted MCP: https://cooperemail.com/mcp (64 tools). Notes: 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".
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"}'
{
"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.
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:
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:
{
"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.
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.
Six MCP tools, same rules
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.
{
"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.
Cooper Email is built by Avatar 8 LLC. Questions: ops@avatar33.com.