---
title: "Subscribe a webhook endpoint"
url: https://docs.light-ai.app/api-reference/webhooks/create-webhook-endpoint.md
canonical_url: https://docs.light-ai.app/api-reference/webhooks/create-webhook-endpoint
last_updated: 2026-10-13
summary: "POST /v1/webhook_endpoints: REST-hook compatible (Zapier, Make, n8n)."
api_status: beta
related:
  - https://docs.light-ai.app/api-reference/webhooks
---

# Subscribe a webhook endpoint

> POST /v1/webhook_endpoints: REST-hook compatible (Zapier, Make, n8n).

`POST /v1/webhook_endpoints`

REST-hook compatible (Zapier, Make, n8n). Returns the signing secret once (`secret`). Subscribing the same URL and events again returns the same endpoint with a freshly rotated secret (HTTP 200); the previous secret keeps working for 24 hours. A re-subscribe also applies the static_header (e.g. a rotated n8n credential), exclude_own_origin and description it sends; fields it omits keep their stored values. The organization's owners are notified of every endpoint created through the API. Partner keys create an endpoint for one connected organization (send `Light-AI-Organization`); a partner-wide endpoint (every connected organization, with per-organization lanes and optional grant filters) is created by the partner owner in Settings → Developers with a second factor, never with a key, so a leaked key can never open a channel to every contractor at once. While the organization has disabled one of your endpoints to a receiver (status_reason `disabled_by_organization`), creating another to that receiver answers 409 stale_state.

