# Receive events

**You will:** Register an endpoint, answer fast and handle retries.

**You need:**
- A key with webhooks:write
- A public https URL that answers 2xx

## 1. Register an endpoint

```bash title="cURL"
curl https://api.lelemon.cl/v2/webhook-endpoints \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  -H "Idempotency-Key: webhook-main" \
  --json '{
    "url": "https://example.com/lelemon",
    "enabledEvents": ["message.created", "case.control_changed", "case.escalated"]
  }'
```

```ts title="Node.js"
const endpoint = await lelemon.webhookEndpoints.create({
  url: 'https://example.com/lelemon',
  enabledEvents: ['message.created', 'case.control_changed', 'case.escalated'],
});
```


`enabledEvents` takes types from the [event catalog](/webhooks/events), or `"*"` for all. The response includes `secret`, shown only once. `scope` is `self` for the events of this organization or `customers` for the events of its subaccounts. An organization can have up to 16 endpoints, enabled or not, because every event goes to each one that listens to it. The next one answers `422 webhook_endpoint_limit`: delete one you no longer use, or add event types to one that already exists.

## 2. Test it

`POST /v2/webhook-endpoints/{id}/test` sends a signed `webhook_endpoint.test` event to that endpoint only.

## 3. Answer fast

Respond `2xx` as soon as you have stored the event, and do the work afterwards. Any other answer, or no answer, is retried.

Each event is an envelope:

```json title="Event"
{
  "object": "event",
  "id": "evt_...",
  "type": "message.created",
  "createdAt": "2026-10-29T14:10:42.000Z",
  "organizationId": "org_...",
  "customerId": "sub_...",
  "data": {}
}
```


`customerId` is present when the event belongs to a subaccount. The `data` shape depends on `type`.

**Event**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `object` | enum | yes | `event` |
| `id` | string | yes |  |
| `type` | string | yes | The event type, such as case.escalated. See GET /v2/event-types. |
| `createdAt` | string | yes |  |
| `organizationId` | string | yes |  |
| `customerId` | string | no | The subaccount the event belongs to, when there is one. |
| `data` | object | yes | The shape depends on `type`. See GET /v2/event-types. |

## Retries

**A signed webhook and its retries**

Answer 2xx fast. Anything else, or no answer, is retried with growing waits, and the same event id comes back every time.

1. Lelemon sends the event with Lelemon-Signature and a Lelemon-Delivery-Id. The v1 value is an HMAC-SHA256 of t, a dot and the raw body.
2. Your endpoint checks the signature against the raw body and rejects a t more than 5 minutes from its clock.
3. Any answer that is not 2xx, or no answer, counts as a failure.
4. Lelemon waits and tries again, up to 12 attempts in all.
5. The event id stays the same on every attempt and the Lelemon-Delivery-Id changes. Deduplicate by the evt_ id.
6. Your endpoint stores the event, answers 2xx and does the work afterwards. The delivery is done.
7. After the last attempt the delivery is exhausted. An endpoint with no successful delivery for 72 hours becomes disabled and Lelemon sends an email.

| Attempt | Wait before it |
| --- | --- |
| 2 | 5 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 5 hours |
| 7 to 12 | 10 hours each |

After the last attempt the delivery is `exhausted`. If an endpoint has no successful delivery for 72 hours, it becomes `disabled` and Lelemon sends an email.

Events can arrive twice and out of order. Deduplicate by the `evt_` id and compare `createdAt`. Each attempt carries a `Lelemon-Delivery-Id` header, a `whd_` id that changes per attempt while the event id stays the same. It is the `id` of that attempt in `GET /v2/webhook-endpoints/{id}/deliveries`, so you can match what you received with what Lelemon recorded.

## Inspect and replay

- `GET /v2/webhook-endpoints/{id}/deliveries` lists the attempts of an endpoint.
- `POST /v2/events/{id}/resend` sends an event again, to one endpoint with `endpointId` or to every subscribed endpoint.

Events and their delivery attempts are kept for 30 days. After that, an event can no longer be resent (`404`) and its attempts leave the list. An event that is still being retried or was resent within the last 30 days stays until that window passes.

## Local development

Local URLs are rejected unless the API runs with `NODE_ENV=development` or `test` and `LELEMON_ALLOW_LOCAL_WEBHOOKS=true`. Then `localhost`, `127.0.0.1` and `::1` are accepted. No other private range is. Elsewhere, expose your machine with a tunnel.
