Skip to content

Webhooks

When a verification reaches a final state (pipeline decision or a reviewer’s manual decision), BAQSHI-KYC POSTs to the webhook_url you registered at creation:

{ "type": "verification.decided", "verification_id": "ver_…", "status": "approved" }

The payload deliberately carries no identity data — if your webhook endpoint is ever compromised or misrouted, nothing personal leaks. Fetch details with your API key after verifying the signature.

Headers on each request:

HeaderContent
X-Baqshi-TimestampUnix seconds at send time
X-Baqshi-SignatureHex HMAC-SHA256 over "{timestamp}.{raw_body}"

The signing key is your client’s own whsec_… webhook signing secret, issued alongside your API key (verifications created with an operator/root key are signed with the global WEBHOOK_SIGNING_KEY instead).

import hashlib, hmac, time
def verify(raw_body: bytes, headers, signing_key: str) -> bool:
ts = headers["X-Baqshi-Timestamp"]
if abs(time.time() - int(ts)) > 300: # reject replays older than 5 min
return False
expected = hmac.new(
signing_key.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, headers["X-Baqshi-Signature"])

Rules that matter:

  • Sign over the raw bytes — verify before JSON-parsing; re-serialized JSON won’t match.
  • Use constant-time comparison (hmac.compare_digest), never ==.
  • Enforce the timestamp window — the signature alone doesn’t stop replays.
  • The signing key is shared out-of-band by the operator (in the reference deployment: the WEBHOOK_SIGNING_KEY Railway variable).
  • At-least-once: up to 3 attempts with backoff (1s, 5s, 15s). Make your handler idempotent on verification_id + status.
  • Ordering: a verification can fire twice — once for manual_review, later for the reviewer’s approved/rejected. Treat the latest status as truth; better, re-fetch GET /v1/verifications/{id} on every delivery.
  • Timeout: respond 2xx within 10 seconds — do real work async.
  • No delivery is not no decision: if all attempts fail (your endpoint was down), the verification still holds its final status. Reconcile with a periodic poll of any verification_ids you’re awaiting.