stile
Guides

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 once

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

The simplest approach — works with any framework that uses the standard Web API Request object (Next.js, Hono, Cloudflare Workers, Bun, Deno, etc.):

app/api/webhooks/route.ts
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:

AttemptDelay
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/webhooks

Register the ngrok URL as a webhook endpoint in the dashboard, then trigger test events by creating verification sessions with your vk_test_ key.

On this page