Approve sends
Hold an agent's or person's sends until a different person approves or denies them, and react to the pending event.
A mailbox send records an action you can follow until Samva accepts the email or determines that it was not sent. A send mode or mailbox policy can require a person's approval first. You can change the send mode without issuing a new key.
| Send mode | What happens to a send |
|---|---|
send | Samva queues it for sending within your account's limits. This is the default. |
approval | It waits for a person other than the sender to approve it. |
Set the mode on a grant, on a mailbox-scoped key, or when you install an extension. The same mailbox can let a person send directly while an agent's replies wait for review.
The receipt's decision.reason identifies the restriction: an extension installation, an API
key's send mode, its owner's mailbox member grant, or a mailbox policy. More than one restriction
can require approval. Treat the reason as explanatory text, not a machine-readable error code.
Read a send receipt
Send, reply, reply to all, forward, and draft send return 202 with a receipt when work is waiting
or queued. The response does not mean the email has been sent. Read state and preserve actionId.
{
"actionId": "mbxact_...",
"draftId": "mdraft_...",
"threadId": "thread_...",
"state": "approvalRequired",
"messageId": null,
"acceptedAt": null,
"waitReason": null,
"reviewExpectedAt": null,
"skippedRecipients": [],
"executeUntil": null,
"retryAt": null,
"problem": null,
"retryEligible": false,
"decision": {
"outcome": "requireApproval",
"reason": "This installation requires approval.",
"policyId": null,
"source": "installation",
"decidedAt": "2026-10-07T14:05:00.000Z"
},
"createdAt": "2026-10-07T14:05:00.000Z",
"updatedAt": "2026-10-07T14:05:00.000Z"
}| State | Meaning |
|---|---|
approvalRequired | A permitted person must decide. No email has been accepted. |
queued | Sending is authorized and may be waiting or running. No email has been accepted. |
accepted | Samva accepted the email for delivery. Its mailbox sent item may still be catching up. |
completed | The accepted email's sent item and mailbox event are recorded. Recipient delivery is a separate outcome. |
notSent | This action cannot send. Its draft is editable again. |
uncertain | Samva cannot yet establish the outcome safely. Keep the action and contact support; do not send a replacement. |
messageId and acceptedAt stay null until acceptance is established. draftId and threadId
can be null. skippedRecipients lists recipients omitted from the accepted email and why.
waitReason and reviewExpectedAt describe any delivery wait after acceptance; they can be null.
executeUntil is the sending deadline, and retryAt is the next planned attempt, each an ISO 8601
instant or null. problem contains code and message, or is null. When a typed sending
error caused the wait or refusal, problem.error retains its _tag and structured fields.
decision explains why the action was allowed, held, or denied. Its source is grant,
installation, policy, or null; policyId is null when no policy supplied the decision.
The whole decision can be null. Use this attribution rather than assuming every hold is a policy.
createdAt, updatedAt, and a decision's decidedAt are ISO 8601 instants.
Follow an action
Read GET /v1/mailboxes/{id}/actions/{actionId} or use the SDK:
const action = await samva.mailboxes.getAction({ id: mailboxId, actionId: receipt.actionId });
if (action.type === "sendMessage" || action.type === "reply") {
console.log(action.state, action.send.messageId, action.send.problem);
}Send and reply actions expose their receipt as send and their intent, the frozen recipients, sender, subject,
body, attachments, and reply context being sent. intent can be null when the original content
requires reconciliation. An attachment uses the same public attachment
shape as an email send. Changes to mailbox settings do not change an approved email. Other action
types have their own states; handle type before interpreting a send state.
The associated draft is locked with lockedByActionId while approval, sending, or reconciliation
is pending. Editing it cannot change the held intent. Denial, cancellation, or a definite unsent
outcome reopens it; acceptance marks it sent. Changed content requires a new action and any required
approval.
Subscribe to mailbox.action.pending_approval to learn about held sends, or list actions to find
state approvalRequired. See Events.
Approve or deny
Approve with POST /v1/mailboxes/{id}/actions/{actionId}/approve. It returns 202 with the action
while the send is pending and queues the frozen email; approval is not a sent confirmation.
Accepted, completed, and notSent replays return 200. Non-send approvals execute synchronously
and return 200 with their own execution states. Read the send receipt to follow progress.
Repeating the same permitted approval observes the same action rather than sending another email.
curl -X POST https://api.samva.dev/v1/mailboxes/$MAILBOX_ID/actions/$ACTION_ID/approve \
-H "Authorization: Bearer $SAMVA_OAUTH_TOKEN" \
-H "x-org-slug: acme"Deny with POST /v1/mailboxes/{id}/actions/{actionId}/deny and a reason. It returns 200 with the
action. Nothing is sent, and the author's draft becomes editable.
curl -X POST https://api.samva.dev/v1/mailboxes/$MAILBOX_ID/actions/$ACTION_ID/deny \
-H "Authorization: Bearer $SAMVA_OAUTH_TOKEN" \
-H "x-org-slug: acme" \
-H "Content-Type: application/json" \
-d '{ "reason": "Wrong recipient." }'Both need a person with an admin grant on the mailbox. Nobody approves or denies their own
proposal. API keys and extensions cannot decide. A conflicting decision returns 409.
The hosted MCP tools cannot approve or deny.
Automatic waits and the sending deadline
Approval holds have no automatic expiry. Allowing or approving an action starts a 15-minute
sending window. executeUntil records its fixed deadline; retries and repeated approvals never
extend it. A send can have a shorter effective window, so follow the reported deadline.
Samva waits automatically for sender readiness, rate limits, a temporary mailbox pause, temporary service dependencies, or genuinely pending account review within that window. A request for more information, a requirement to reapply, rejection, revoked permission, or a payment requirement stops the unsent action. Fixing those conditions does not silently send it later. An ambiguous outcome needs reconciliation; it is not permission to submit again.
The deadline bounds acceptance, not delivery. An accepted email can still wait for delivery review and later delivery events. See Mailbox events.
Cancel or retry
A permitted person who proposed or approved the send can cancel before acceptance with
POST /v1/mailboxes/{id}/actions/{actionId}/cancel. It returns 200 with a notSent receipt. A held send
can also be cancelled. Once accepted, the email cannot be withdrawn: cancellation returns 409.
Retry uses POST /v1/mailboxes/{id}/actions/{actionId}/retry and returns 202 with a receipt.
It continues the original action only when retryEligible is true, its prior attempt has ended,
and its original deadline still permits sending. It never extends that deadline. An ordinary wait
needs no Retry request. API keys and extensions cannot cancel or Retry; these operations require a
permitted person. Retry cannot resubmit notSent or resolve uncertain.
Disabling or uninstalling the proposing extension cancels its definitely-unsent queued sends, even after a person approved them. Accepted emails cannot be withdrawn that way.
Recover a lost response
Keep the original Idempotency-Key and body. Repeating them returns the same action's persisted
phase; using the key with a different body returns 409. Once you know actionId, read the action
to learn the outcome. An accepted or completed replay returns 200 with the accepted identity.
A queued, held, or uncertain replay returns 202. Do not invent a new key to recover a timeout or
an uncertain outcome: a new key is a new intentional send and can send another email.
An active action keeps its key binding through resolution. Terminal actions retain it for at least 24 hours after their terminal outcome. After that retention expires, reuse can create a new action. Save the action ID independently of the request key.
Send failures
Before an action is recorded, a request can return a structured API error. Branch on _tag and
follow the Error reference. If an error includes a receipt,
keep its actionId and observe that action before submitting anything again.
After proposal or approval, sending failures appear in the same action's problem and state.
Temporary conditions can leave it queued with retryAt while Samva waits within the fixed
executeUntil deadline. A definite unsent outcome becomes notSent; an ambiguous outcome becomes
uncertain and needs reconciliation. Repeating approval observes the same action. Follow
Cancel or retry rather than creating a replacement send to recover.
Send-time RateLimitedError and SenderNotReadyError leave the action queued; use retryAt
to see when Samva will check again. OnboardingReviewRequiredError waits only for an undecided
automated or manual review whose nextAction is wait_for_review. Other review decisions stop
the action as notSent. Inspect problem.error for the original review code, decision,
stage, nextAction, and identityId when supplied. Deadline expiry retains the last typed
error under problem.error. These conditions do not change an admitted command's 202 into
a synchronous 429, 503, or review 403. Request throttling before admission still returns
429; validation returns 422, and mailbox policy denial returns 403 with its receipt.
Hold or refuse sends with a policy
A mailbox policy applies to every send from the mailbox, whoever proposes it. An enabled policy with
outcome requireApproval holds each send for a person; one with outcome deny refuses it, and
deny wins when both apply. Policies are part of configuring the mailbox, which needs an admin
grant.
const policy = await samva.mailboxes.createPolicy({
id: "mbx_...",
name: "Review every send",
outcome: "requireApproval",
reason: "A person reviews outbound mail from this mailbox.",
});
// Stop it applying without deleting it.
await samva.mailboxes.updatePolicy({ id: "mbx_...", policyId: policy.id, enabled: false });Deleting a policy does not release sends it already holds: they stay held until a person approves or denies them. See create a policy for the limit on policies per mailbox.