Skip to content

Webhooks and HMAC

Webhooks push incoming mail and unsubscribe events to an HTTPS endpoint. New keys should include webhook:write to create, update, or delete registrations. Read access remains accepted for older keys where the compatibility path is documented.

  1. Register an HTTPS URL with register_webhook or POST /v1/webhooks. Omit the inbox to cover the connection’s selected inboxes, project, or organization, or provide one accessible inbox. Legacy agent keys cover their own inboxes. The signing secret is returned once.

  2. Receive message.received or unsubscribe.received deliveries. Extrovert signs the raw request body and includes the event name in X-Extrovert-Event.

  3. Verify X-Extrovert-Signature before parsing or trusting the body.

  4. Acknowledge with a successful HTTP response. Failed deliveries use bounded retry and backoff.

Manage registrations with list_webhooks, get_webhook, update_webhook, and delete_webhook. Secrets are redacted after creation. Lists use data, has_more, and next_cursor; SDK and MCP clients normalize this to their page shape. Broader connections can narrow requests with org_id and project_id.

The returned resource_scope is the registration’s durable delivery boundary. Selected inboxes bind stable inbox_ids, so deleting an inbox and recreating its address does not transfer its subscription. Project and organization registrations include future inboxes. Clearing inbox removes only the address filter; it does not widen selected inbox IDs.

Registrations remain active after the creating connection expires or is revoked. Review and delete unneeded registrations separately. created_by_connection_id identifies the creating connection. An inbox create request may also include webhook_url.

X-Extrovert-Signature: t=1749751542,v1=3b2a...
X-Extrovert-Event: message.received

The signature is the hexadecimal HMAC-SHA256 of "{t}.{raw_body}" using the webhook secret. Verify the exact bytes received. Re-serializing parsed JSON changes the signed input.

import { verifyWebhookSignature } from "@extrovert.dev/sdk";
const payload = await request.text();
const valid = await verifyWebhookSignature({
payload,
signature: request.headers.get("x-extrovert-signature")!,
secret: process.env.EXTROVERT_WEBHOOK_SECRET!,
});

Reject a valid signature whose timestamp is outside the replay window your service accepts. Five minutes is a common starting point, but choose a window that matches expected queueing and clock skew.

message.received includes the canonical message object. Its text and html fields independently represent source MIME alternatives. extracted_text and extracted_html are nullable, best-effort derivatives. A missing format is not synthesized from the other.

unsubscribe.received reports a recorded opt-out through the same signed delivery path. Treat the event as notification; the server-side suppression row is authoritative.

MethodUse it when
WebhookA long-running service needs signed push delivery
wait_for_emailOne task needs the next matching message or code before a deadline
Message or thread listA worker or interface can poll bounded pages
Event streamA connected console or agent needs low-latency notification and can reconnect with a cursor

Webhooks and streams reduce latency but do not replace durable state. Re-read the message, suppression, or review record after reconnecting or recovering from a failed handler.