Skip to main content
WEBHOOK

Headers

X-Listo-Signature
string
required

Proves the event came from us. The value is t=<unix seconds>,v1=<hex>, where the hex is an HMAC-SHA256, keyed on your signing secret, over the string "<t>.<raw request body>".

To verify: split the header on the comma, rebuild "<t>.<body>" from the RAW bytes you received (never a parsed and re-serialized object, or whitespace and escape sequences will change the digest), compute the HMAC, and compare it to v1 in constant time. Reject anything that does not match.

Then check the clock. Reject an event whose t is more than 5 minutes away from now in EITHER direction: a t far in the past is the replay this scheme exists to stop, and one far in the future is the same attack with a clock nobody checked.

Two secrets can be active at once so you can rotate without downtime: add the new one, accept either, promote it, then drop the old one. We always sign with the primary.

Example:

"t=1753728063,v1=8d2f4a1b6c7e9f0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f607182"

Body

application/json

The fields every event shares. Delivery is at-least-once and unordered by design: dedupe on id, and order by created_at or by the object's own timestamps rather than by arrival. type and data are added by each event, because their values depend on which event it is.

id
string
required

The event id, evt_ prefixed. Dedupe on this.

Example:

"evt_9pQ2vX7mKdR4tYb1nZ3sLwCe"

created_at
string<date-time>
required

When the event was recorded, ISO-8601 in UTC.

Example:

"2026-07-28T18:41:03Z"

type
enum<string>
required

Which event this is.

Available options:
delivery.updated
data
object
required

The shape of a delivery: every field the object can carry, with the nullability each one really has. It is the response of every operation that answers a delivery (the create, the fetch, a list row, the reschedule, the cancel, the retry and the refund) and the data of every full delivery event, so ONE type holds all of them.

Every operation returns the whole object. A field that a given call cannot yet have is present and null rather than missing: a create response has a null courier, because no courier has been assigned yet, and a reschedule only ever answers with a still-scheduled order, so its delivered_at is null.

Money is integer centavos, timestamps are ISO-8601 in UTC, and every enum below is ours rather than a passthrough of anyone else's.

test
boolean

Present, and true, only on a test event you asked for with POST /v1/test_webhooks. A real event does not carry the field at all. A test event's data is a sample whose ids resolve to nothing.

Example:

true

Response

200

Return any 2xx to acknowledge the event.