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 project or provide one owned inbox. 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. 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.