Skip to content

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.

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"])) < tolerance

Both halves matter. Without the timestamp check a request captured once can be replayed for as long as the secret lives.

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.

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.