Webhooks
Receive real-time notifications when verification events occur in your account.
Setup
Register a webhook endpoint in the dashboard or via the API. Your endpoint must be a publicly accessible HTTPS URL that returns a 2xx response within 30 seconds.
const endpoint = await stile.webhookEndpoints.create({
url: "https://yourapp.com/api/webhooks",
enabled_events: ["verification_session.verified", "verification_session.failed"],
});
// Save endpoint.secret to your environment variables — it's only shown oncePayload structure
Every webhook delivery is an HTTP POST with a JSON body containing an event object and a signature header.
// POST https://yourapp.com/api/webhooks
// Headers:
// Content-Type: application/json
// stile-signature: t=1741564800,v1=abc123...
{
"id": "evt_abc123",
"object": "event",
"type": "verification_session.verified",
"livemode": false,
"created": 1741564800,
"data": {
"id": "vks_xyz789",
"object": "verification_session",
"status": "verified",
"type": "identity",
"client_reference_id": "user_123",
"livemode": false,
"expires_at": 1741651200,
"completed_at": 1741564800,
"created": 1741561200
}
}Signature verification
The stile-signature header contains a timestamp and an HMAC-SHA256 signature. Always verify it before processing the event.
The signature format is: t={timestamp},v1={signature}. The signed payload is the raw request body.
Using fromRequest() (recommended)
The simplest approach — works with any framework that uses the standard Web API Request object (Next.js, Hono, Cloudflare Workers, Bun, Deno, etc.):
import Stile from "@stile/node";
const stile = new Stile(process.env.STILE_API_KEY!);
export async function POST(req: Request) {
let event;
try {
event = await stile.webhooks.fromRequest(
req,
process.env.WEBHOOK_SECRET!,
);
} catch (err) {
console.error("Webhook signature verification failed:", err);
return new Response("Invalid signature", { status: 400 });
}
switch (event.type) {
case "verification_session.verified":
await handleVerified(event.data);
break;
case "verification_session.failed":
await handleFailed(event.data);
break;
case "verification_session.expired":
await handleExpired(event.data);
break;
}
return Response.json({ received: true });
}Using constructEvent() (low-level)
If your framework doesn't use the standard Request object, use constructEvent() directly with the raw body and signature header:
const event = stile.webhooks.constructEvent(
rawBody, // string or Buffer — the unmodified request body
signatureHeader, // the stile-signature header value
webhookSecret, // your endpoint's signing secret
);Raw body is required
JSON body parsers transform the request body before signature verification, which will always fail. Make sure you pass the raw, unmodified request body to either fromRequest() or constructEvent().
Retry behavior
If your endpoint returns a non-2xx response or doesn't respond within 30 seconds, Stile retries the delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 (initial) | Immediate |
| 2 | ~5 minutes |
| 3 | ~30 minutes |
| 4 | ~2 hours |
| 5 (final) | ~5 hours |
After 5 failed attempts, the delivery is marked as permanently failed. You can view delivery history in the dashboard under Webhooks > Deliveries.
Handling duplicates
Due to retries, your endpoint may receive the same event more than once. Use the event id to deduplicate:
const alreadyProcessed = await db.processedEvents.findUnique({
where: { eventId: event.id },
});
if (alreadyProcessed) {
return Response.json({ received: true }); // Acknowledge but skip
}
await handleEvent(event);
await db.processedEvents.create({ data: { eventId: event.id } });Local testing
Use a tunnel tool like ngrok or Cloudflare Tunnel to expose your local server during development:
# Using ngrok
ngrok http 3000
# Your webhook URL becomes something like:
# https://abc123.ngrok-free.app/api/webhooksRegister the ngrok URL as a webhook endpoint in the dashboard, then trigger test events by creating verification sessions with your vk_test_ key.
Related
- Webhook Endpoints API — create, update, and delete endpoints via the API
- Events API — list and retrieve event objects
- Error Handling — handling API errors and retries