Webhooks

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.

1
An event occurs in Authon (e.g., a user signs up)
2
Authon signs the payload with HMAC-SHA256 using your webhook secret
3
Authon sends a POST request to your endpoint URL
4
Your server verifies the signature and processes the event
5
Your server responds with 2xx to acknowledge receipt

Setting Up a Webhook

  1. 1
    Navigate to Dashboard → Webhooks → Add Webhook
  2. 2
    Enter your endpoint URL — must be publicly accessible via HTTPS
  3. 3
    Select the events you want to receive
  4. 4
    Copy the signing secret — shown only once, store it securely
  5. 5
    Set AUTHON_WEBHOOK_SECRET in your server environment

Event Types

EventTrigger
user.createdA new user completes registration
user.updatedA user's profile is updated
user.deletedA user account is permanently deleted
user.bannedA user is banned by an admin
user.unbannedA banned user is restored
session.createdA user successfully signs in (new session)
session.endedA user signs out normally
session.revokedA session is force-revoked by an admin
provider.linkedA user links an OAuth provider to their account
provider.unlinkedA 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.

json
{
  "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.

Always use the raw body buffer for verification, not a parsed JSON string. Express's json() middleware re-serializes the body which may alter whitespace and break the signature.

Node.js Example

routes/webhooks.ts
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:

ts
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.

AttemptDelayCumulative Time
Initial0s
1st retry5 seconds~5s
2nd retry30 seconds~35s
3rd retry5 minutes~6m
4th retry30 minutes~36m
5th retry2 hours~2h 36m

Best Practices

Always verify the signature
Never skip signature verification. Anyone can POST to your endpoint — verification proves the request came from Authon.
Use raw body parsing
Signature verification requires the exact raw bytes of the request body. Avoid JSON middleware on webhook routes.
Respond quickly, process async
Return 200 immediately and process the event asynchronously. Authon will retry if your endpoint takes longer than 10s.
Make handlers idempotent
You may receive the same event more than once due to retries. Use the event id to deduplicate — store processed IDs in your DB.
Rotate secrets periodically
Generate a new signing secret from the Dashboard every 90 days. Update AUTHON_WEBHOOK_SECRET in your deployment without downtime.
Authon — Universal Authentication Platform