Webhooks
Add an endpoint on the Embedding screen. We POST JSON to it.
{ "event": "lead.created", "createdAt": "2026-08-29T11:02:19Z", "data": { "id": 1042, "name": "Anna K.", "phone": "+371…", "shortId": "kX3f9a", "priceCents": 432000, "currency": "EUR" }}Events: lead.created, configuration.saved, product.requested, ar.opened.
Check the signature
Section titled “Check the signature”X-Duor-Signature: t=1756463739,v1=5257a8… — the HMAC-SHA256 of
"{t}.{body}" with your signing secret.
import hashlib, hmac, time
def verify(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) signed = f"{parts['t']}.{body.decode()}".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() # Constant time: a byte-by-byte comparison leaks the signature one # character at a time to anybody willing to measure. if not hmac.compare_digest(expected, parts["v1"]): return False # And the timestamp, or a captured request can be replayed forever. return abs(time.time() - int(parts["t"])) < toleranceBoth halves matter. Without the timestamp check a request captured once can be replayed for as long as the secret lives.
Retries
Section titled “Retries”Anything other than a 2xx is retried: 1 min → 5 → 30 → 2 h → 6 h → 24 h,
then we stop and mark the delivery failed. Six attempts over roughly a day and
a half.
The delivery log on the Embedding screen keeps the status, the attempt count and the body of the response for failures, so an endpoint returning 500 can be debugged without writing to support.
Make your handler idempotent
Section titled “Make your handler idempotent”A webhook may arrive twice. A timeout on your side that we retried is
indistinguishable, from here, from one that never landed — so we send again,
and the id in data is the same. Key on it.