Webhooks are on the Pro plan, as are live API keys. On a smaller plan you cannot add a webhook endpoint in live mode. Test mode is never limited. See Plans and limits.
How to think about webhooks
- Subscribe only to the events you actually handle.
- Treat each delivery as an event notification, not a full source of truth sync.
- Use the webhook event
id(also inX-LazyApp-Delivery) as your deduplication key. - Verify
X-LazyApp-Signaturewith the signing secret shown when you create the endpoint. - Acknowledge quickly and move heavier work to async jobs.
Delivery flow
1
Register an endpoint
Create one with
POST /v1/webhooks, or from Settings → Webhooks in the console.2
Choose events
Pass
subscribed_events, or omit it to receive every event. Prefer an explicit list.3
Receive POSTs
LazyApp sends a
POST whenever a subscribed event occurs. The body is the envelope below; headers carry signature, event type, and delivery id.4
Return 2xx
Respond
2xx after you have accepted the payload. Default timeout is 10 seconds (configurable 1–30 per endpoint).5
Validate
Use
POST /v1/webhooks/{webhook}/test and GET /v1/webhooks/deliveries to confirm your integration.secret, shown once. Store it — you need it to verify deliveries.
You can register several endpoints and restrict each to specific phone numbers.
Payload
Every delivery uses the same envelope:data is keyed by object type — message, contact, template, call, and so on — so a handler can switch on type and read the right key.
Any metadata you attached when sending, and the originating API request_id, come back on message events.
Platform deliveries
If you are a platform, events from a customer’s workspace are also delivered to your endpoints. Those copies include acustomer envelope:
customer.external_id (your own id). The customer’s own endpoint does not receive this envelope. See Build a platform.
Headers
Delivery retries
A delivery succeeds when your endpoint returns a2xx within the endpoint’s timeout (default 10 seconds, range 1–30). Any other outcome — non-2xx, timeout, or connection error — is retried.
Default max_attempts is 3 (configurable 1–8). Backoff between attempts, measured from when the previous attempt finished:
After the last attempt fails, the delivery is kept as a dead letter — it is not retried automatically. Inspect failures with
GET /v1/webhooks/deliveries (attempt is which try produced the log).
After 5 consecutive failures, LazyApp auto-pauses the endpoint so a dead receiver does not accumulate an unbounded backlog. Resume with POST /v1/webhooks/{webhook}/resume. You can also pause or remove endpoints yourself from settings.
Deliveries go through a transactional outbox: the event is committed with the state change that caused it, then delivered. A crash mid-request cannot lose an event that your data already reflects.
Idempotency
Webhook deliveries use at-least-once semantics: the same event may arrive more than once if a previous response was lost or your endpoint took too long to acknowledge. Your handler must be idempotent. Every payload carries a stable event identifier that is also a header:idin the body — canonical event ID (evt_…)X-LazyApp-Delivery— the same value
Signature verification
Every delivery includesX-LazyApp-Signature: lowercase hex HMAC-SHA256 of the raw request body, prefixed with sha256=, keyed by your endpoint secret.
See Verifying webhooks for Node, Python, and PHP examples. Reject unsigned or mismatched requests — do not process the payload.
If you lost the secret, rotate it with POST /v1/webhooks/{webhook}/rotate-secret instead of deleting the endpoint.
To change an endpoint’s URL, subscribed_events, headers, number restrictions or retry policy, use PATCH /v1/webhooks/{webhook} with just the fields to change. The secret and any queued retries are kept, which deleting and registering again would lose.
Custom headers
Add up to 20 static headers for a shared token or routing hint:X-LazyApp-* names cannot be overridden.
Ordering
Webhooks are not ordered. Amessage.read can arrive before message.delivered for the same message. Treat status as a set you converge on, using timestamps, rather than a sequence.
Available events
Events are grouped by area. The event reference lists every type and when it fires.Messages
Received, queued, sent, delivered, read, played, failed
Templates
Pending, approved, rejected, paused, disabled, category changes
Contacts
Profile updates, opt-in and opt-out
Numbers and connections
Quality rating, connection lifecycle
Flows and workflows
Flow publish status, automation runs
Platform
Customer workspace lifecycle for resellers
Testing
POST /v1/webhooks/{webhook}/test queues a signed delivery with "test": true and an evt_test_… id. It is stamped with the event type you pass (default message.sent) so you can exercise routing, but it is not a real message — ignore it in production handlers.