---
title: "Webhooks"
url: https://docs.light-ai.app/webhooks.md
canonical_url: https://docs.light-ai.app/webhooks
last_updated: 2026-10-13
summary: "Signed, retried and replayable event delivery to your HTTPS endpoint, compatible with Zapier, Make and n8n."
api_status: beta
related:
  - https://docs.light-ai.app/webhooks/verify
  - https://docs.light-ai.app/webhooks/events
  - https://docs.light-ai.app/pagination
  - https://docs.light-ai.app/api-reference/webhooks
---

# Webhooks

> Signed, retried and replayable event delivery to your HTTPS endpoint, compatible with Zapier, Make and n8n.

Light AI sends an HTTP POST to your endpoint for each event you subscribe to (27 types, see the [event catalog](https://docs.light-ai.app/webhooks/events)). Endpoints are created in **Settings → Developers → Webhooks**, or through the API with the opt-in `webhooks:manage` scope.

## Subscribe

curl:

```bash
curl -X POST "https://api.light-ai.app/v1/webhook_endpoints" \
  -H "Authorization: Bearer $LIGHT_AI_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://hooks.example.com/light-ai",
  "events": [
    "lead.created",
    "quote.approved"
  ],
  "description": "CRM sync"
}'
```

JavaScript:

```javascript
const response = await fetch('https://api.light-ai.app/v1/webhook_endpoints', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LIGHT_AI_API_KEY}`,
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://hooks.example.com/light-ai",
    "events": [
      "lead.created",
      "quote.approved"
    ],
    "description": "CRM sync"
  }),
});
const data = await response.json();
```

Python:

```python
import os, uuid, requests

