Skip to content

Webhooks

Zautha sends your server a signed POST request when something changes in an instance: a user signs up, a session ends, a member joins an organization. The event catalog lists every event.

In the dashboard, open the instance’s Webhooks page and add an endpoint:

  • URL: an absolute https URL on a public host name. Not an IP address, not localhost, and without a user name or password. Zautha refuses to connect to private, loopback and other non-public addresses, even when a public host name resolves to one, and it does not follow redirects.
  • Events: the event types the endpoint receives. Leave all unchecked to receive every event.

An instance can have up to 20 endpoints. Development and production instances have their own endpoints, so a test sign-up never reaches your production server.

Each endpoint has a signing secret (whsec_…). Reveal it on the endpoint’s page and give it to your server. Rotate secret makes a new one; the old one keeps signing alongside it for 24 hours, so you can switch without missing an event.

POST /webhooks/zautha HTTP/1.1
Content-Type: application/json
User-Agent: Zautha-Webhooks/1
webhook-id: msg_…
webhook-timestamp: 1767225600
webhook-signature: v1,…
{
"type": "user.created",
"object": "event",
"timestamp": 1767225600123,
"instance_id": "ins_…",
"data": { "id": "user_…", "primary_email_address": "ada@example.com", … }
}
  • type is the event; data is the object it is about, exactly as the Backend API returns it, as it was when the event happened. Deletions of users and organizations carry only { id, object, deleted: true }.
  • timestamp is when the event happened, in Unix milliseconds.
  • The headers follow Standard Webhooks: webhook-id identifies the message (the same on every retry and resend), webhook-timestamp is when this attempt was signed (Unix seconds) and webhook-signature is v1, followed by the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, with the base64 after whsec_ as the key. During a rotation it has two signatures separated by a space; one matching is enough.

An event is sent only once the change it reports is saved: a change that fails sends nothing.

Verify the body exactly as it arrived: a body parsed and serialized again no longer matches its signature. The SDKs check the signature and that webhook-timestamp is at most 5 minutes from your clock, then return the typed event.

app/api/webhooks/route.ts
import { verifyWebhook, isZauthaWebhookVerificationError } from "@zautha/nextjs/webhooks";
export async function POST(request: Request): Promise<Response> {
try {
const event = await verifyWebhook(request);
if (event.type === "user.created") console.log(event.data.primary_email_address);
return new Response(null, { status: 204 });
} catch (error) {
if (isZauthaWebhookVerificationError(error)) return new Response(error.code, { status: 400 });
throw error;
}
}

verifyWebhook reads the signing secret from ZAUTHA_WEBHOOK_SIGNING_SECRET. Keep the route out of auth.protect(): Zautha’s requests carry no session.

A request that fails verification is ZauthaWebhookVerificationError in the JavaScript SDKs, with code missing_headers, invalid_signature or timestamp_out_of_tolerance, and ZauthaWebhookVerificationException in Zautha.AspNetCore, whose Error says why. See @zautha/backend and Zautha.AspNetCore for the event types each one returns.

Answer with any 2xx status, quickly. Anything else, or no answer within 15 seconds, is a failed attempt, and the message is sent again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 10 hours and then every 10 hours (each delay moved by up to 10 % either way), until 5 days have passed since its first attempt. Then it is marked failed.

  • At least once: the same message can arrive more than once. It keeps its webhook-id across retries and resends, so use it to skip duplicates.
  • In any order: messages are not guaranteed to arrive in the order the changes happened. Use the event’s timestamp, or the object’s updated_at, to keep the latest state; or fetch the object from the Backend API.
  • Do the work later: answer first and process the event in a background job, so a slow task does not turn into a timeout and a retry.

An endpoint whose deliveries keep failing for 5 days with no success in between is disabled: its pending messages are marked failed and the workspace’s owners and admins get an email. Enable it again on its page, then recover what it missed.

The endpoint’s page in the dashboard shows its delivery log: each message’s event, status (pending, succeeded or failed) and attempts, with the response status, the duration and the first kilobyte of your server’s response.

  • Resend sends one message again.
  • Recover sends again every message since a time that the endpoint did not receive: failed deliveries, and events it missed while it was disabled or before it existed. Only events Zautha recorded can be recovered: an event is recorded when at least one endpoint of the instance receives its type.
  • Send a test event sends example data of an event type to this endpoint only. It appears in the log, marked as a test. Test events are never recovered.

Messages are kept as long as the workspace’s plan keeps its audit log: 7 days on Free, 90 days on Pro.