|   |   |
| --- | --- |
| Scope | `webhooks:manage` |
| Credential | API key: `Authorization: Bearer lai_live_…` (or `lai_test_…`). Partner keys add `Light-AI-Organization`. |
| Test mode | Works with test keys (test objects only). |
| Idempotency | `Idempotency-Key` supported. See [Idempotency](https://docs.light-ai.app/idempotency). |

## Headers

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `Light-AI-Organization` | string |  | Partner keys: the connected organization to act for (`org_…`). Organization keys may omit it. Id prefix `org_`. |
| `Light-AI-Version` | string |  | Pin a dated API version (defaults to the key's version). One of: `2026-11-01`. |
| `Idempotency-Key` | string |  | Retry-safe writes: the same key and body replay the first response for 24 hours (`Idempotent-Replayed: true`). 8–128 characters. |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | https only, port 443 or 8443, public host. At most 2048 characters. |
| `events` | string[] | yes | Event types, or ["*"] for every type your scopes can read and the organization's plan delivers live. A live endpoint refuses a listed type its plan does not deliver (403 plan_upgrade_required naming the type). Only listed types are delivered: list api_key.expiring and webhook_endpoint.disabled (or use ["*"]) to receive those notices here; a ping reaches only the endpoint it is sent to. 1–40 items. |
| `description` | string |  | At most 500 characters. |
| `static_header` | object |  | A fixed header sent with every delivery (e.g. for n8n Header Auth). Stored encrypted; never shown again. It belongs to its URL: a request that changes url must send static_header again (or null to remove it), otherwise 422 at /static_header. |
| `static_header.name` | string | yes |  |
| `static_header.value` | string | yes | Visible ASCII (spaces allowed inside); surrounding whitespace is trimmed. Stored encrypted. Responses show only its last 4 characters, and only when it is at least 12 characters long. 1–1000 characters. |
| `exclude_own_origin` | boolean |  | Skip events caused by this key's own writes (loop prevention). Default false. |

## Response 200

Re-subscribed: same endpoint, rotated secret, the sent static_header, exclude_own_origin and description applied.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Id prefix `we_`. |
| `object` | string | yes | Always `webhook_endpoint`. |
| `url` | string | yes |  |
| `description` | string \| null | yes |  |
| `events` | string[] | yes | One of: `lead.created`, `lead.touched`, `lead.updated`, `lead.mockup_ready`, `client.created`, `client.updated`, `client.deleted`, `project.created`, `project.stage_changed`, `quote.published`, `quote.viewed`, `quote.external_link_clicked`, `quote.change_requested`, `quote.approved`, `quote.declined`, `quote.expired`, `quote.link_disabled`, `quote.link_enabled`, `quote.superseded`, `mockup.completed`, `mockup.failed`, `organization.entitlement_changed`, `grant.created`, `grant.revoked`, `api_key.expiring`, `webhook_endpoint.disabled`, `ping`. |
| `unentitled_events` | string[] | yes | Subscribed types the organization's current plan does not deliver live (e.g. after a downgrade). They resume when the plan includes them. One of: `lead.created`, `lead.touched`, `lead.updated`, `lead.mockup_ready`, `client.created`, `client.updated`, `client.deleted`, `project.created`, `project.stage_changed`, `quote.published`, `quote.viewed`, `quote.external_link_clicked`, `quote.change_requested`, `quote.approved`, `quote.declined`, `quote.expired`, `quote.link_disabled`, `quote.link_enabled`, `quote.superseded`, `mockup.completed`, `mockup.failed`, `organization.entitlement_changed`, `grant.created`, `grant.revoked`, `api_key.expiring`, `webhook_endpoint.disabled`, `ping`. |
| `livemode` | boolean | yes | false for objects created with a test key. |
| `status` | string | yes | One of: `enabled`, `disabled`, `disabled_by_system`, `disabled_by_revocation`, `suspended`. |
| `status_reason` | string \| null | yes | Why it is not enabled. `disabled_by_organization`: the contractor disabled it in Settings → Developers. Until they enable it there (it then returns to the state it had before their disable) or remove it there, the API cannot enable, re-point or delete it, nor create or point another endpoint at the same receiver (the same host, port and path; query string, case, repeated or trailing slashes and :443 ignored): each answers 409 stale_state with current_state `disabled_by_organization`. A partner's partner-level endpoints at that receiver receive none of that contractor's events meanwhile (its organization.entitlement_changed included; grant notices addressed to the partner still arrive). A different path or host is a different receiver. This stops deliveries, not access: a partner's key still reads through its grant (the contractor suspends or revokes the grant to stop the partner). `gone` / `circuit_breaker`: Light AI stopped it (410, or 72 hours of failures). `grant_suspended`, `grant_revoked`, key and security reasons: access was suspended or revoked. |
| `disabled_while_suspended` | boolean | yes |  |
| `api_version` | string | yes |  |
| `exclude_own_origin` | boolean | yes |  |
| `scopes` | string[] | yes | One of: `leads:read`, `leads:write`, `clients:read`, `clients:write`, `projects:read`, `projects:write`, `quotes:read`, `quotes:write`, `quotes:share_link`, `quotes:asset_links`, `webhooks:manage`, `events:read`, `mockups:read`, `mockups:write`, `forms:read`, `forms:write`. |
| `kind` | string | yes | One of: `organization`, `partner`. |
| `grant_ids` | string[] \| null | yes |  |
| `static_header` | object \| null | yes |  |
| `static_header.name` | string | yes |  |
| `static_header.value_redacted` | string | yes | `…` plus the last 4 characters of a value of 12 or more characters; `…` alone for a shorter value. |
| `created_by` | object | yes |  |
| `created_by.type` | string | yes | One of: `api_key`, `user`. |
| `created_by.id` | string \| null | yes |  |
| `created_by.name` | string \| null | yes |  |
| `created_by.partner` | string \| null |  |  |
| `lane` | string | yes | One of: `normal`, `degraded`. |
| `backlog` | integer | yes |  |
| `oldest_pending_at` | timestamp \| null | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `last_success_at` | timestamp \| null | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `last_failure_at` | timestamp \| null | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `created_at` | timestamp | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `updated_at` | timestamp | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `secret` | string |  | The signing secret (whsec_…). Returned only when created, re-subscribed or rotated. |

## Response 201

Endpoint created (with its secret).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | Id prefix `we_`. |
| `object` | string | yes | Always `webhook_endpoint`. |
| `url` | string | yes |  |
| `description` | string \| null | yes |  |
| `events` | string[] | yes | One of: `lead.created`, `lead.touched`, `lead.updated`, `lead.mockup_ready`, `client.created`, `client.updated`, `client.deleted`, `project.created`, `project.stage_changed`, `quote.published`, `quote.viewed`, `quote.external_link_clicked`, `quote.change_requested`, `quote.approved`, `quote.declined`, `quote.expired`, `quote.link_disabled`, `quote.link_enabled`, `quote.superseded`, `mockup.completed`, `mockup.failed`, `organization.entitlement_changed`, `grant.created`, `grant.revoked`, `api_key.expiring`, `webhook_endpoint.disabled`, `ping`. |
| `unentitled_events` | string[] | yes | Subscribed types the organization's current plan does not deliver live (e.g. after a downgrade). They resume when the plan includes them. One of: `lead.created`, `lead.touched`, `lead.updated`, `lead.mockup_ready`, `client.created`, `client.updated`, `client.deleted`, `project.created`, `project.stage_changed`, `quote.published`, `quote.viewed`, `quote.external_link_clicked`, `quote.change_requested`, `quote.approved`, `quote.declined`, `quote.expired`, `quote.link_disabled`, `quote.link_enabled`, `quote.superseded`, `mockup.completed`, `mockup.failed`, `organization.entitlement_changed`, `grant.created`, `grant.revoked`, `api_key.expiring`, `webhook_endpoint.disabled`, `ping`. |
| `livemode` | boolean | yes | false for objects created with a test key. |
| `status` | string | yes | One of: `enabled`, `disabled`, `disabled_by_system`, `disabled_by_revocation`, `suspended`. |
| `status_reason` | string \| null | yes | Why it is not enabled. `disabled_by_organization`: the contractor disabled it in Settings → Developers. Until they enable it there (it then returns to the state it had before their disable) or remove it there, the API cannot enable, re-point or delete it, nor create or point another endpoint at the same receiver (the same host, port and path; query string, case, repeated or trailing slashes and :443 ignored): each answers 409 stale_state with current_state `disabled_by_organization`. A partner's partner-level endpoints at that receiver receive none of that contractor's events meanwhile (its organization.entitlement_changed included; grant notices addressed to the partner still arrive). A different path or host is a different receiver. This stops deliveries, not access: a partner's key still reads through its grant (the contractor suspends or revokes the grant to stop the partner). `gone` / `circuit_breaker`: Light AI stopped it (410, or 72 hours of failures). `grant_suspended`, `grant_revoked`, key and security reasons: access was suspended or revoked. |
| `disabled_while_suspended` | boolean | yes |  |
| `api_version` | string | yes |  |
| `exclude_own_origin` | boolean | yes |  |
| `scopes` | string[] | yes | One of: `leads:read`, `leads:write`, `clients:read`, `clients:write`, `projects:read`, `projects:write`, `quotes:read`, `quotes:write`, `quotes:share_link`, `quotes:asset_links`, `webhooks:manage`, `events:read`, `mockups:read`, `mockups:write`, `forms:read`, `forms:write`. |
| `kind` | string | yes | One of: `organization`, `partner`. |
| `grant_ids` | string[] \| null | yes |  |
| `static_header` | object \| null | yes |  |
| `static_header.name` | string | yes |  |
| `static_header.value_redacted` | string | yes | `…` plus the last 4 characters of a value of 12 or more characters; `…` alone for a shorter value. |
| `created_by` | object | yes |  |
| `created_by.type` | string | yes | One of: `api_key`, `user`. |
| `created_by.id` | string \| null | yes |  |
| `created_by.name` | string \| null | yes |  |
| `created_by.partner` | string \| null |  |  |
| `lane` | string | yes | One of: `normal`, `degraded`. |
| `backlog` | integer | yes |  |
| `oldest_pending_at` | timestamp \| null | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `last_success_at` | timestamp \| null | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `last_failure_at` | timestamp \| null | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `created_at` | timestamp | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `updated_at` | timestamp | yes | RFC 3339 timestamp (UTC, milliseconds). |
| `secret` | string |  | The signing secret (whsec_…). Returned only when created, re-subscribed or rotated. |

## Errors

| Status | Meaning |
| --- | --- |
| `400` | Bad request. [`bad_request`](https://docs.light-ai.app/errors#bad_request), [`test_mode_mismatch`](https://docs.light-ai.app/errors#test_mode_mismatch), [`test_mode_unsupported`](https://docs.light-ai.app/errors#test_mode_unsupported), [`unsupported_api_version`](https://docs.light-ai.app/errors#unsupported_api_version) |
| `401` | Missing or invalid credential. [`authentication_required`](https://docs.light-ai.app/errors#authentication_required), [`invalid_api_key`](https://docs.light-ai.app/errors#invalid_api_key), [`api_key_revoked`](https://docs.light-ai.app/errors#api_key_revoked), [`api_key_expired`](https://docs.light-ai.app/errors#api_key_expired), [`api_key_suspended`](https://docs.light-ai.app/errors#api_key_suspended) |
| `403` | Not allowed. [`insufficient_scope`](https://docs.light-ai.app/errors#insufficient_scope), [`plan_upgrade_required`](https://docs.light-ai.app/errors#plan_upgrade_required), [`organization_paused`](https://docs.light-ai.app/errors#organization_paused), [`organization_off_season`](https://docs.light-ai.app/errors#organization_off_season), [`api_disabled`](https://docs.light-ai.app/errors#api_disabled), [`api_access_not_enabled`](https://docs.light-ai.app/errors#api_access_not_enabled), [`ip_not_allowed`](https://docs.light-ai.app/errors#ip_not_allowed), [`forbidden`](https://docs.light-ai.app/errors#forbidden) |
| `404` | Not found (also for objects of other organizations and for organizations without an active grant). |
| `409` | Conflict. [`stale_state`](https://docs.light-ai.app/errors#stale_state), [`conflict`](https://docs.light-ai.app/errors#conflict), [`idempotency_in_progress`](https://docs.light-ai.app/errors#idempotency_in_progress), [`idempotency_scope_mismatch`](https://docs.light-ai.app/errors#idempotency_scope_mismatch), [`quote_already_decided`](https://docs.light-ai.app/errors#quote_already_decided), [`quote_not_current`](https://docs.light-ai.app/errors#quote_not_current), [`billing_review_required`](https://docs.light-ai.app/errors#billing_review_required) |
| `413` | Request bodies are limited to 256 KB. |
| `415` | The body is not JSON: send Content-Type: application/json. [`unsupported_media_type`](https://docs.light-ai.app/errors#unsupported_media_type) |
| `422` | Validation failed. [`idempotency_key_reused`](https://docs.light-ai.app/errors#idempotency_key_reused) |
| `429` | Rate limited or daily cap reached. See Retry-After and the RateLimit headers. [`rate_limited`](https://docs.light-ai.app/errors#rate_limited), [`daily_cap_reached`](https://docs.light-ai.app/errors#daily_cap_reached) |
| `500` | Unexpected server error. Safe to retry with exponential backoff; quote the Request-Id to support. [`internal_error`](https://docs.light-ai.app/errors#internal_error) |
| `503` | Temporarily unavailable. Retry with exponential backoff. [`service_unavailable`](https://docs.light-ai.app/errors#service_unavailable) |

## Examples

**Request**

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": "n8n workflow",
  "static_header": {
    "name": "X-N8N-Auth",
    "value": "a-long-random-value"
  },
  "exclude_own_origin": true
}'
```

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": "n8n workflow",
    "static_header": {
      "name": "X-N8N-Auth",
      "value": "a-long-random-value"
    },
    "exclude_own_origin": true
  }),
});
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": "n8n workflow",
      "static_header": {
        "name": "X-N8N-Auth",
        "value": "a-long-random-value"
      },
      "exclude_own_origin": True
    },
    timeout=30,
)
data = response.json()
```

**Response 201**

```json
{
  "id": "we_3f9c2a7be41d…",
  "object": "webhook_endpoint",
  "url": "https://example.com/…",
  "description": "string",
  "events": [
    "lead.created"
  ],
  "unentitled_events": [
    "lead.created"
  ],
  "livemode": false,
  "status": "enabled",
  "status_reason": "string",
  "disabled_while_suspended": true,
  "api_version": "2026-11-01",
  "exclude_own_origin": true,
  "scopes": [
    "leads:read"
  ],
  "kind": "organization",
  "grant_ids": [
    "grant_3f9c2a7be41d…"
  ],
  "static_header": {
    "name": "Dana Reyes",
    "value_redacted": "string"
  },
  "created_by": {
    "type": "api_key",
    "id": "string",
    "name": "Dana Reyes",
    "partner": "string"
  },
  "lane": "normal",
  "backlog": 0,
  "oldest_pending_at": "2026-10-12T14:03:11.000Z",
  "last_success_at": "2026-10-12T14:03:11.000Z",
  "last_failure_at": "2026-10-12T14:03:11.000Z",
  "created_at": "2026-10-12T14:03:11.000Z",
  "updated_at": "2026-10-12T14:03:11.000Z",
  "secret": "string"
}
```
