Insights · 22 October 2026
Approval gates for agent email: keep the send button shared
Drafting is cheap and reversible. Sending is neither. This is the case for making an agent ask before it writes to someone new, and a working setup you can copy.
Most of what an email agent does can be undone. A wrong label comes off. A bad draft gets rewritten. A misread message gets read again. The one step with no undo is the send. Once a message reaches someone else's server, it belongs to them, along with whatever the agent put in it.
That is why the send deserves different treatment from everything else a bot does. Agents make mistakes for ordinary reasons. A model fills in a recipient from the wrong part of a thread. An instruction hidden in an inbound email convinces it that forwarding a file to a stranger is part of the task. A retry loop sends the same apology forty times. None of these need a malicious operator. They need only a bot that can send to anyone, at any volume, without anyone looking.
An approval gate puts a second party on the send button for exactly the cases that carry risk, and stays out of the way for the rest.
Decide what counts as inside
A gate that stops every message trains people to click approve without reading. The useful version separates routine sends from unusual ones and only stops the unusual ones. On EMLbot that boundary is the key's allow_recipients list. Each entry is an exact address, a whole domain written as @yourco.com, or team, which matches any inbox hosted on EMLbot.
Recipients on the list are inside. The bot can write to them directly, within its daily cap. Anyone else is outside. What happens to an outside send depends on the key's approval setting. With "outside" the send is parked for review. With the default, "none", it is refused with 403 forbidden naming the address.
One detail catches people out. If allow_recipients is empty, the key has no recipient limit at all, everything counts as inside, and the approval setting never triggers. Any key that can send should have an allowlist, even a short one.
Start narrow. For a support bot, inside might be your own domain and your other bots. For a bot that answers inbound leads, inside might be nobody, so every reply waits for a person until you trust it. You can widen the list later by issuing a new key, and you will have the approval history to justify it.
Set up the two keys
The gate needs two credentials: the bot's key, which can send, and an approver's key, which can release parked sends. Keep them apart. A key cannot approve its own parked send even if it holds both scopes, but separate keys also mean separate audit entries, so you can always tell who released what.
# The bot: may send inside the allowlist, parks everything else
curl -X POST https://api.emlbot.com/v0/keys \
-H "Authorization: Bearer $EMLBOT_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "leads-bot",
"scopes": ["read", "label", "send"],
"inbox_ids": ["leads@bots.emlbot.com"],
"allow_recipients": ["@yourco.com"],
"daily_send_cap": 50,
"approval": "outside"
}'
# The approver: can see and release parked sends for that inbox only
curl -X POST https://api.emlbot.com/v0/keys \
-H "Authorization: Bearer $EMLBOT_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "leads-approver",
"scopes": ["read", "approve"],
"inbox_ids": ["leads@bots.emlbot.com"]
}'A person does not need a key at all. Owners and admins signed in to the console at app.emlbot.com can review and decide approvals there. The approver key is for when you want a script, or a reviewer bot, to do it.
What the bot sees when a send is parked
When the leads bot replies to a prospect at an outside address, the call returns 202 instead of 200:
{"status": "pending_approval", "approval_id": "apr_..."}The bot's instructions should cover this case explicitly. It should record the approval_id, label the source message (for example awaiting-approval) and move on. It should not retry. Every retry creates another parked send, and the approver ends up with duplicates to reject. If your agent framework retries tool calls automatically, treat 202 as success so it does not.
The same moment produces a message.approval_requested event in the event log and on any webhook subscribed to it. The payload carries the pending send's recipients, subject, a short preview of the body and attachment names. Point a webhook at your own small service and post that summary wherever your reviewers already work, so nobody has to remember to check a queue.
Reviewing and deciding
The approver lists what is waiting, reads it, and decides:
curl "https://api.emlbot.com/v0/approvals?status=pending" \
-H "Authorization: Bearer $EMLBOT_APPROVER_KEY"
curl -X POST "https://api.emlbot.com/v0/approvals/$APPROVAL_ID/approve" \
-H "Authorization: Bearer $EMLBOT_APPROVER_KEY"
curl -X POST "https://api.emlbot.com/v0/approvals/$APPROVAL_ID/reject" \
-H "Authorization: Bearer $EMLBOT_APPROVER_KEY"Approving performs the send there and then, and it counts against the requesting bot's daily cap, so a pile of approvals cannot be used to get around the cap. A decision is final. A second approve or reject on the same item returns 409 conflict, which also means two reviewers clicking at once cannot send a message twice. Every decision is written to the audit log as approval.approve or approval.reject, with who made it and when.
When you review, look at the recipient first. Most bad sends are bad because of who they go to. Then check the attachments, then the body. If the bot is asking to write to an address that appeared only inside an inbound message, that is the pattern prompt injection produces, and it is worth a closer look.
The daily cap is a second gate
The allowlist controls who. The cap controls how much. Every key has a daily_send_cap, counted per UTC day, defaulting to 200 when you do not set one. Each send response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and when the cap is spent sends return 429 rate_limited with Retry-After.
Set the cap to what the job needs, not to what feels generous. A bot that answers twenty inbound questions a day does not need a cap of 500. A tight cap turns a runaway loop into a short, visible incident. Have the bot read X-RateLimit-Remaining and stop on its own when it gets low, and treat a 429 as a reason to alert a person.
Can the approver be a bot?
Yes, with care. A reviewer bot with the approve scope can check parked sends against rules you write down: the recipient replied to us first, there are no attachments, the body contains no prices. Running the reviewer on a different model or platform from the sender helps, because the two are less likely to share the same blind spot. It cannot release its own sends, because the API refuses that.
Keep a person in the loop for first contact with people outside your organisation, for anything with an attachment, and for anything that commits money or dates. Have the reviewer bot approve only the clear cases and leave the rest pending for a human.
One more rule: never give a bot the admin credential. The admin token is a break-glass key, and it is not subject to send policy, so a bot holding it has no gate at all.
Where to go next
The quickstart walks through keys and inboxes from scratch, and the errors page lists every status code a send can return. The approval gate is one layer of a wider defence against hostile mail, covered in prompt injection by email. For what EMLbot enforces on its side, see security.
Get access
EMLbot is in early access. Email info@emlbot.com with what your bots send, or see pricing.