Webhooks API
The Webhooks API has two halves. Outbound subscriptions push Puck events (investigation results, findings, agent lifecycle) to URLs you control. Inbound endpoints let external systems (CrowdStrike alerts, GitHub Actions, generic POST) trigger investigations without going through the console.
All webhook endpoints require the webhooks:manage scope unless noted otherwise.
Related concept page: Webhooks.
POST /v1/webhooks/subscriptions
Create an outbound webhook subscription. Puck will POST a signed JSON payload to url for every event type in events that passes the optional filters.
The secret field is returned exactly once. Copy it immediately — read endpoints never return it. Use the secret to verify the X-Puck-Signature-256 HMAC-SHA256 header on incoming deliveries.
curl https://api.puck.security/v1/webhooks/subscriptions \ -H "Authorization: Bearer $PUCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.example/puck/events", "events": ["investigation.completed", "finding.discovered"], "filters": { "min_severity": "high" } }'const res = await fetch("https://api.puck.security/v1/webhooks/subscriptions", { method: "POST", headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://your-server.example/puck/events", events: ["investigation.completed", "finding.discovered"], filters: { min_severity: "high" }, }),});const { id, secret } = await res.json();// Store secret securely — never returned againNo request parameters or body.
Status: 201 — Subscription created.
Empty response body.
| Status | Meaning |
|---|---|
400 | Validation error. |
403 | API key lacks `webhooks:manage`. |
GET /v1/webhooks/subscriptions
List all outbound webhook subscriptions for the account. Secrets are never included in list or get responses.
curl https://api.puck.security/v1/webhooks/subscriptions \ -H "Authorization: Bearer $PUCK_API_KEY"No request parameters or body.
Status: 200 — Subscription list.
| Field | Type | Required | Description |
|---|---|---|---|
subscriptions | array | yes |
No documented error responses for this endpoint.
GET /v1/webhooks/subscriptions/{id}
Fetch a single subscription by ID. Returns the same shape as the list response — no secret.
curl "https://api.puck.security/v1/webhooks/subscriptions/sub_01hx…" \ -H "Authorization: Bearer $PUCK_API_KEY"No request parameters or body.
Status: 200 — Subscription detail.
Empty response body.
| Status | Meaning |
|---|---|
404 | Not found. |
PATCH /v1/webhooks/subscriptions/{id}
Update an existing subscription. All fields are optional; only the fields you include are changed. To add a new event type without removing existing ones, fetch the current subscription, append to events, and send the full array.
# Pause a subscriptioncurl -X PATCH "https://api.puck.security/v1/webhooks/subscriptions/sub_01hx…" \ -H "Authorization: Bearer $PUCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "active": false }'No request parameters or body.
Status: 200 — Subscription updated.
Empty response body.
| Status | Meaning |
|---|---|
404 | Not found. |
DELETE /v1/webhooks/subscriptions/{id}
Permanently delete a subscription. In-flight deliveries for this subscription are abandoned. Returns 204 No Content on success.
curl -X DELETE "https://api.puck.security/v1/webhooks/subscriptions/sub_01hx…" \ -H "Authorization: Bearer $PUCK_API_KEY"No request parameters or body.
Status: 204 — Deleted.
Empty response body.
| Status | Meaning |
|---|---|
404 | Not found. |
POST /v1/webhooks/subscriptions/{id}/test
Fire a synthetic delivery to verify your endpoint’s wiring. The test payload uses the same shape as a real delivery but with placeholder data. Returns 202 Accepted with a delivery_id you can track via GET /v1/webhooks/deliveries.
curl -X POST "https://api.puck.security/v1/webhooks/subscriptions/sub_01hx…/test" \ -H "Authorization: Bearer $PUCK_API_KEY"const res = await fetch( `https://api.puck.security/v1/webhooks/subscriptions/${id}/test`, { method: "POST", headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}` }, });const { delivery_id } = await res.json();No request parameters or body.
Status: 202 — Test delivery queued.
| Field | Type | Required | Description |
|---|---|---|---|
delivery_id | string | yes |
| Status | Meaning |
|---|---|
404 | Not found. |
GET /v1/webhooks/deliveries
List delivery attempts for outbound subscriptions. Filter by ?status=failed_permanent to see the dead-letter queue. Deliveries are retained for 30 days.
# Dead-letter queue for a specific subscriptioncurl "https://api.puck.security/v1/webhooks/deliveries?status=failed_permanent&subscription_id=sub_01hx…" \ -H "Authorization: Bearer $PUCK_API_KEY"| Field | In | Type | Required | Description |
|---|---|---|---|---|
status | query | object | no | |
subscription_id | query | string | no | |
limit | query | integer | no |
Status: 200 — Deliveries.
| Field | Type | Required | Description |
|---|---|---|---|
deliveries | array | yes |
No documented error responses for this endpoint.
POST /v1/webhooks/deliveries/{id}/replay
Reset a delivery to pending so the worker retries the POST to your endpoint. The replay reuses the original event_id — your endpoint must deduplicate on event_id to avoid processing the same event twice.
curl -X POST "https://api.puck.security/v1/webhooks/deliveries/dlv_01hx…/replay" \ -H "Authorization: Bearer $PUCK_API_KEY"No request parameters or body.
Status: 202 — Replay queued.
| Field | Type | Required | Description |
|---|---|---|---|
delivery_id | string | yes | |
status | string | yes |
| Status | Meaning |
|---|---|
404 | Delivery not found. |
POST /v1/webhooks/incoming/endpoints
Create an inbound webhook endpoint. Puck generates a per-endpoint URL secret and returns the full receiver URL exactly once. Supported providers: generic, crowdstrike, github.
Optionally supply a provider_signing_secret (e.g. the GitHub webhook secret). When set, Puck verifies the provider’s signature on each inbound POST.
curl https://api.puck.security/v1/webhooks/incoming/endpoints \ -H "Authorization: Bearer $PUCK_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "provider": "github", "provider_signing_secret": "gh_secret_abc123", "default_depth": "standard", "active": true }'const res = await fetch("https://api.puck.security/v1/webhooks/incoming/endpoints", { method: "POST", headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ provider: "github", provider_signing_secret: process.env.GITHUB_WEBHOOK_SECRET, default_depth: "standard", active: true, }),});const { id, url } = await res.json();// url is the full receiver URL — store it securely, configure it in GitHub| Field | In | Type | Required | Description |
|---|---|---|---|---|
provider | body | string | yes | |
provider_signing_secret | body | string | no | Provider-side signing secret (e.g. GitHub webhook secret). Optional. Adds integrity verification on top of URL auth. |
default_scope | body | object | no | |
default_depth | body | object | no | |
active | body | boolean | no |
Status: 201 — Endpoint created.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | |
provider | string | yes | |
url | string | yes | Full receiver path with the URL secret. Returned exactly once. |
url_key_prefix | string | yes | |
active | boolean | yes | |
default_scope | object | no | |
default_depth | string | no | |
created_at | string | yes |
No documented error responses for this endpoint.
GET /v1/webhooks/incoming/endpoints
List all inbound webhook endpoints for the account. URL secrets are never included — only the url_key_prefix for identification.
curl https://api.puck.security/v1/webhooks/incoming/endpoints \ -H "Authorization: Bearer $PUCK_API_KEY"No request parameters or body.
Status: 200 — List of endpoints (without URL secrets).
| Field | Type | Required | Description |
|---|---|---|---|
endpoints | array | yes |
No documented error responses for this endpoint.
DELETE /v1/webhooks/incoming/endpoints/{id}
Permanently delete an inbound endpoint. Any further POSTs to the endpoint’s receiver URL return 401 Unauthorized. Returns 204 No Content on success.
curl -X DELETE "https://api.puck.security/v1/webhooks/incoming/endpoints/ep_01hx…" \ -H "Authorization: Bearer $PUCK_API_KEY"No request parameters or body.
Status: 204 — Deleted.
Empty response body.
| Status | Meaning |
|---|---|
404 | Not found. |
POST /v1/webhooks/incoming/{provider}/{key}
Receive an inbound event from an external system. This is the receiver URL that Puck generates when you create an inbound endpoint. You configure this URL in your third-party tool (e.g. as a GitHub webhook payload URL); you do not call it directly from application code.
The {provider} path segment must match the provider set when the endpoint was created. The {key} segment is the URL secret. Mismatches return 401.
On success: returns 202 Accepted with { "investigation_id": "..." } for known event types, or 202 with { "ignored": true, "reason": "..." } for unrecognised event types. A failed signature check (when provider_signing_secret was set) returns 401.
# Example: posting a generic eventcurl -X POST "https://api.puck.security/v1/webhooks/incoming/generic/abc123secret…" \ -H "Content-Type: application/json" \ -d '{ "query": "check the host that just alerted", "agent_ids": ["agent_01hx…"] }'No request parameters or body.
Status: 202 — Trigger queued (or event ignored).
Empty response body.
| Status | Meaning |
|---|---|
401 | Invalid URL key, provider mismatch, or signature mismatch. |
404 | Unknown provider. |