Skip to main content
Sending is synchronous; everything after it is not. Delivery reports, replies, template approvals, and quality changes arrive as signed webhooks.
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 in X-LazyApp-Delivery) as your deduplication key.
  • Verify X-LazyApp-Signature with 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

The response includes a signing 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 a customer envelope:
Route on 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 a 2xx 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.
Keep your handler fast. Persist the event, return 2xx, then process on a worker. A slow handler risks the timeout and an unnecessary retry.
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:
  • id in the body — canonical event ID (evt_…)
  • X-LazyApp-Delivery — the same value
Use that identifier as your deduplication key. A typical pattern is to insert it into a unique-indexed table or cache before processing, and skip when the insert conflicts.

Signature verification

Every delivery includes X-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:
These are additional to signature verification, not a replacement. Reserved X-LazyApp-* names cannot be overridden.

Ordering

Webhooks are not ordered. A message.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.
You can also send a test from Settings → Webhooks in the console.

Replays

Exhausted deliveries are kept. Replay from the console once your endpoint is healthy. Replays are signed with your current secret by default, so rotating a secret does not break a replay. A byte-identical replay with the original signature is also available when you need to reproduce exactly what you first received.