Skip to content

API & developers

Webhooks and signature verification

Get codes pushed to your server the moment they arrive, and verify that every delivery really came from us.

Last updated
Updated
Reading time
2 min read
On this page6
  1. Setting up
  2. Events
  3. Payload
  4. Verifying the signature
  5. Delivery and retries
  6. Rotating the secret

Instead of polling, give us an HTTPS URL and we'll POST an event to it whenever something happens to your orders or rentals.

Setting up

  1. In Settings, enter your endpoint URL. In production it must use https:// and be reachable on the public internet.
  2. Copy your signing secret — it starts with whsec_. It's created the first time you save a URL and stays the same if you change the URL later.
  3. Use Send test event to receive a webhook.test event and check your endpoint end to end.

Events

Event Sent when
order.code_received An SMS arrives on an order (once per message).
order.expired An order timed out without an SMS and was refunded.
order.cancelled An order was cancelled and refunded.
rental.message_received An SMS arrives on a rented number.
webhook.test You sent a test from Settings.

Payload

Every event uses the same envelope:

JSON
{
  "id": "evt_…",
  "type": "order.code_received",
  "created_at": "2026-10-06T14:03:12.000Z",
  "data": {
    "order": {
      "id": "ord_…",
      "status": "received",
      "phone_number": "+12025550147",
      "code": "482913"
    }
  }
}

order.* events carry { "order": … }, the same order object the API returns (abridged above). rental.message_received carries { "rental": …, "message": … }.

Each delivery has these headers:

Header Value
X-Passcode-Signature t=<unix seconds>,v1=<hex signature>
X-Passcode-Event The event type
X-Passcode-Delivery The event id (evt_…), the same on a retry

Verifying the signature

The signature is an HMAC-SHA256, keyed with your signing secret, of the timestamp, a full stop, and the raw request body: <t>.<body>. To verify a delivery:

  1. Read the raw body before parsing it. Re-serialized JSON won't match byte for byte.
  2. Split the header on commas and take t and every v1 value.
  3. Compute the HMAC-SHA256 of t + "." + body with your secret, as hex.
  4. Compare it with each v1 in constant time. Accept if any of them matches.
  5. Reject the delivery if t is more than 5 minutes away from your clock. That stops replays.
verify.js (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPasscodeSignature(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  let timestamp = NaN;
  const signatures = [];
  for (const part of header.split(",")) {
    const [key, value] = part.trim().split("=");
    if (key === "t") timestamp = Number(value);
    if (key === "v1" && value) signatures.push(value);
  }
  if (!Number.isInteger(timestamp) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  return signatures.some((signature) => {
    const given = Buffer.from(signature, "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}
verify.py (Python)
import hashlib
import hmac
import time

def verify_passcode_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    if not header:
        return False
    timestamp, signatures = None, []
    for part in header.split(","):
        key, _, value = part.strip().partition("=")
        if key == "t" and value.isdigit():
            timestamp = int(value)
        elif key == "v1" and value:
            signatures.append(value)
    if timestamp is None or not signatures:
        return False
    if abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, signature) for signature in signatures)

Warning

Always verify, including in development. Anyone who learns your endpoint's URL can send it requests; only we know your signing secret.

Delivery and retries

  • Answer with any 2xx status within 5 seconds, and do slow work after you've responded.
  • If your endpoint times out, can't be reached, or answers 429 or 5xx, we retry once, about a second later. Other responses, such as 400 or 404, aren't retried.
  • Redirects aren't followed, so point us at the final URL.
  • Deliveries can occasionally arrive twice or out of order. Use the event id to ignore duplicates, and treat the order's status as the source of truth.
  • A webhook is a fast path, not the only one. If a delivery fails, GET /api/v1/orders/{id} still has the code.

Rotating the secret

Rotate the signing secret in Settings whenever you need to. New deliveries are signed with the new secret straight away, so update your server promptly — and during the switch, accept a signature that matches either secret.