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 modeWhat happens to a send
sendSamva queues it for sending within your account's limits. This is the default.
approvalIt 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"
}
StateMeaning
approvalRequiredA permitted person must decide. No email has been accepted.
queuedSending is authorized and may be waiting or running. No email has been accepted.
acceptedSamva accepted the email for delivery. Its mailbox sent item may still be catching up.
completedThe accepted email's sent item and mailbox event are recorded. Recipient delivery is a separate outcome.
notSentThis action cannot send. Its draft is editable again.
uncertainSamva 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:

read-send-action.ts
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.

require-approval-policy.ts
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.

Next steps

Related documentation

On this page