Recipes

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

NeedWhy
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_UNIBOXTo list drafts
WRITE_UNIBOX on the same keyTo 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.

FieldWhat it holds
idThe draft id you approve or discard
thread_idThe conversation. Read it with GET /unibox/thread to show the reviewer the context
email_account_idThe mailbox the reply will be sent from
to_addr, subjectRecipient and the subject the reply will carry
bodyThe suggested reply, plain text
campaign_id, contact_idSet when the thread belongs to a campaign and a known contact
intent_class, confidenceHow the agent classified the reply it answered
modelThe model that wrote it
statusAlways pending on this list
groundingWhat 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.

kindMeaning
link_not_allowedA link the brain does not give
address_not_allowedAn email address the brain does not give
phone_not_allowedA phone number the brain does not give
unverified_figureA price or figure the brain does not give
internal_figureA figure taken from your internal rules
internal_phraseA run of words quoted from your internal rules
do_not_sayA 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:

  1. Approve without acknowledge_flags.
  2. On 409 with code: "draft_flagged", show the returned flags to the reviewer.
  3. When the reviewer confirms, approve again with "acknowledge_flags": true and a new Idempotency-Key (or none). Reusing the first key with a different body answers 409.
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"].

EventWhenWhat to do
AI_DRAFT_READYThe agent stored a new draft. Carries thread_id and draft_idRefetch GET /unibox/agent-drafts
AUDIT_CREATED with entity_type: "unibox"Someone approved or discarded a draftRefetch, 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.

On this page