Questions · Updated 2026-09-28

What does approval_required mean?

approval_required means the send was held for a person to decide, not refused. The error carries action_id; GET /agent-actions lists the held row, and a person releases it with POST /agent-actions/{id}/approve or closes it with POST /agent-actions/{id}/reject.

approval_required means this send is waiting for a person and has not failed. Its error catalogue message, "This action requires human approval before it executes.", describes a hold, and retryable is false because a decision releases it, not time. The error carries action_id, which names the held row at GET /agent-actions. A person sends it with POST /agent-actions/{id}/approve or closes it with POST /agent-actions/{id}/reject; the key that met the error can do neither.

What holds a send today

Today the loop guard is what holds a send: a candidate that would be the 4th near-identical message to one recipient inside 60 minutes. When the guard holds a send, message is the guard's own sentence, naming the count and the window, and details carries the evidence it used; the fix stays the catalogue's. An agent on MCP can also put a send into the same queue on purpose with request_approval, before attempting it. What the guard measures, including the recipient-collapse signal, is on loop detection.

The fix, and what to call

The catalogue fix says "Do not send it again: a person approves or rejects it in the console under Agents → Approvals, and approving sends it — the message then appears in GET /emails. action_id in this error names the held send; the key that asked cannot approve itself, and a retry waits on the same approval."

GET /agent-actions lists the queue; its summary is "Every held agent action, newest first — nothing waits invisibly. Filter by state to read the inbox or the audit trail." Filter with state=pending and match id to your action_id. The row's preview.held_reason says which check held it, and decided_by, decision_reason and decided_at are null until someone decides. Reading the queue takes a full_access key; a sending_access key is answered restricted_api_key. Over MCP, list_agent_actions reads the same rows.

curl -sS -X POST https://api.agentisend.com/agent-actions/9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42/approve \
  -H "Authorization: Bearer $AGENTISEND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"
201
{
  "action": {},
  "message_id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
  "status": "string"
}
Response

The approve response carries message_id, the id the message has in GET /emails from then on. Rejecting is POST /agent-actions/{id}/reject with a reason, and the summary says the reason is preserved with the row. Both are a person's calls: a full_access key is answered human_action_required, and a sending_access key restricted_api_key.

While it waits

Do not send it again. retryable is false, so waiting changes nothing, and a resend from the same key does not create a second row: the guard matches it to the pending action and answers with the same action_id, so the queue cannot fill with copies of one message. Reworded content is a new request and, if the guard still matches it, a new held row. Do not report it to anyone as a failure: nothing was sent and nothing was dropped, and the Approvals guide says an action left in the queue stays in the queue.

To hear the decision without polling, register a webhook with POST /webhooks for agent.approval_requested, agent.approved and agent.rejected.

Next