response = requests.post(
    "https://api.light-ai.app/v1/webhook_endpoints",
    headers={
        "Authorization": f"Bearer {os.environ['LIGHT_AI_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "url": "https://hooks.example.com/light-ai",
        "events": [
            "lead.created",
            "quote.approved"
        ],
        "description": "CRM sync"
    },
)
data = response.json()
```

- `url`: HTTPS on port 443 or 8443, a public host. Light AI never follows redirects.
- `events`: the types to receive, or `["*"]` for every type your scopes can read.
- `secret` (`whsec_…`) is returned once. Subscribing the same URL and events again returns the same endpoint with a fresh secret; the old one keeps working for 24 hours (REST-hook friendly).
- `static_header`: an optional fixed header (for n8n Header Auth), stored encrypted.
- `exclude_own_origin`: skip events caused by this key's own writes. Default `false`.

## The request

| Header | Value |
| --- | --- |
| `webhook-id` | The event id (`evt_…`), the same on every retry and replay. De-duplicate on it. |
| `webhook-timestamp` | Unix seconds when this attempt was signed. |
| `webhook-signature` | Space-separated `v1,<base64 HMAC-SHA256>` over `{id}.{timestamp}.{body}`. See [Verify signatures](https://docs.light-ai.app/webhooks/verify). |
| `Light-AI-Event-Type` | The event type, e.g. `quote.approved`. |
| `Light-AI-Delivery` | The delivery id (`whd_…`), for the delivery log. |
| `Light-AI-Organization` | The contractor organization (`org_…`). |

## The envelope

`data.object` is the object as it is when the delivery is sent (rendered at delivery time), `data.previous_attributes` holds changed fields for updates, and `origin` says who caused the event: `app`, `embed` (website form), `portal` (the homeowner), `api`, `inbound` or `system`. Partner deliveries carry `grant` with your `external_reference`.

**lead.created**

```json
{
  "id": "evt_3f9c2a7be41d…",
  "object": "event",
  "api_version": "2026-11-01",
  "created": "2026-10-12T14:03:11.000Z",
  "livemode": false,
  "organization": {
    "id": "org_3f9c2a7be41d…",
    "name": "Dana Reyes"
  },
  "grant": {
    "id": "grant_3f9c2a7be41d…",
    "external_reference": "string",
    "metadata": {}
  },
  "origin": {
    "type": "app",
    "principal_id": "string",
    "source_system": "string",
    "external_ref": {
      "system": "string",
      "namespace": "string",
      "id": "string"
    }
  },
  "type": "lead.created",
  "data": {
    "object": {
      "id": "lead_3f9c2a7be41d…",
      "object": "lead",
      "livemode": false,
      "status": "new",
      "held": true,
      "needs_address": true,
      "source": "website_widget",
      "source_detail": "string",
      "contact": {
        "first_name": "Dana",
        "last_name": "Reyes",
        "email": "dana@example.com",
        "email_raw": "string",
        "phone": "+15125550134",
        "phone_raw": "string",
        "phone_verified": true
      },
      "address": {
        "line1": "4102 Oak Hollow Dr",
        "line2": "string",
        "city": "Austin",
        "region": "TX",
        "postal_code": "78731",
        "country": "US",
        "formatted": "string",
        "lat": 0,
        "lng": 0
      },
      "service_interest": "christmas_lights",
      "service_interest_raw": "string",
      "message": "string",
      "attribution": {},
      "consent": {},
      "custom_fields": {},
      "notes": "string",
      "status_reason": "string",
      "client_id": "cli_3f9c2a7be41d…",
      "property_id": "prop_3f9c2a7be41d…",
      "possible_client_match": {
        "client_id": "cli_3f9c2a7be41d…"
      },
      "project": {
        "id": "prj_3f9c2a7be41d…",
        "status": "intake",
        "pipeline_stage": "request",
        "app_url": "https://example.com/…"
      },
      "form": {
        "id": "form_3f9c2a7be41d…",
        "name": "Dana Reyes"
      },
      "mockup_status": "not_requested",
      "form_answers": [
        {
          "key": "surfaces",
          "question": "What should we light?",
          "group": "lighting",
          "type": "multi_choice",
          "value": [
            "eaves",
            "garage"
          ],
          "labels": [
            "Roof peaks + gutters",
            "Garage doors"
          ]
        }
      ],
      "preview_looks": [
        "warm_white"
      ],
      "offer": {
        "template": "black_friday",
        "headline": "Black Friday: 20% off installs",
        "discount_type": "percent",
        "discount_percent": 20,
        "discount_amount": null,
        "label": "20% off",
        "ends_at": "2026-11-30T05:59:00Z",
        "book_by": "Book by Dec 1 for install before Christmas",
        "captured_at": "2026-11-27T14:03:11Z"
      },
      "assets": {
        "photos": [
          {
            "id": "string",
            "url": "https://example.com/…",
            "expires_at": "2026-10-12T14:03:11.000Z"
          }
        ],
        "mockups": [
          {
            "id": "string",
            "url": "https://example.com/…",
            "expires_at": "2026-10-12T14:03:11.000Z",
            "look": "warm_white"
          }
        ]
      },
      "external_refs": [
        {
          "system": "string",
          "namespace": "string",
          "id": "string",
          "owner": "self",
          "read_only": true
        }
      ],
      "touch_count": 1,
      "last_touched_at": "2026-10-12T14:03:11.000Z",
      "submitted_at": "2026-10-12T14:03:11.000Z",
      "created_at": "2026-10-12T14:03:11.000Z",
      "updated_at": "2026-10-12T14:03:11.000Z",
      "replayed": true,
      "fixture": true
    },
    "previous_attributes": {}
  }
}
```

## Respond fast

Answer any `2xx` within **10 seconds**, then process asynchronously. A `410 Gone` disables the endpoint at once. Anything else (or a timeout) is retried.

## Delivery semantics

- **At least once.** The same event can arrive twice (a retry after a timeout, a replay), always with the same `webhook-id`.
- **Not ordered.** Compare `data.object.updated_at` (and `data.stage_change.sequence` for `project.stage_changed`) with what you stored, skip older ones, and upsert by `data.object.id`.
- Large quotes (over 256 KB) arrive with `truncated: true` and empty `line_items`: read the full quote with `GET /v1/quotes/{id}`.

## Retries

A failed attempt is retried 10 times: after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours, then 24 hours three times, each ±20% jitter (about 4 days in all). Then the delivery is `abandoned`.

An endpoint with no success for 72 hours and at least 20 failures is disabled. The owners get an e-mail, and endpoints that subscribe to `webhook_endpoint.disabled` get that event.

## Logs, retry and replay

- Deliveries are logged for 30 days: `GET /v1/webhook_endpoints/{id}/deliveries` and Settings → Developers.
- `POST /v1/webhook_deliveries/{id}/retry` sends one delivery again now.
- `POST /v1/webhook_endpoints/{id}/replay` re-queues up to 5,000 events from the last 30 days (by default only those not yet delivered, including events missed while the endpoint was disabled). Replays reuse the original `webhook-id`.

curl:

```bash
curl -X POST "https://api.light-ai.app/v1/webhook_endpoints/we_…/replay" \
  -H "Authorization: Bearer $LIGHT_AI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "created_after": "2026-10-10T00:00:00Z",
  "types": [
    "quote.approved"
  ]
}'
```

JavaScript:

```javascript
const response = await fetch('https://api.light-ai.app/v1/webhook_endpoints/we_…/replay', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LIGHT_AI_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "created_after": "2026-10-10T00:00:00Z",
    "types": [
      "quote.approved"
    ]
  }),
});
const data = await response.json();
```

Python:

```python
import os, requests

response = requests.post(
    "https://api.light-ai.app/v1/webhook_endpoints/we_…/replay",
    headers={
        "Authorization": f"Bearer {os.environ['LIGHT_AI_API_KEY']}",
    },
    json={
        "created_after": "2026-10-10T00:00:00Z",
        "types": [
            "quote.approved"
        ]
    },
)
data = response.json()
```

## Loops

When your integration writes to Light AI and also listens to its events, set `source_system` on your writes (e.g. `jobber`) and skip events whose `origin.source_system` is yours, or set `exclude_own_origin: true` on the endpoint.

## Security

- Always verify the signature; reject timestamps older than 5 minutes.
- Endpoints cannot target private networks or Light AI hosts. Owners are e-mailed for every endpoint created through the API.
- Payloads never contain image URLs or customer quote links unless the endpoint holds the opt-in scopes.
