Banata

Operate Your Project

Webhooks

Receive real-time notifications when auth events occur — user sign-ups, session changes, organization updates, and more.

Webhooks let you receive real-time HTTP notifications whenever something happens in your auth system. When a user signs up, a session is created, or an organization is updated, Banata Auth sends a POST request to your endpoint with the event details. You can use these notifications to sync data to external systems, trigger workflows, send emails, or track analytics.

Every webhook is signed with HMAC-SHA256 so you can verify it came from Banata Auth, and automatically retried if your endpoint is unavailable.


Setting Up Webhooks

From the Dashboard

  1. Go to Settings > Webhooks in your project dashboard.
  2. Click Add Endpoint.
  3. Enter your Endpoint URL (e.g., https://myapp.com/api/webhooks/banata).
  4. Select which events you want to receive.
  5. Copy the generated signing secret (starts with whsec_). Store it somewhere safe — you will need it to verify incoming webhooks.

Using the SDK

You can also manage webhook endpoints programmatically:

typescript
import { BanataAuth } from "@banata-auth/sdk";
 
const banata = new BanataAuth({
  apiKey: "your-api-key",
  baseUrl: "https://auth.banata.dev",
});
 
// Create a webhook endpoint
const endpoint = await banata.webhooks.createEndpoint({
  url: "https://myapp.com/api/webhooks/banata",
  events: [
    "user.created",
    "user.updated",
    "session.created",
    "organization.created",
  ],
});
 
// Save this secret securely — you need it to verify signatures
console.log(endpoint.secret); // "whsec_abc123..."
 
// List all endpoints
const { data: endpoints } = await banata.webhooks.listEndpoints();
 
// Update an endpoint
await banata.webhooks.updateEndpoint({
  endpointId: endpoint.id,
  url: "https://myapp.com/api/webhooks/v2/banata",
  events: ["user.created", "user.deleted"],
});
 
// Delete an endpoint
await banata.webhooks.deleteEndpoint({
  endpointId: endpoint.id,
});

Webhook Payload Format

Every webhook arrives as a POST request with a JSON body:

json
{
  "event": "user.created",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "data": {
    "id": "usr_01HXYZ...",
    "email": "user@example.com",
    "name": "Jane Doe",
    "createdAt": "2025-01-15T10:30:00.000Z"
  }
}

The data field contains the full resource object relevant to the event.

Headers

Each request includes these headers:

HeaderExample ValueDescription
Content-Typeapplication/jsonAlways JSON
Webhook-Idmsg_01HXYZ...Unique message ID (use for deduplication)
Webhook-Timestamp1705312200Unix timestamp in seconds
Webhook-Signaturet=1705312200,v1=abc123...HMAC-SHA256 signature

Signing and Verification

Every webhook is signed so you can confirm it was sent by Banata Auth and has not been tampered with.

Signature Format

The Webhook-Signature header looks like this:

typescript
t={timestamp},v1={hmac_hex}
  • t — The Unix timestamp (seconds), matching the Webhook-Timestamp header.
  • v1 — The HMAC-SHA256 hex digest of the string {timestamp}.{raw request body}.

The signing secret starts with whsec_. When computing the HMAC, strip the whsec_ prefix first and use the remaining value as the key.

The SDK's constructEvent method handles verification for you. It is async because it uses the Web Crypto API under the hood — always await it.

typescript
import { BanataAuth } from "@banata-auth/sdk";
 
const banata = new BanataAuth("your-api-key");
 
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("webhook-signature")!;
 
  try {
    const event = await banata.webhooks.constructEvent({
      payload: body,
      sigHeader: signature,
      secret: "whsec_your-endpoint-secret",
    });
 
    // event is verified and parsed — safe to use
    switch (event.event) {
      case "user.created":
        await handleUserCreated(event.data);
        break;
      case "session.created":
        await handleSessionCreated(event.data);
        break;
    }
 
    return new Response("OK", { status: 200 });
  } catch (error) {
    console.error("Webhook verification failed:", error);
    return new Response("Invalid signature", { status: 401 });
  }
}

Manual Verification

If you prefer to verify signatures without the SDK (or you are using a language other than TypeScript), here is the process:

typescript
async function verifyWebhookSignature(
  body: string,
  signatureHeader: string,
  secret: string
): Promise<boolean> {
  // 1. Parse the signature header
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const [key, value] = part.split("=");
      return [key, value];
    })
  );
 
  const timestamp = parts.t;
  const expectedSignature = parts.v1;
 
  if (!timestamp || !expectedSignature) {
    return false;
  }
 
  // 2. Reject timestamps older than 5 minutes (replay protection)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }
 
  // 3. Strip the whsec_ prefix from the secret
  const rawSecret = secret.startsWith("whsec_")
    ? secret.slice(6)
    : secret;
 
  // 4. Compute the HMAC-SHA256 signature
  const message = `${timestamp}.${body}`;
  const encoder = new TextEncoder();
 
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(rawSecret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"]
  );
 
  const signatureBuffer = await crypto.subtle.sign(
    "HMAC",
    key,
    encoder.encode(message)
  );
 
  const computedSignature = Array.from(new Uint8Array(signatureBuffer))
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
 
  // 5. Use timing-safe comparison
  return timingSafeEqual(computedSignature, expectedSignature);
}
 
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return result === 0;
}

