POST /v1/webhook-endpoints
Register a new webhook endpoint. Returns the signing secret once — store it immediately.
POST
/v1/webhook-endpointsPhase 1stableidempotent
- Auth
- X-Api-Key
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.
Headers
Idempotency-Keystring (UUID)optionalReplays within 24h return the original response.
Body
urlstringrequiredHTTPS URL. http://localhost is accepted in dev only.eventsstring[]required1–50 event types from the catalog. See overview for the full list.descriptionstringoptionalHuman label shown in the admin UI + delivery logs.projectIdstring (UUID)optionalIf set, only events for this project are delivered.
Example request
curl -X POST https://api.layers.com/v1/webhook-endpoints \
-H "X-Api-Key: $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"
}'import { randomUUID } from "node:crypto";
const res = await fetch("https://api.layers.com/v1/webhook-endpoints", {
method: "POST",
headers: {
"X-Api-Key": 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.import os, uuid, requests
res = requests.post(
"https://api.layers.com/v1/webhook-endpoints",
headers={
"X-Api-Key": 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.Response
201Created — signing secret returned once
{
"endpoint": {
"id": "d4c71b62-7f08-4dc9-9d2c-8f7e2b9c4411",
"organizationId": "2481fa5c-a404-44ed-a561-565392499abc",
"projectId": null,
"url": "https://your-app.example.com/layers/webhooks",
"description": "prod reconciler",
"status": "active",
"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."
}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. |
| 409 | IDEMPOTENCY_CONFLICT | Idempotency-Key reused with a different body. |
See also
- Webhooks overview — signing, retry, dedupe semantics
- Rotate secret
- Test endpoint