Register a callback URL and receive signed, idempotent event notifications from the Kira API.
Register a callback URL and receive signed event notifications. Webhooks let you react to user verification, virtual account, deposit, and payout lifecycle changes without polling.
Every delivery carries the x-signature-sha256 header. Its value is the hex HMAC-SHA256 over the raw request body, keyed with your webhook secret.To verify a delivery:
1
Read the raw request body
Verify against the raw bytes exactly as received. Do not re-serialize the JSON before hashing — re-serialization changes whitespace and key order, which breaks the signature.
2
Compute the HMAC
Compute HMAC-SHA256(raw_body, webhook_secret) and hex-encode it. Use the exact, case-sensitive secret.
3
Compare in constant time
Compare your computed hex digest against the x-signature-sha256 header value using a constant-time comparison to avoid timing attacks.
import crypto from "node:crypto";function verifyWebhook(rawBody, signatureHeader, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) // raw bytes — do NOT re-serialize .digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(signatureHeader); return a.length === b.length && crypto.timingSafeEqual(a, b);}
Single delivery, NO retry on this pin — your endpoint must be highly available. A missed delivery is not re-sent.
De-duplicate on data.event_id. This is live-verified: event_id sits at data.event_id in both envelope shapes. There is no root-level event_id.
Make processing idempotent. Because deliveries are not retried and you must dedupe, design your handler so that re-processing the same data.event_id has no additional effect.
Return 2xx (and log) for unknown event types. New event types may appear; acknowledge them so they are not treated as failures.
A returned payout surfaces as payout.returned and resolves to status FAILED — RETURNED is not a payout status.
A returned/refunded deposit resolves to status REFUNDED — there is no RETURNED deposit status either. Branch on data.status == "refunded", not the event name.
KYT_PENDING / IN_REVIEW surface only via payout.status_changed.
Not emitted:virtual_account.failed, virtual_account.deactivated, payout.kyt_pending, payout.in_review, and any dispute events.