# POST /v1/webhook-endpoints (/docs/api/reference/webhooks/create-endpoint)



<Endpoint method="POST" path="/v1/webhook-endpoints" auth="Bearer" phase="1" />

Creates a new webhook subscription under the calling org. Pass `projectId` to narrow to a single project (events unrelated to that project are dropped at emit time); omit it for an org-wide endpoint.

The response carries a `signingSecret` **shown exactly once**. Store it in your secrets manager before the response closes - we cannot display it again. Use it to verify every incoming delivery via the signature helper in the [webhooks overview](/docs/api/operational/webhooks#verifying-the-signature).

<Parameters
  title="Headers"
  rows="[
  { name: 'Idempotency-Key', type: 'string (UUID)', description: 'Replays within the idempotency window return the original response.' },
]"
/>

<Parameters
  title="Body"
  rows="[
  { name: 'url', type: 'string', required: true, description: 'HTTPS URL. http://localhost is accepted in dev only.' },
  { name: 'events', type: 'string[]', required: true, description: '1–50 event types from the catalog. See overview for the full list.' },
  { name: 'description', type: 'string', description: 'Human label shown in the admin UI + delivery logs.' },
  { name: 'metadata', type: 'object', description: 'Optional opaque key/value pairs for your own bookkeeping (Stripe-style). Returned on reads; Layers never reads or indexes it.' },
  { name: 'projectId', type: 'string (UUID)', description: 'If set, only events for this project are delivered.' },
  { name: 'scope', type: '&#x22;own&#x22; | &#x22;all_children&#x22;', description: <>Delivery scope. <code>own</code> (default) delivers only this org&apos;s events. <code>all_children</code> is the firehose — this org&apos;s events PLUS every direct child&apos;s, each tagged with <code>data.organizationId</code>. Setting <code>all_children</code> requires an <code>org:admin</code> key.</> },
]"
/>

<Callout type="info">
  **Firehose (`scope: "all_children"`).** If you run customers as [sub-organizations](/docs/api/concepts/organizations), one parent endpoint can receive every child's events instead of registering an endpoint per child. Each delivery carries `data.organizationId` so you can attribute it to the right customer. Only a parent `org:admin` key may open one; a non-admin key requesting it gets `403 FORBIDDEN_SCOPE`.
</Callout>

## Example request [#example-request]

<Tabs items="['curl', 'TypeScript', 'Python']">
  <Tab value="curl">
    ```bash
    curl -X POST https://api.layers.com/v1/webhook-endpoints \
      -H "Authorization: Bearer $LAYERS_API_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://your-app.example.com/layers/webhooks",
        "events": ["job.completed", "job.failed", "content.approved"],
        "description": "prod reconciler"
      }'
    ```
  </Tab>

  <Tab value="TypeScript">
    ```ts
    import { randomUUID } from "node:crypto";

    const res = await fetch("https://api.layers.com/v1/webhook-endpoints", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.LAYERS_API_KEY}`,
        "Idempotency-Key": randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        url: "https://your-app.example.com/layers/webhooks",
        events: ["job.completed", "job.failed", "content.approved"],
        description: "prod reconciler",
      }),
    });
    const { endpoint, signingSecret } = await res.json();
    // Persist `signingSecret` to your secret store immediately.
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import os, uuid, requests

    res = requests.post(
        "https://api.layers.com/v1/webhook-endpoints",
        headers={
            "Authorization": f"Bearer {os.environ[\'LAYERS_API_KEY\']}",
            "Idempotency-Key": str(uuid.uuid4()),
            "Content-Type": "application/json",
        },
        json={
            "url": "https://your-app.example.com/layers/webhooks",
            "events": ["job.completed", "job.failed", "content.approved"],
            "description": "prod reconciler",
        },
    )
    body = res.json()
    # Persist body["signingSecret"] to your secret store immediately.
    ```
  </Tab>
</Tabs>

## Response [#response]

<Response status="201" description="Created - signing secret returned once">
  ```json
  {
    "endpoint": {
      "id": "d4c71b62-7f08-4dc9-9d2c-8f7e2b9c4411",
      "organizationId": "org_2481fa5c-a404-44ed-a561-565392499abc",
      "projectId": null,
      "url": "https://your-app.example.com/layers/webhooks",
      "description": "prod reconciler",
      "status": "active",
      "scope": "own",
      "events": ["job.completed", "job.failed", "content.approved"],
      "apiVersion": "v1",
      "createdAt": "2026-04-20T18:28:00.000Z",
      "updatedAt": "2026-04-20T18:28:00.000Z",
      "lastSuccessAt": null,
      "lastFailureAt": null,
      "consecutiveFailureCount": 0
    },
    "signingSecret": "whsec_s3cret...",
    "warning": "Signing secret shown once. Store it securely - rotate via POST /v1/webhook-endpoints/:id/rotate-secret if compromised."
  }
  ```
</Response>

## Errors [#errors]

| Status | Code                   | When                                                          |
| ------ | ---------------------- | ------------------------------------------------------------- |
| 422    | `VALIDATION`           | URL invalid, events empty, unknown event type, >50 events.    |
| 404    | `NOT_FOUND`            | `projectId` doesn't belong to the calling org.                |
| 403    | `FORBIDDEN_SCOPE`      | `scope: "all_children"` requested without an `org:admin` key. |
| 409    | `IDEMPOTENCY_CONFLICT` | Idempotency-Key reused with a different body.                 |

## See also [#see-also]

* [Webhooks overview](/docs/api/operational/webhooks) - signing, retry, dedupe semantics
* [Rotate secret](/docs/api/reference/webhooks/rotate-secret)
* [Test endpoint](/docs/api/reference/webhooks/test-endpoint)
