Recipe: review and approve AI reply drafts
List the AI reply drafts waiting in the inbox, read what each one used and flagged, then approve, edit or discard them from your own app with the unibox API.
You build a human-in-the-loop approval queue for AI sales replies with three unibox endpoints. The inbox agent keeps at most one pending draft per thread; your code lists them with GET /unibox/agent-drafts, shows each draft's grounding and flags to a reviewer, and sends the chosen one with POST /unibox/agent-drafts/:id/approve. A draft that carries flags answers 409 draft_flagged to an API key until the call repeats with "acknowledge_flags": true.
An agent draft is a suggested reply the inbox agent writes when a person answers one of your campaigns. It is stored, not sent. Approving is the only path that sends it, and it goes out through the normal reply path from the draft's mailbox (email_account_id), threaded to the prospect's reply.
Before you start
| Need | Why |
|---|---|
| Inbox agent turned on (Settings, Workspace, Draft replies for me) | Without it no agent drafts are written. It is a paid feature that costs 5 AI credits per drafted reply |
An API key with READ_UNIBOX | To list drafts |
WRITE_UNIBOX on the same key | To approve or discard. Approving sends mail |
WRITE_CAMPAIGNS (optional) | To see flags that quote your internal rules, see step 2 |
REALTIME_SUBSCRIBE (optional) | To hear about new drafts instead of polling, see keep it live |
Requests below use https://api.ayinbox.com/v1 and a key in the Authorization header. Scopes and how to grant them are in permissions.
1. List drafts
curl -s "https://api.ayinbox.com/v1/unibox/agent-drafts" \
-H "Authorization: Bearer aybox_..."The answer is {"data": [...]}: the workspace's pending drafts, newest first, up to 100. The route takes no cursor or limit, and a draft leaves the list as soon as it is approved or discarded.
| Field | What it holds |
|---|---|
id | The draft id you approve or discard |
thread_id | The conversation. Read it with GET /unibox/thread to show the reviewer the context |
email_account_id | The mailbox the reply will be sent from |
to_addr, subject | Recipient and the subject the reply will carry |
body | The suggested reply, plain text |
campaign_id, contact_id | Set when the thread belongs to a campaign and a known contact |
intent_class, confidence | How the agent classified the reply it answered |
model | The model that wrote it |
status | Always pending on this list |
grounding | What the draft read, present when a campaign or brain applied |
grounding names what the draft read and never holds the source text. brains lists the campaign brain (with its revision), campaign names the campaign and sequence, fields lists the brain fields used, and sources lists up to 40 sources the draft read (material, pages and playbooks) with a title, character count and why each was picked. read and left_out count the brain's material only: how many sources it read and how many it left out, and budget_chars is the reading budget. Show this as a "used" line next to the draft so a reviewer knows which pricing page or playbook the answer leaned on. The full shape is in draft grounding.
2. Show the flags
After a draft is written, AY Inbox checks it against the brain and records anything the brain does not back as grounding.flags. Each flag has a kind, the match copied from the draft body, a sentence you can show as is, and from_inbound: true when the draft repeats something from the prospect's own email.
kind | Meaning |
|---|---|
link_not_allowed | A link the brain does not give |
address_not_allowed | An email address the brain does not give |
phone_not_allowed | A phone number the brain does not give |
unverified_figure | A price or figure the brain does not give |
internal_figure | A figure taken from your internal rules |
internal_phrase | A run of words quoted from your internal rules |
do_not_say | A phrase on your do-not-say list |
What a caller sees depends on its scope. A key without WRITE_CAMPAIGNS (or whose member lacks the manage campaigns permission) gets unverified_figure in place of internal_figure and no internal_phrase at all, so internal rules never leak to a reader who may not see them. Render the flags as badges beside the send button. The check is a heuristic: it catches specific mistakes, misses some and over-flags others, so treat it as a prompt for the reviewer, not a verdict. See flags.
3. Edit before approving
To send a changed reply, pass the reviewer's text as body on approve. An empty or missing body sends the draft as written.
{ "body": "Hi Dana,\n\nTuesday at 10 works. I'll send an invite.\n\nSam" }When the text you send differs from the stored draft, AY Inbox records that the draft was edited. The campaign's auto-reply panel reads that record in Supervised mode, where it reports how many answers went out as written and how many were edited or discarded. Editing does not clear flags: an edited body on a flagged draft still needs the acknowledgement in step 4.
4. Approve or discard
Approve with an Idempotency-Key so a retry after a timeout cannot send twice:
curl -s -X POST "https://api.ayinbox.com/v1/unibox/agent-drafts/$DRAFT_ID/approve" \
-H "Authorization: Bearer aybox_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: approve-$DRAFT_ID-1" \
-d '{}'A success returns the queued send (task_id, scheduled_at, send_mode). If the draft has flags the caller may read, the call is held instead and the draft stays pending:
{
"error": "Conflict",
"code": "draft_flagged",
"message": "This draft has flags. Read them, then approve again with acknowledge_flags set to true and a new Idempotency-Key (or none).",
"flags": [{ "kind": "link_not_allowed", "match": "https://example.net/x", "from_inbound": true, "sentence": "Repeats a link from their email. Check it before sending." }],
"request_id": "req_8f3a1c2b9d"
}The flow your code should follow:
- Approve without
acknowledge_flags. - On
409withcode: "draft_flagged", show the returnedflagsto the reviewer. - When the reviewer confirms, approve again with
"acknowledge_flags": trueand a newIdempotency-Key(or none). Reusing the first key with a different body answers409.
curl -s -X POST "https://api.ayinbox.com/v1/unibox/agent-drafts/$DRAFT_ID/approve" \
-H "Authorization: Bearer aybox_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: approve-$DRAFT_ID-2" \
-d '{"acknowledge_flags": true}'Only API key and OAuth callers are held. A signed-in member approving in the dashboard sees the same flags as badges and is never blocked. A caller without WRITE_CAMPAIGNS is held only on the flags it can read, so a draft whose only flag is an internal_phrase is not held for it.
Other answers to handle: 404 when the draft does not exist in this workspace, and 409 "this draft was already handled" when a teammate approved or discarded it first. Two approvals can never both send, because the first one claims the draft before sending.
To close a draft unsent:
curl -s -X POST "https://api.ayinbox.com/v1/unibox/agent-drafts/$DRAFT_ID/discard" \
-H "Authorization: Bearer aybox_..."It answers {"status": "discarded"}, or 404 when the draft is gone or already handled. All error bodies follow the envelope in error codes.
5. Draft on demand
The agent only drafts on inbound human replies. To get a draft for any thread yourself, call POST /unibox/reply/draft with thread_id and an optional instruction:
curl -s -X POST "https://api.ayinbox.com/v1/unibox/reply/draft" \
-H "Authorization: Bearer aybox_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: draft-thread-42-1" \
-d '{"thread_id": "...", "instruction": "propose Tuesday at 10"}'It returns text, credits_charged, credits_remaining, model and, when a campaign or brain applied, the same grounding and flags. It never sends and is not stored as an agent draft, so send the reviewed text with POST /unibox/reply. It costs at least 2 AI credits, plus a usage-based amount when the call uses many tokens, for example with a large brain. An Idempotency-Key pays for one draft, a retry replays the first answer, and a brain that cannot be read answers 503 brain_unavailable before anything is charged.
An agent built on agent tools or MCP uses generate_reply_draft (READ_UNIBOX and AI_DRAFT), which requires an idempotency_key argument. A call without one answers 422 idempotency_key_required, and a key whose draft succeeded will not draft again, so use a new key for each new draft. The same surfaces offer list_agent_drafts and discard_agent_draft. There is no tool that approves: sending stays with a person or with your own code calling the approve route.
Keep it live
Instead of polling the list, open a realtime socket with a key holding REALTIME_SUBSCRIBE and READ_UNIBOX, join org:<org_id>, and narrow the stream with intents such as ["AI_DRAFT", "AUDIT"].
| Event | When | What to do |
|---|---|---|
AI_DRAFT_READY | The agent stored a new draft. Carries thread_id and draft_id | Refetch GET /unibox/agent-drafts |
AUDIT_CREATED with entity_type: "unibox" | Someone approved or discarded a draft | Refetch, so the draft leaves every reviewer's queue |
For how drafts are written and why a person stays in the loop, see AI replies to cold email. The endpoint reference is unibox.
Frequently asked questions
Does the inbox agent send replies on its own?
No. The inbox agent only writes a pending draft. A reply goes out when a person in the dashboard, or your code with a WRITE_UNIBOX key, calls approve. Sending without approval is a separate per-campaign opt-in called auto-reply.
Can an API key limited to some mailboxes review agent drafts?
Yes, for its own mailboxes only. GET /unibox/agent-drafts leaves out drafts from mailboxes the key is not allowed, and approving one of those is refused.
What happens if the send fails after I approve a draft?
The draft goes back to pending, so it is not lost and you can approve it again. The error from the send path is returned to your call.
Why does a draft have no grounding object?
Grounding is only attached when a campaign or campaign brain applied to the draft. A draft without it carries no flags, so approving it never answers 409 draft_flagged.
How long does a pending agent draft wait?
Until someone approves or discards it. Turning the inbox agent off stops new drafts but leaves waiting ones in place, and a calendar booking draft replaces whatever draft was pending on that thread.
Recipe: get notified of positive replies
Receive a signed webhook when a prospect replies to a campaign, read its intent and confidence, and route interested replies to Slack, a CRM or your code.
Recipe: book a meeting from a reply
Use the booking API to offer open Cal.com or Calendly times in a reply, detect the prospect's pick and book it, step by step with exact routes and bodies.