Webhooks
Receive real-time HTTP notifications when authentication events happen in your project. Sync users to your database, send welcome emails, or trigger workflows whenever users sign up, sign in, or get banned.
How Webhooks Work
When an event occurs in your Authon project, the API sends a POST request to your registered endpoint with a JSON payload. Each request includes an Authon-Signature header — an HMAC-SHA256 signature of the raw request body using your webhook signing secret. Always verify this signature before processing the event.
Setting Up a Webhook
- 1Navigate to Dashboard → Webhooks → Add Webhook
- 2Enter your endpoint URL — must be publicly accessible via HTTPS
- 3Select the events you want to receive
- 4Copy the signing secret — shown only once, store it securely
- 5Set AUTHON_WEBHOOK_SECRET in your server environment
Event Types
| Event | Trigger |
|---|---|
| user.created | A new user completes registration |
| user.updated | A user's profile is updated |
| user.deleted | A user account is permanently deleted |
| user.banned | A user is banned by an admin |
| user.unbanned | A banned user is restored |
| session.created | A user successfully signs in (new session) |
| session.ended | A user signs out normally |
| session.revoked | A session is force-revoked by an admin |
| provider.linked | A user links an OAuth provider to their account |
| provider.unlinked | A user unlinks an OAuth provider |
Payload Format
Every webhook payload is a JSON object with a consistent top-level structure. The data field contains event-specific details.
{
"id": "evt_1a2b3c4d5e6f",
"type": "user.created",
"projectId": "proj_abc123",
"timestamp": "2026-01-15T10:30:00.000Z",
"data": {
"user": {
"id": "usr_abc123",
"email": "user@example.com",
"displayName": "Jane Doe",
"emailVerified": false,
"isBanned": false,
"publicMetadata": null,
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
}
}
}Forsession.*events, the datafield also includes a sessionobject with
Signature Verification
Each webhook request contains an Authon-Signature header with the format sha256=<hex_digest>. Compute the expected HMAC-SHA256 over the raw request body using your webhook secret and compare with timing-safe equality.
json() middleware re-serializes the body which may alter whitespace and break the signature.Node.js Example
import { createHmac, timingSafeEqual } from "crypto";
import express from "express";
const router = express.Router();
function verifySignature(
rawBody: Buffer,
signature: string,
secret: string,
): boolean {
const expected = createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const actual = signature.replace("sha256=", "");
const expectedBuf = Buffer.from(expected, "hex");
const actualBuf = Buffer.from(actual, "hex");
return (
expectedBuf.length === actualBuf.length &&
timingSafeEqual(expectedBuf, actualBuf)
);
}
router.post(
"/webhooks/authon",
express.raw({ type: "application/json" }), // raw body required
(req, res) => {
const signature = req.headers["authon-signature"] as string;
const webhookSecret = process.env.AUTHON_WEBHOOK_SECRET!;
if (!verifySignature(req.body, signature, webhookSecret)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString()) as {
id: string;
type: string;
projectId: string;
timestamp: string;
data: Record<string, unknown>;
};
switch (event.type) {
case "user.created": {
const { user } = event.data as any;
// Sync new user to your database
await db.users.upsert({ authonId: user.id, email: user.email });
break;
}
case "user.updated": {
const { user } = event.data as any;
await db.users.update({ authonId: user.id }, { displayName: user.displayName });
break;
}
case "user.deleted": {
const { user } = event.data as any;
await db.users.delete({ authonId: user.id });
break;
}
case "user.banned": {
const { user } = event.data as any;
await db.users.update({ authonId: user.id }, { suspended: true });
break;
}
case "session.created": {
const { user, session } = event.data as any;
console.log(`${user.email} signed in from ${session.ipAddress}`);
break;
}
}
res.status(200).json({ received: true });
}
);
export default router;Using the SDK
The@authon/node SDK provides a built-in verify helper that handles timing-safe comparison for you:
import { AuthonBackend } from "@authon/node";
const authon = new AuthonBackend(process.env.AUTHON_SECRET_KEY!);
router.post(
"/webhooks/authon",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["authon-signature"] as string;
let event: Record<string, unknown>;
try {
event = authon.webhooks.verify(
req.body,
signature,
process.env.AUTHON_WEBHOOK_SECRET!,
);
} catch {
return res.status(401).json({ error: "Invalid signature" });
}
// event is the parsed and verified JSON payload
console.log("Verified event:", event.type);
res.json({ received: true });
}
);Retry Policy
If your endpoint returns a non-2xx status code or does not respond within 10 seconds, Authon retries the delivery with exponential backoff. After 5 failed attempts the event is marked as failed and you can manually retry it from the Dashboard.
| Attempt | Delay | Cumulative Time |
|---|---|---|
| Initial | — | 0s |
| 1st retry | 5 seconds | ~5s |
| 2nd retry | 30 seconds | ~35s |
| 3rd retry | 5 minutes | ~6m |
| 4th retry | 30 minutes | ~36m |
| 5th retry | 2 hours | ~2h 36m |