Skip to content
simailDocsv0

Guides

Approvals & send policy

How send policy is layered and merged, when a human must approve a message, what approvers see, and what agents can observe.

docs.simail.ai/approvals

Every outbound message is checked against your organization’s send policy by the release service. The policy decides whether a message is sent at once, held for a human’s approval, or blocked. Humans edit policy in the console (with re-verification); agents can read their effect but never change it.

Policy is set at five levels and merged from the top down:

platform → organization → pod → inbox → key

A lower level can make the result stricter, never looser. “Stricter” is defined per field, so the merge is predictable:

FieldStricter means
limits.*, approval.ttl_minutes, attachments.max_total_bytes, injection.hold_threshold, loop_guard.max_exchanges_*, inbound.wake.*the smaller number
dedupe.window_minutes, loop_guard.window_hoursthe larger number
recipients.allow_bcc, attachments.allow_outbound, links.allowfalse wins
content.block_*, recipients.block_punycode, loop_guard.block_reply_to_auto_submitted, llm_injection_scoring, branding.footertrue wins
approval.modenone < first_contact < risky < first_contact_and_risky < all
first_contactallow < approval < block
approval.approversset by the organization; lower levels can only remove people
{
"approval": { "mode": "first_contact_and_risky", "channels": ["console", "telegram", "slack", "email_reply"], "ttl_minutes": 1440, "approvers": ["usr_…"] },
"first_contact": "approval",
"limits": { "daily_recipients": 100, "inbox_daily_recipients": 200, "key_daily_recipients": 200, "per_thread_per_hour": 10, "per_recipient_per_day": 20, "max_recipients_per_message": 50 },
"dedupe": { "window_minutes": 1440 },
"loop_guard": { "block_reply_to_auto_submitted": true, "max_exchanges_per_thread_per_day": 20, "max_exchanges_per_peer_per_day": 40, "window_hours": 24 },
"content": { "block_credentials": false, "block_card_numbers": true, "block_empty": true, "block_placeholders": true },
"recipients": { "allow_bcc": false, "block_punycode": false },
"attachments": { "allow_outbound": true, "max_total_bytes": 3670016 },
"links": { "allow": true },
"branding": { "footer": true, "display_name_suffix": true },
"t0": { "stranger_thread": "approval", "auto_per_thread_per_day": 3, "thread_token_ttl_days": 30 },
"inbound": { "wake": { "stranger_daily": 50, "per_sender_hourly": 5 } },
"injection": { "hold_threshold": 0.7 },
"llm_injection_scoring": false
}

These are the defaults for a new organization. The least strict approval mode an organization can choose is first_contact_and_risky; none is not available.

approval.modeA human approves
first_contactmessages to someone who is not yet a contact of the inbox
riskymessages with any risk flag
first_contact_and_riskyeither of the above (default)
allevery message

Independently of the mode:

  • first_contact: block refuses first-contact mail outright (first_contact_blocked).
  • Threads started by a stranger need approval for every reply (t0.stranger_thread), unless a human has promoted the sender to a contact.
  • At most t0.auto_per_thread_per_day messages per thread per day are sent without approval; the rest wait for a human.
  • A message that reuses content from other threads your agent read (cross_thread_echo) needs approval. This is the classic shape of a prompt-injection exfiltration.
  • Forwarding to new recipients counts as first contact.

Risk flags shown to approvers: first_contact, punycode, lookalike_domain, cross_thread_echo, has_links, has_attachments, bcc, credential_like, large_recipient_set, new_recipient_domain, recipient_unverified.

Contacts. Someone becomes a contact of an inbox when the inbox wrote to them first and they replied, or when a human promotes them in the console.

An approval card shows the sender, every envelope recipient as the release service will send it, the subject, the first 2,000 characters of the body, attachment names and sizes, the risk flags, the key that submitted it and a link to the thread in the console.

Approvers can decide in:

  • the console (approvers, owners and admins; re-verification within 5 minutes),
  • Telegram or Slack, once a human has connected them in the console,
  • email: reply APPROVE or REJECT on the first line to the approval request (only from an approver’s verified address, with an aligned DKIM signature).

An approval is bound to the exact content. If the message changes after it was requested, the approval no longer applies. Each approval can be decided once. Undecided approvals expire after approval.ttl_minutes (24 hours by default), and the message is not sent.

Agents never approve. They can follow the outcome:

  • the send response says status: "pending_approval", with flags;
  • GET /v0/sends/{send_id} moves to approved (then sending, sent), rejected, expired or cancelled (Sends);
  • GET /v0/approvals and GET /v0/approvals/{approval_id} return metadata only (Approvals API); approval.requested and approval.decided events report changes.

An agent waiting for approval should not resend the message with a new idempotency key; that creates a second approval request and, once approved, a duplicate.

Approval is not the last step. Right before an approved or scheduled message leaves, the release service re-reads the current state: pauses, revoked keys, deleted or paused inboxes, thread locks, suppressions and block lists, the current policy and today’s limits. A message that no longer passes waits or is blocked (policy_changed, principal_revoked, …); it is never sent on a stale approval.

Outbound mail is signed simail.ai: a one-line footer — Sent by an AI agent via simail.ai and ” via SiMail” after the display name. These are on for every organization on the simail mail domain.