Core Concepts

Webhooks

Signed HTTP callbacks delivered to your registered endpoint when OTP events happen.

Events

EventFires whenStatus
otp.createdAn OTP is generated by /v1/otp/sendActive
otp.verifiedA code is successfully verifiedActive
otp.failedAn incorrect code is submitted, or max attempts are exceededActive
otp.expiredA verification is attempted after the 5-minute TTLActive
identity.createdReserved, not yet emitted
identity.updatedReserved, not yet emitted
application.createdReserved, not yet emitted
application.deletedReserved, not yet emitted

You can register a webhook for a reserved event today — it just won't receive any deliveries until that event type is wired up on the platform side. Don't build logic that depends on them yet.

Delivery payload

{
  "id": "e3a9c1f0-...",
  "event": "otp.verified",
  "createdAt": "2026-07-03T14:22:10.000Z",
  "payload": {
    "requestId": "a4b8c2d1...",
    "phone": "+999 482 918 102",
    "verifiedAt": "2026-07-03T14:22:10.000Z"
  }
}

The OTP code itself is never included in any webhook payload, under any event type.

Delivery headers

HeaderMeaning
x-cobinar-signaturesha256=<hex digest> — HMAC-SHA256 of the raw request body using your application's Webhook Secret
x-cobinar-eventThe event type, e.g. otp.verified
x-cobinar-event-idUnique ID for this delivery — use it to deduplicate retried deliveries
x-cobinar-timestampISO 8601 timestamp the event was created

Verifying the signature

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, webhookSecret) {
  const expected = crypto
    .createHmac('sha256', webhookSecret)
    .update(rawBody)          // the exact raw bytes, before any JSON.parse
    .digest('hex');
  return `sha256=${expected}` === signatureHeader;
}

app.post('/webhooks/auth', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-cobinar-signature'];
  if (!verifyWebhook(req.body, sig, process.env.COBINAR_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  // handle event.type / event.payload
  res.sendStatus(200);
});
Sign the raw request body, not a re-serialized version of the parsed JSON. Most frameworks reformat whitespace when you re-stringify an object, which changes every byte and breaks the signature. Read the body as raw text/bytes before parsing.

Retries

Delivery is handled by a dedicated worker so a slow endpoint never delays your API response. Each delivery gets up to 3 attempts with backoff (1s, then 4s between attempts) and an 8-second timeout per attempt. The final outcome is recorded against the webhook and visible in the console's Webhooks page.

Testing without a server

Use the webhook-echo tool in the API Sandbox to see a real signed delivery — and verify it in your browser — before you've written any receiving code. See Testing Your Integration.