> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lazyapp.in/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> How LazyApp webhook deliveries work, including retries, idempotency, and signature verification.

Sending is synchronous; everything after it is not. Delivery reports, replies, template approvals, and quality changes arrive as signed webhooks.

> [!NOTE]
>
> 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](https://docs.lazyapp.in/billing/pricing-and-tiers.md#feature-gating).

## 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`](https://docs.lazyapp.in/api-reference/webhooks/create-a-webhook-endpoint), or from **Settings → Webhooks** in the console.

**2. Choose events**

Pass `subscribed_events`, or omit it to receive [every event](https://docs.lazyapp.in/webhooks/events.md). 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`](https://docs.lazyapp.in/api-reference/webhooks/send-a-test-webhook-delivery) and [`GET /v1/webhooks/deliveries`](https://docs.lazyapp.in/api-reference/webhooks/list-webhook-deliveries) to confirm your integration.

```bash theme={null}
curl https://lazyapp.in/v1/webhooks \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.example/webhooks/lazyapp",
    "description": "Order service",
    "subscribed_events": ["message.delivered", "message.failed"],
    "timeout_seconds": 10
  }'
```

The response includes a signing `secret`, shown once. Store it — you need it to [verify deliveries](https://docs.lazyapp.in/webhooks/verifying.md).

You can register several endpoints and restrict each to specific phone numbers.

## Payload

Every delivery uses the same envelope:

```json theme={null}
{
  "id": "evt_9f8b1c2d-4e5f-4a6b-8c7d-0e1f2a3b4c5d",
  "type": "message.delivered",
  "created_at": "2026-08-19T11:42:07+00:00",
  "data": {
    "message": {
      "uuid": "1a2b3c4d-...",
      "status": "delivered",
      "to": "+919812345670",
      "provider_message_id": "wamid.HBgMOTE5ODEyMzQ1Njcw",
      "request_id": "req_…",
      "metadata": { "order_id": "1042" },
      "created_at": "2026-08-19T11:41:59+00:00"
    }
  }
}
```

`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:

```json theme={null}
{
  "id": "evt_…",
  "type": "message.delivered",
  "created_at": "2026-08-19T11:42:07+00:00",
  "customer": {
    "id": "cus_9f8b1c2d",
    "external_id": "acct_8812",
    "name": "Acme Retail"
  },
  "data": {
    "message": { "uuid": "…", "status": "delivered" }
  }
}
```

Route on `customer.external_id` (your own id). The customer's own endpoint does not receive this envelope. See [Build a platform](https://docs.lazyapp.in/platform/overview.md#step-4-route-webhooks-back-to-the-right-customer).

## Headers

| Header | Purpose |
| - | - |
| `X-LazyApp-Signature` | `sha256=<hmac>` over the raw body. [Verify it](https://docs.lazyapp.in/webhooks/verifying.md) |
| `X-LazyApp-Event` | The event type, so you can route before parsing |
| `X-LazyApp-Delivery` | The event id, for deduplication |
| `X-LazyApp-Api-Version` | The payload version for this endpoint |

## 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:

| Attempt | Delay before this attempt | Notes |
| - | - | - |
| 1 | immediate | First try |
| 2 | 10s | |
| 3 | 1m | Default `max_attempts` stops here |
| 4 | 5m | Only if `max_attempts` ≥ 4 |
| 5 | 15m | |
| 6 | 30m | |
| 7 | 1h | |
| 8 | 2h | Job ceiling |

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`](https://docs.lazyapp.in/api-reference/webhooks/list-webhook-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`](https://docs.lazyapp.in/api-reference/webhooks/resume-a-paused-webhook-endpoint). You can also pause or remove endpoints yourself from settings.

> [!TIP]
>
> **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](https://docs.lazyapp.in/webhooks/verifying.md) 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`](https://docs.lazyapp.in/api-reference/webhooks/rotate-a-webhook-signing-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:

```json theme={null}
{ "custom_headers": { "X-Tenant": "acme" } }
```

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](https://docs.lazyapp.in/webhooks/events.md) lists every type and when it fires.

> **[Messages](https://docs.lazyapp.in/webhooks/events.md#messages)**
>
> Received, queued, sent, delivered, read, played, failed

> **[Templates](https://docs.lazyapp.in/webhooks/events.md#templates)**
>
> Pending, approved, rejected, paused, disabled, category changes

> **[Contacts](https://docs.lazyapp.in/webhooks/events.md#contacts)**
>
> Profile updates, opt-in and opt-out

> **[Numbers and connections](https://docs.lazyapp.in/webhooks/events.md#numbers-and-connections)**
>
> Quality rating, connection lifecycle

> **[Flows and workflows](https://docs.lazyapp.in/webhooks/events.md#flows)**
>
> Flow publish status, automation runs

> **[Platform](https://docs.lazyapp.in/webhooks/events.md#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.

```bash theme={null}
curl -X POST https://lazyapp.in/v1/webhooks/$WEBHOOK_ID/test \
  -H "Authorization: Bearer $LAZYAPP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"event": "message.delivered"}'
```

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.