Retry Policy

If your endpoint returns a non-2xx status code (or is unreachable), Banata Auth retries with exponential backoff:

AttemptDelay After FailureCumulative Time
1st (initial)Immediate0
2nd (1st retry)5 minutes5 min
3rd (2nd retry)30 minutes35 min
4th (3rd retry)2 hours2 hr 35 min
5th (4th retry)24 hours26 hr 35 min

After 5 failed attempts, the delivery is marked as permanently failed.

Tips for handling retries:

  • Return 200 quickly. Do your heavy processing in a background job so the request does not time out.
  • Deduplicate with Webhook-Id. Store processed message IDs and skip any you have already seen.
  • Handle out-of-order delivery. Retries can arrive after newer events. Use the timestamp field to determine the correct state.

Available Events

You can subscribe to any combination of the following events.

User Events

EventTriggered When
user.createdA new user account is created
user.updatedA user profile is updated
user.deletedA user account is deleted
user.bannedA user account is banned
user.unbannedA user account is unbanned

Session Events

EventTriggered When
session.createdA new session starts (sign-in)
session.revokedA session ends (sign-out or revocation)

Organization Events

EventTriggered When
organization.createdA new organization is created
organization.updatedOrganization details are updated
organization.deletedAn organization is deleted
member.addedA member is added to an organization
member.removedA member is removed from an organization
member.role_updatedA member's role changes
invitation.createdAn invitation is sent
invitation.acceptedAn invitation is accepted
invitation.revokedAn invitation is revoked

Security Events

EventTriggered When
email.verifiedA user verifies their email address
password.resetA user resets their password
two_factor.enabledA user enables multi-factor authentication
two_factor.disabledA user disables multi-factor authentication
api_key.createdA new API key is created
api_key.revokedAn API key is revoked

Complete Next.js Handler Example

Here is a full webhook handler you can drop into a Next.js App Router project:

typescript
// src/app/api/webhooks/banata/route.ts
import { BanataAuth } from "@banata-auth/sdk";
 
const banata = new BanataAuth("your-api-key");
const WEBHOOK_SECRET = process.env.BANATA_WEBHOOK_SECRET!;
 
export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("webhook-signature");
  const timestamp = request.headers.get("webhook-timestamp");
 
  if (!signature || !timestamp) {
    return new Response("Missing signature headers", { status: 400 });
  }
 
  // Verify the webhook signature
  let event;
  try {
    event = await banata.webhooks.constructEvent({
      payload: body,
      sigHeader: signature,
      secret: WEBHOOK_SECRET,
    });
  } catch (err) {
    console.error("Webhook signature verification failed:", err);
    return new Response("Invalid signature", { status: 401 });
  }
 
  // Process the event
  try {
    switch (event.event) {
      case "user.created":
        // Sync the new user to your CRM or analytics platform
        await syncUserToCRM(event.data);
        break;
 
      case "user.deleted":
        // Clean up user data in external systems
        await cleanupExternalData(event.data.id);
        break;
 
      case "organization.created":
        // Provision resources for the new organization
        await provisionOrgResources(event.data);
        break;
 
      case "session.created":
        // Track sign-in events for analytics
        await trackSignIn(event.data);
        break;
 
      default:
        console.log(`Unhandled event type: ${event.event}`);
    }
  } catch (err) {
    // Log the error but still return 200 to prevent unnecessary retries
    console.error(`Error processing ${event.event}:`, err);
  }
 
  return new Response("OK", { status: 200 });
}

Testing Locally

During development, your local server is not publicly accessible. Use a tunneling tool like ngrok to expose it:

bash
# Start your Next.js dev server
npm run dev
 
# In another terminal, create a tunnel to port 3000
ngrok http 3000

Copy the HTTPS URL that ngrok gives you (e.g., https://abc123.ngrok.io) and register it as a webhook endpoint in the dashboard:

typescript
https://abc123.ngrok.io/api/webhooks/banata

You can now trigger auth events locally and see the webhook requests arrive in real time.


Security Best Practices

  1. Always verify signatures. Never process a webhook payload without checking the HMAC signature first.
  2. Check timestamp freshness. Reject webhooks with timestamps older than 5 minutes to guard against replay attacks.
  3. Use HTTPS endpoints. Never receive webhook data over unencrypted HTTP in production.
  4. Store secrets in environment variables. Keep your whsec_ signing secret out of source code and version control.
  5. Respond quickly, process later. Return a 200 response right away and handle the event asynchronously in a background job.
  6. Implement idempotency. Use the Webhook-Id header to detect and skip duplicate deliveries.

Next Steps

  • Audit Logs — Track and query a detailed history of auth events.
  • SDK Reference — Explore the full SDK API, including webhook management methods.
  • Deploy — Take your project to production with monitoring and alerting.