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
- Go to Settings > Webhooks in your project dashboard.
- Click Add Endpoint.
- Enter your Endpoint URL (e.g.,
https://myapp.com/api/webhooks/banata). - Select which events you want to receive.
- 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:
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:
{
"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:
| Header | Example Value | Description |
|---|---|---|
Content-Type | application/json | Always JSON |
Webhook-Id | msg_01HXYZ... | Unique message ID (use for deduplication) |
Webhook-Timestamp | 1705312200 | Unix timestamp in seconds |
Webhook-Signature | t=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:
t={timestamp},v1={hmac_hex}t— The Unix timestamp (seconds), matching theWebhook-Timestampheader.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.
Using the SDK (Recommended)
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.
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:
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:
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1st (initial) | Immediate | 0 |
| 2nd (1st retry) | 5 minutes | 5 min |
| 3rd (2nd retry) | 30 minutes | 35 min |
| 4th (3rd retry) | 2 hours | 2 hr 35 min |
| 5th (4th retry) | 24 hours | 26 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
timestampfield to determine the correct state.
Available Events
You can subscribe to any combination of the following events.
User Events
| Event | Triggered When |
|---|---|
user.created | A new user account is created |
user.updated | A user profile is updated |
user.deleted | A user account is deleted |
user.banned | A user account is banned |
user.unbanned | A user account is unbanned |
Session Events
| Event | Triggered When |
|---|---|
session.created | A new session starts (sign-in) |
session.revoked | A session ends (sign-out or revocation) |
Organization Events
| Event | Triggered When |
|---|---|
organization.created | A new organization is created |
organization.updated | Organization details are updated |
organization.deleted | An organization is deleted |
member.added | A member is added to an organization |
member.removed | A member is removed from an organization |
member.role_updated | A member's role changes |
invitation.created | An invitation is sent |
invitation.accepted | An invitation is accepted |
invitation.revoked | An invitation is revoked |
Security Events
| Event | Triggered When |
|---|---|
email.verified | A user verifies their email address |
password.reset | A user resets their password |
two_factor.enabled | A user enables multi-factor authentication |
two_factor.disabled | A user disables multi-factor authentication |
api_key.created | A new API key is created |
api_key.revoked | An 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:
// 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:
# Start your Next.js dev server
npm run dev
# In another terminal, create a tunnel to port 3000
ngrok http 3000Copy the HTTPS URL that ngrok gives you (e.g., https://abc123.ngrok.io) and register it as a webhook endpoint in the dashboard:
https://abc123.ngrok.io/api/webhooks/banataYou can now trigger auth events locally and see the webhook requests arrive in real time.
Security Best Practices
- Always verify signatures. Never process a webhook payload without checking the HMAC signature first.
- Check timestamp freshness. Reject webhooks with timestamps older than 5 minutes to guard against replay attacks.
- Use HTTPS endpoints. Never receive webhook data over unencrypted HTTP in production.
- Store secrets in environment variables. Keep your
whsec_signing secret out of source code and version control. - Respond quickly, process later. Return a
200response right away and handle the event asynchronously in a background job. - Implement idempotency. Use the
Webhook-Idheader 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.