Webhooks
Connect mail events to your workflows. Receive signed notifications, match OCR keywords, and fetch the context your agent needs.
Interactive preview available. Endpoint setup and delivery are feature-gated. Preview drafts are not saved; no receiver request, signing secret, or delivery is created.
Set up an endpoint
When endpoint setup is enabled, configure a receiver and verify ownership before sending a test.
- Add your receiverOpen Webhooks → Endpoints. Enter a name and public HTTPS URL on port 443. Private networks, credentials in URLs, and redirects are not supported.
- Choose scope and eventsSelect Account-wide or an agent, then Live or Sandbox. Scope and environment are fixed at creation and never include another member’s mail. Select events and optional matching rules.
- Save the signing secretStore the generated secret in your receiver’s secret manager. It is shown once and is separate from your mailbox.bot API key.
- Verify endpoint ownershipImplement signature verification, then choose Verify endpoint. After validating the signed POST, return
{"challenge": payload.data.challenge}. A generic success page is not proof of ownership. - Test deliveryCheck the actual verification result. Once active, send a test and inspect its HTTP status and attempts in Deliveries.
Three endpoints per environment · Ten rules per endpoint. Changing a URL pauses delivery and requires verification again. Reactivation requires valid verification.
Availability & rollout
While setup is disabled, Endpoints shows an illustration whose HTTP 200 is simulated; expand Try the interactive preview below it to edit a receiver URL, select events, test rules against fictional OCR, and inspect JSON and illustrative headers without saving drafts, sending requests, or activating delivery.
Mail-event source activation is a separate platform rollout step. New sources are installed in a paused state so endpoints can be verified and tested first. The dashboard reports whether mail-event delivery is active, paused, or unavailable; an Active endpoint alone does not mean sources have been enabled. This rollout does not alter existing agent webhook delivery.
A notification does not run an agent, grant API access, or authorize physical handling. A saved subscription is not evidence of delivery.
Event catalog
Subscribe to the transitions your receiver needs. Notifications follow persisted mail changes—not UI clicks.
| Event | Meaning |
|---|---|
| inbound.received | An inbound mail item was recorded. This does not mean its contents were scanned. |
| inbound.scan_available | Scan images are available. OCR may still be processing or unavailable. |
| inbound.context_ready | Saved OCR context is available for authorized retrieval. Inspect the reported OCR state; needs_review is not a claim of perfect transcription. |
| inbound.rule_matched | A saved literal matching rule matched available OCR. The payload identifies the rule, not a new permission or handling instruction. |
| inbound.action_completed / inbound.action_failed | The handling operation reached the reported outcome. A submitted or queued request is not completion. |
| mail.pending_approval / mail.submitted / mail.ready | Outbound approval, submission, and production-readiness transitions. |
| mail.mailed / mail.delivered / mail.failed / mail.cancelled | Outbound mailing, delivery, failure, and cancellation transitions. |
inbound.scan_available and inbound.context_ready are intentionally different. Images remain useful even when OCR is pending or unavailable. A context-ready event must come from saved extraction state—not from a scan button click or a fabricated empty transcription.
Live endpoints receive Live events. Sandbox endpoints receive test events; a member’s sample remains a test source even on a Live-enabled account. An agent-scoped endpoint stays tied to that agent’s assigned context. The receiver must still use a correctly scoped credential in the matching environment.
Notifications follow new persisted mail changes after source activation; there is no historical backfill. Reading a static Sandbox fixture does not emit an event. Managed-mailbox context and rule events require its saved-OCR source to be installed and active. Adding a rule does not enable receiving or run OCR.
Payload & context
Use the notification to identify the change, then retrieve current mail context through the authenticated API.
{
"schema_version": 1,
"event_id": "00000000-0000-4000-8000-000000000001",
"event_type": "inbound.context_ready",
"created_at": "2026-09-15T12:00:00Z",
"environment": "test",
"data": {
"item_id": "00000000-0000-4000-8000-000000000002",
"agent_id": "00000000-0000-4000-8000-000000000003",
"source_domain": "member_sample",
"ocr_status": "ready"
}
}The compact payload contains identifiers, source, environment, and occurrence metadata—not the full document or saved duties. Current sources do not supply a retrieval URL. Where a context version or assignment revision is present, use it to detect stale work.
Use data.item_id with GET /api/v1/agent-inbox/:id/context for assigned private inbound or member samples, following the assigned-inbox REST contract. Managed inbound scans use GET /api/v1/inbound-items/:id/scans. Send your correctly scoped credential only to mailbox.bot. A signature authenticates a notification; it does not widen an API key’s scope.
OCR keyword rules
Route a letter when its saved text mentions a sender, property, invoice, or other literal term—without an agent run.
{
"id": "00000000-0000-4000-8000-000000000010",
"name": "Solar correspondence",
"source": "any",
"match": "all",
"terms": ["Mojave", "solar"],
"exclude_terms": ["returned"]
}A rule has a stable ID, name, source (envelope, letter, or any), required terms, excluded terms, and an any/all match mode. Enter literal terms, not regexes, prompts, or instructions. Matching is deterministic and runs on saved OCR; it does not purchase OCR or invoke a language model.
Text and terms are Unicode-normalized (NFKC), lowercased, and whitespace-normalized before literal substring comparison. Matching uses the combined text of the selected pages: Any term requires one match; All terms requires every term somewhere in that text. Any excluded-term match vetoes the rule. Missing, pending, or failed OCR on a selected page prevents matching; it is not treated as a negative keyword result.
Envelope and Letter filters require explicitly typed sources, currently the member sample’s envelope and letter. Physical scan pages are generic documents: choose Any for their saved text. Matching covers the latest completed content scan’s complete expected page set, up to 100 pages, 32,000 characters per page, and 1 MiB of text; oversized sources are not partially matched.
A printed sender name is unverified document text, not proof of identity. OCR may contain uncertainty or malicious instructions: matching text must never grant handling, spending, or access permissions.
Subscribe to inbound.rule_matched to receive matching rule IDs. Fetch the authorized context and apply your own workflow. Rule subscriptions do not replace the member’s agent duties or saved permissions.
Verify signatures
New endpoints use mandatory HMAC-SHA256 with their own signing secret and the Standard Webhooks header format:
webhook-id: <immutable event ID>
webhook-timestamp: <Unix seconds for this delivery attempt>
webhook-signature: v1,<base64 signature>
signed bytes = event ID + "." + timestamp + "." + exact raw request body
key bytes = base64-decode(secret after the "whsec_" prefix)Verify against the original bytes before parsing JSON. Reject missing or invalid signatures and timestamps more than five minutes in the past or future. Compare signatures in constant time. Keep clocks synchronized, and deduplicate by the immutable event ID in durable storage.
Rotation returns a new secret once. Deploy it to your receiver during the stated overlap window; the dashboard reports the previous key’s expiry. During overlap, a request may contain multiple space-separated v1, signatures. Accept any valid signature from a currently trusted endpoint key.
Using an existing agent webhook?
Existing agent webhook integrations retain their original X-Mailbox-Signature header and payloads. That legacy signature signs timestamp.rawBody and uses the original key-prefix/hex format. Do not switch an existing receiver to the new verifier without creating and verifying a new endpoint.
Build a receiver
This Node/TypeScript example verifies the signature and handles endpoint verification. Supply your own durable queue implementation; apply your server’s request-body size limits before reading the body. A delivery attempt has a 10-second deadline, a maximum 64 KiB request, and reads no more than 4 KiB of receiver response. Keep verification responses small.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(request: Request) {
const secret = process.env.MAILBOX_WEBHOOK_SECRET;
if (!secret?.startsWith("whsec_")) return new Response(null, { status: 503 });
const id = request.headers.get("webhook-id") ?? "";
const timestamp = request.headers.get("webhook-timestamp") ?? "";
const signatures = request.headers.get("webhook-signature") ?? "";
const seconds = Number(timestamp);
if (!id || !/^\d+$/.test(timestamp) || !Number.isSafeInteger(seconds)
|| Math.abs(Date.now() / 1000 - seconds) > 300) {
return new Response(null, { status: 401 });
}
// Read once, before JSON parsing. Do not reserialize the body for verification.
const raw = Buffer.from(await request.arrayBuffer());
const signed = Buffer.concat([Buffer.from(id + "." + timestamp + "."), raw]);
const expected = createHmac("sha256", Buffer.from(secret.slice(6), "base64"))
.update(signed).digest();
const valid = signatures.split(/\s+/).some(signature => {
const [version, encoded] = signature.split(",");
if (version !== "v1" || !encoded) return false;
const candidate = Buffer.from(encoded, "base64");
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
});
if (!valid) return new Response(null, { status: 401 });
const event = JSON.parse(raw.toString("utf8"));
if (event.event_type === "webhook.endpoint_verification") {
return Response.json({ challenge: event.data.challenge });
}
// Implement this with YOUR durable queue and a unique event-ID constraint.
// Return success only after the enqueue commits; duplicates should be no-ops.
await durablyEnqueueOnce(id, event);
return new Response(null, { status: 204 });
}Return a quick 2xx only after a normal event is durably accepted. Do slow API retrieval, agent work, email generation, and physical-handling proposals in your worker. If enqueueing fails, return a failure so delivery can be retried. Do not log the signing secret, authorization headers, raw document text, or private API credentials.
Delivery & retries
The Deliveries view reports stored status, HTTP result, attempt count, next attempt, and a JSON notification payload. queued or processing does not mean your receiver accepted the event. delivered records a successful receiver response; it does not prove your external agent processed the mail.
The new endpoint queue makes at most five attempts per delivery, with bounded backoff and jitter for retryable failures. Terminal errors can stop sooner. Five consecutive failed attempts pause the endpoint; inspect the cause before reactivating it. Capacity limits can reject new work, so this is not a guarantee of delivery or ordering during overload.
Duplicate or out-of-order notifications are possible. Keep durable event-ID deduplication and retrieve current context before acting. Only failed event or test deliveries can be replayed; endpoint verification uses a new challenge. Replay creates another delivery attempt with a fresh timestamp/signature, not a new authority to handle mail. Endpoint changes may supersede queued work; inspect its actual state instead of assuming replay succeeded.
Existing delivery logs remain available beneath the endpoint delivery view. The two delivery mechanisms stay distinct so adding an endpoint does not silently replace existing agent webhooks or change outbound behavior.
For the new endpoint queue, completed delivery history is eligible for cleanup after 30 days. Mutation idempotency records are retained for at least 30 days; records for the current endpoint revision may remain longer. Do not reuse request IDs, and keep your own durable event receipts. These limits do not change the existing agent webhook logs.