Webhooks
Listo pushes domain events to URLs you register on a per-installation basis. You receive HTTP POSTs from gateway.listoglobal.com shortly after the underlying change occurs in Listo, signed with HMAC-SHA256 according to the Standard Webhooks v1 specification.
This page is the entry point. Skip directly to:
- Subscribing — register a webhook URL on an installation.
- Verifying signatures — copy-paste verifier in Node, Python, Go, and Ruby.
- Delivery & retries — timeouts, retry schedule, idempotency.
- Event types — parent overview of the event catalog, with cross-cutting semantics. Per-event payload references:
At a glance
| Property | Value |
|---|---|
| Transport | HTTPS POST, Content-Type: application/json |
| User-Agent | ListoGlobal-Gateway/1.0 |
| Signature scheme | Standard Webhooks v1 — HMAC-SHA256, base64-encoded |
| Required headers | webhook-id, webhook-timestamp, webhook-signature |
| Per-attempt timeout | 30 seconds |
| Retries | Up to 3 attempts (1 initial + 2 retries), exponential backoff (~2s, ~4s, ~8s) |
| Replay window | ±300 seconds (5 minutes), enforced on producer and verifier |
| Ordering | Best-effort. Not guaranteed. |
| Idempotency key | webhook-id header (= the body's top-level id), stable across retries |
| Versioning | Two-axis: envelope specVersion + per-type dataVersion |
Anatomy of a delivery
Every webhook is a single HTTP POST whose body is a DomainEvent envelope (described below) — no outer wrapper — and whose headers carry the Standard Webhooks signature triplet:
POST /your/webhook/path HTTP/1.1
Host: hooks.your-domain.com
Content-Type: application/json
User-Agent: ListoGlobal-Gateway/1.0
webhook-id: lglsoevt_uZJK2rPmA1nGqvE5w
webhook-timestamp: 1735689600
webhook-signature: v1,3v+OYVnOwzbGcpRz1aRr7T0pYXukB0z2yDoa2QH1vNw=
{
"id": "lglsoevt_uZJK2rPmA1nGqvE5w",
"type": "client.created",
"specVersion": 1,
"dataVersion": 1,
"occurredAt": "2026-05-02T10:14:32.184Z",
"entity": { "type": "client", "id": "lglsocli_uZIIHfKqYBwyaRGGs" },
"data": { /* type-specific — see the event reference */ }
}Headers
Listo follows Standard Webhooks header naming exactly. Every delivery carries:
| Header | Required | Description |
|---|---|---|
webhook-id | yes | The event's globally unique id (lglsoevt_…). Stable across retries — your dedupe key. |
webhook-timestamp | yes | Unix epoch seconds at signing. Fresh per attempt; powers the 5-minute replay window. |
webhook-signature | yes | v1,<base64-hmac-sha256>. May contain multiple space-separated values during signing-key rotation. |
Content-Type | yes | Always application/json. |
User-Agent | yes | Always ListoGlobal-Gateway/1.0. Useful for inbound-traffic firewall rules. |
The DomainEvent envelope
DomainEvent envelopeEvery body shares this shape, regardless of type:
| Field | Type | Description |
|---|---|---|
id | string | Globally unique event id (lglsoevt_…). Equals webhook-id. |
type | string | Event type, e.g. client.created. Hierarchical, dot-delimited. |
specVersion | integer | Envelope version. Currently 1. Bumps only on envelope-level breaking changes. |
dataVersion | integer | Per-type data version. Bumps independently when a single event type's payload shape breaks. |
occurredAt | ISO-8601 UTC | When the event was produced at the source. Use this for ordering / auditing. |
entity.type | string | Domain entity type — one of "client", "user", "worker", "workerContract". |
entity.id | string | Public id of the entity (matches the relevant data.*Id). |
data | object | Type-specific payload. See the event-reference pages. |
data.links.self (when present) is a relative path you can append to https://gateway.listoglobal.com and call to fetch the entity's current state. Listo substitutes your installation's instanceIntegrationId into the path before signing the body, so the link is ready to call as-is.
Event-type catalog
| Type | Description | Data version |
|---|---|---|
client.created | A new client has been created on the instance. | 1 |
user.created | A new dashboard user has been created for a client on the instance. | 1 |
worker.onboarded | A worker's first contract for a client has been executed (one event per worker per client). | 1 |
worker.updated | A worker's profile fields (email, name, address, phone, date of birth, organization unit, organization location) have changed. | 1 |
worker.contract.updated | A worker's contract status, job title, or manager has changed (suppressed for the EXECUTED transition that triggers worker.onboarded). | 1 |
The list will grow over time. Design your handler to ignore unknown event types gracefully (HTTP 200 + log) rather than reject them with 4xx — returning a non-2xx for an unknown type would consume your retry budget for no benefit.
Versioning policy
Two independent axes — pin your handler against the (type, dataVersion) pair, never against specVersion alone.
| Change | What bumps | Migration window |
|---|---|---|
New optional field on data | Nothing | Tolerate unknown fields. Not announced as a breaking change. |
| New event type | Nothing | Tolerate unknown types. Subscribe explicitly to opt in. |
Breaking shape change on a specific type's data | That type's dataVersion | Old and new versions delivered in parallel for a deprecation window. |
Envelope-level breaking change (rename of type, etc.) | specVersion | Announced in advance with parallel delivery during deprecation. |
🛈 Tolerate unknowns. Your verifier should accept and log unfamiliar
typevalues and unfamiliar fields underdata. Listo's contract is to not break additive changes; rejecting them on your side defeats that.
Where to next
Updated 1 day ago
