> For the complete documentation index, see [llms.txt](https://docs.projectsim.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.projectsim.ai/api-reference/webhooks.md).

# Webhooks

## List Webhooks

> List your registered webhook endpoints.\
> \
> Signing secrets are never returned here; they are shown only when\
> the webhook is created.

```json
{"openapi":"3.1.0","info":{"title":"projectsim API","version":"1.0.0-beta"},"security":[{"ProjectSimToken":[]}],"components":{"securitySchemes":{"ProjectSimToken":{"bearerFormat":"kps_live_...","description":"A per-user API token. The tenant is the user, so no request body carries a user id. Tokens without the `kps_` prefix are rejected before any lookup.","scheme":"bearer","type":"http"}},"schemas":{"WebhookListResponse":{"properties":{"webhooks":{"items":{"$ref":"#/components/schemas/WebhookResponse"},"title":"Webhooks","type":"array"}},"required":["webhooks"],"title":"WebhookListResponse","type":"object"},"WebhookResponse":{"properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"events":{"items":{"type":"string"},"title":"Events","type":"array"},"id":{"title":"Id","type":"string"},"url":{"title":"Url","type":"string"}},"required":["id","url","events","created_at"],"title":"WebhookResponse","type":"object"},"Problem":{"description":"An RFC 7807 error, the shape every failure uses.\n\nDeclared explicitly because the handlers replace FastAPI's default\nvalidation response; without this the spec would advertise\n`HTTPValidationError`, whose `detail` is a list rather than a string,\nand every generated client would crash parsing a 422.","properties":{"detail":{"description":"Human-readable explanation of this occurrence.","title":"Detail","type":"string"},"errors":{"anyOf":[{"items":{"$ref":"#/components/schemas/ValidationDetail"},"type":"array"},{"type":"null"}],"description":"Present on a 422; one entry per failing field.","title":"Errors"},"instance":{"description":"Identifies this occurrence; quote it to support.","title":"Instance","type":"string"},"path":{"title":"Path","type":"string"},"request_id":{"description":"Also returned in the X-Request-Id response header.","title":"Request Id","type":"string"},"status":{"title":"Status","type":"integer"},"title":{"title":"Title","type":"string"},"type":{"description":"Stable URI identifying the error class.","title":"Type","type":"string"}},"required":["type","title","status","detail","instance","path","request_id"],"title":"Problem","type":"object"},"ValidationDetail":{"description":"One field-level validation failure.","properties":{"loc":{"description":"Dotted path to the offending field.","title":"Loc","type":"string"},"msg":{"title":"Msg","type":"string"},"type":{"title":"Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationDetail","type":"object"}}},"paths":{"/v1/webhooks":{"get":{"description":"List your registered webhook endpoints.\n\nSigning secrets are never returned here; they are shown only when\nthe webhook is created.","operationId":"list_webhooks","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}},"description":"Successful Response"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Missing or invalid API token"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request body too large"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request failed validation"}},"summary":"List Webhooks","tags":["webhooks"]}}}}
```

## Create Webhook

> Register an endpoint to receive event notifications.\
> \
> The response contains \`secret\`, shown \*\*exactly once\*\* — store it\
> now. Every delivery is signed with it as\
> \`X-ProjectSim-Signature: t=\<timestamp>,v1=\<hmac>\`, computed over\
> \`"{t}." + raw\_request\_body\`. Verify against the raw bytes, never\
> a re-serialised payload.\
> \
> \`events\` accepts specific names, or an \`asset.\*\` /\
> \`variation\_set.\*\` wildcard. The URL must be HTTPS.\
> \
> Respond \`2xx\` within 10 seconds. Failures retry with exponential\
> backoff for about 24 hours and are then dead-lettered. Delivery\
> is at-least-once, so deduplicate before acting.\
> \
> Webhooks are an optimisation — polling is always sufficient.

```json
{"openapi":"3.1.0","info":{"title":"projectsim API","version":"1.0.0-beta"},"security":[{"ProjectSimToken":[]}],"components":{"securitySchemes":{"ProjectSimToken":{"bearerFormat":"kps_live_...","description":"A per-user API token. The tenant is the user, so no request body carries a user id. Tokens without the `kps_` prefix are rejected before any lookup.","scheme":"bearer","type":"http"}},"schemas":{"CreateWebhookRequest":{"additionalProperties":false,"properties":{"events":{"items":{"type":"string"},"maxItems":32,"minItems":1,"title":"Events","type":"array"},"url":{"maxLength":2000,"title":"Url","type":"string"}},"required":["url","events"],"title":"CreateWebhookRequest","type":"object"},"CreatedWebhookResponse":{"properties":{"created_at":{"format":"date-time","title":"Created At","type":"string"},"events":{"items":{"type":"string"},"title":"Events","type":"array"},"id":{"title":"Id","type":"string"},"secret":{"description":"The HMAC-SHA256 signing secret, returned only here and never again. Store it now.","title":"Secret","type":"string"},"url":{"title":"Url","type":"string"}},"required":["id","url","events","created_at","secret"],"title":"CreatedWebhookResponse","type":"object"},"Problem":{"description":"An RFC 7807 error, the shape every failure uses.\n\nDeclared explicitly because the handlers replace FastAPI's default\nvalidation response; without this the spec would advertise\n`HTTPValidationError`, whose `detail` is a list rather than a string,\nand every generated client would crash parsing a 422.","properties":{"detail":{"description":"Human-readable explanation of this occurrence.","title":"Detail","type":"string"},"errors":{"anyOf":[{"items":{"$ref":"#/components/schemas/ValidationDetail"},"type":"array"},{"type":"null"}],"description":"Present on a 422; one entry per failing field.","title":"Errors"},"instance":{"description":"Identifies this occurrence; quote it to support.","title":"Instance","type":"string"},"path":{"title":"Path","type":"string"},"request_id":{"description":"Also returned in the X-Request-Id response header.","title":"Request Id","type":"string"},"status":{"title":"Status","type":"integer"},"title":{"title":"Title","type":"string"},"type":{"description":"Stable URI identifying the error class.","title":"Type","type":"string"}},"required":["type","title","status","detail","instance","path","request_id"],"title":"Problem","type":"object"},"ValidationDetail":{"description":"One field-level validation failure.","properties":{"loc":{"description":"Dotted path to the offending field.","title":"Loc","type":"string"},"msg":{"title":"Msg","type":"string"},"type":{"title":"Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationDetail","type":"object"}}},"paths":{"/v1/webhooks":{"post":{"description":"Register an endpoint to receive event notifications.\n\nThe response contains `secret`, shown **exactly once** — store it\nnow. Every delivery is signed with it as\n`X-ProjectSim-Signature: t=<timestamp>,v1=<hmac>`, computed over\n`\"{t}.\" + raw_request_body`. Verify against the raw bytes, never\na re-serialised payload.\n\n`events` accepts specific names, or an `asset.*` /\n`variation_set.*` wildcard. The URL must be HTTPS.\n\nRespond `2xx` within 10 seconds. Failures retry with exponential\nbackoff for about 24 hours and are then dead-lettered. Delivery\nis at-least-once, so deduplicate before acting.\n\nWebhooks are an optimisation — polling is always sufficient.","operationId":"create_webhook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedWebhookResponse"}}},"description":"Successful Response"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Missing or invalid API token"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request body too large"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request failed validation"}},"summary":"Create Webhook","tags":["webhooks"]}}}}
```

## Delete Webhook

> Unregister a webhook endpoint.\
> \
> \`204\`, and idempotent. Deliveries already queued for it stop.

```json
{"openapi":"3.1.0","info":{"title":"projectsim API","version":"1.0.0-beta"},"security":[{"ProjectSimToken":[]}],"components":{"securitySchemes":{"ProjectSimToken":{"bearerFormat":"kps_live_...","description":"A per-user API token. The tenant is the user, so no request body carries a user id. Tokens without the `kps_` prefix are rejected before any lookup.","scheme":"bearer","type":"http"}},"schemas":{"Problem":{"description":"An RFC 7807 error, the shape every failure uses.\n\nDeclared explicitly because the handlers replace FastAPI's default\nvalidation response; without this the spec would advertise\n`HTTPValidationError`, whose `detail` is a list rather than a string,\nand every generated client would crash parsing a 422.","properties":{"detail":{"description":"Human-readable explanation of this occurrence.","title":"Detail","type":"string"},"errors":{"anyOf":[{"items":{"$ref":"#/components/schemas/ValidationDetail"},"type":"array"},{"type":"null"}],"description":"Present on a 422; one entry per failing field.","title":"Errors"},"instance":{"description":"Identifies this occurrence; quote it to support.","title":"Instance","type":"string"},"path":{"title":"Path","type":"string"},"request_id":{"description":"Also returned in the X-Request-Id response header.","title":"Request Id","type":"string"},"status":{"title":"Status","type":"integer"},"title":{"title":"Title","type":"string"},"type":{"description":"Stable URI identifying the error class.","title":"Type","type":"string"}},"required":["type","title","status","detail","instance","path","request_id"],"title":"Problem","type":"object"},"ValidationDetail":{"description":"One field-level validation failure.","properties":{"loc":{"description":"Dotted path to the offending field.","title":"Loc","type":"string"},"msg":{"title":"Msg","type":"string"},"type":{"title":"Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationDetail","type":"object"}}},"paths":{"/v1/webhooks/{webhook_id}":{"delete":{"description":"Unregister a webhook endpoint.\n\n`204`, and idempotent. Deliveries already queued for it stop.","operationId":"delete_webhook","parameters":[{"in":"path","name":"webhook_id","required":true,"schema":{"title":"Webhook Id","type":"string"}}],"responses":{"204":{"description":"Successful Response"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Missing or invalid API token"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request body too large"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request failed validation"}},"summary":"Delete Webhook","tags":["webhooks"]}}}}
```

## List Deliveries

> List delivery attempts for a webhook, newest first.\
> \
> The audit trail when a delivery is in doubt: \`attempts\`,\
> \`last\_response\_code\` and \`next\_retry\_at\` per event. \`status\` is\
> \`pending\` (in flight or awaiting retry), \`delivered\` (got a 2xx)\
> or \`dead\` (retries exhausted).\
> \
> Cursor-paginated, optionally filtered to one \`status\`. \`counts\`\
> covers every delivery for the webhook, so a filtered page still knows\
> how many there are of each. Delivered rows are pruned after a\
> retention period; dead rows are kept longer for debugging.

```json
{"openapi":"3.1.0","info":{"title":"projectsim API","version":"1.0.0-beta"},"security":[{"ProjectSimToken":[]}],"components":{"securitySchemes":{"ProjectSimToken":{"bearerFormat":"kps_live_...","description":"A per-user API token. The tenant is the user, so no request body carries a user id. Tokens without the `kps_` prefix are rejected before any lookup.","scheme":"bearer","type":"http"}},"schemas":{"DeliveryListResponse":{"properties":{"counts":{"$ref":"#/components/schemas/DeliveryCounts","description":"Every delivery for this webhook by status, whatever `status` filter the page was fetched with."},"deliveries":{"items":{"$ref":"#/components/schemas/DeliveryResponse"},"title":"Deliveries","type":"array"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Null on the last page.","title":"Next Cursor"}},"required":["deliveries"],"title":"DeliveryListResponse","type":"object"},"DeliveryCounts":{"properties":{"dead":{"default":0,"title":"Dead","type":"integer"},"delivered":{"default":0,"title":"Delivered","type":"integer"},"pending":{"default":0,"title":"Pending","type":"integer"}},"title":"DeliveryCounts","type":"object"},"DeliveryResponse":{"properties":{"attempts":{"title":"Attempts","type":"integer"},"created_at":{"format":"date-time","title":"Created At","type":"string"},"delivered_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"title":"Delivered At"},"event":{"title":"Event","type":"string"},"id":{"title":"Id","type":"string"},"last_response_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Response Code"},"next_retry_at":{"anyOf":[{"format":"date-time","type":"string"},{"type":"null"}],"description":"Null once the delivery is terminal.","title":"Next Retry At"},"resource":{"anyOf":[{"$ref":"#/components/schemas/ResourceRef"},{"type":"null"}],"description":"Null for a synthetic test delivery."},"status":{"description":"pending (in flight or awaiting retry) · delivered (got a 2xx) · dead (retries exhausted)","title":"Status","type":"string"}},"required":["id","event","status","attempts","created_at"],"title":"DeliveryResponse","type":"object"},"ResourceRef":{"description":"What a job or delivery is about.","properties":{"id":{"title":"Id","type":"string"},"type":{"description":"asset · variation_set · scene · batch","title":"Type","type":"string"}},"required":["type","id"],"title":"ResourceRef","type":"object"},"Problem":{"description":"An RFC 7807 error, the shape every failure uses.\n\nDeclared explicitly because the handlers replace FastAPI's default\nvalidation response; without this the spec would advertise\n`HTTPValidationError`, whose `detail` is a list rather than a string,\nand every generated client would crash parsing a 422.","properties":{"detail":{"description":"Human-readable explanation of this occurrence.","title":"Detail","type":"string"},"errors":{"anyOf":[{"items":{"$ref":"#/components/schemas/ValidationDetail"},"type":"array"},{"type":"null"}],"description":"Present on a 422; one entry per failing field.","title":"Errors"},"instance":{"description":"Identifies this occurrence; quote it to support.","title":"Instance","type":"string"},"path":{"title":"Path","type":"string"},"request_id":{"description":"Also returned in the X-Request-Id response header.","title":"Request Id","type":"string"},"status":{"title":"Status","type":"integer"},"title":{"title":"Title","type":"string"},"type":{"description":"Stable URI identifying the error class.","title":"Type","type":"string"}},"required":["type","title","status","detail","instance","path","request_id"],"title":"Problem","type":"object"},"ValidationDetail":{"description":"One field-level validation failure.","properties":{"loc":{"description":"Dotted path to the offending field.","title":"Loc","type":"string"},"msg":{"title":"Msg","type":"string"},"type":{"title":"Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationDetail","type":"object"}}},"paths":{"/v1/webhooks/{webhook_id}/deliveries":{"get":{"description":"List delivery attempts for a webhook, newest first.\n\nThe audit trail when a delivery is in doubt: `attempts`,\n`last_response_code` and `next_retry_at` per event. `status` is\n`pending` (in flight or awaiting retry), `delivered` (got a 2xx)\nor `dead` (retries exhausted).\n\nCursor-paginated, optionally filtered to one `status`. `counts`\ncovers every delivery for the webhook, so a filtered page still knows\nhow many there are of each. Delivered rows are pruned after a\nretention period; dead rows are kept longer for debugging.","operationId":"list_deliveries","parameters":[{"in":"path","name":"webhook_id","required":true,"schema":{"title":"Webhook Id","type":"string"}},{"in":"query","name":"limit","required":false,"schema":{"default":50,"maximum":100,"minimum":1,"title":"Limit","type":"integer"}},{"in":"query","name":"cursor","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"description":"Only deliveries in this status.","in":"query","name":"status","required":false,"schema":{"anyOf":[{"enum":["pending","delivered","dead"],"type":"string"},{"type":"null"}],"description":"Only deliveries in this status.","title":"Status"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryListResponse"}}},"description":"Successful Response"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Missing or invalid API token"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request body too large"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request failed validation"}},"summary":"List Deliveries","tags":["webhooks"]}}}}
```

## Test Webhook

> Send a synthetic event to a registered endpoint.\
> \
> Use this to verify your handler end to end before real traffic\
> arrives. The envelope and HMAC signature are identical to a real\
> delivery, and the payload has the shape of an \`asset.completed\`\
> event, with \`"event": "test"\`, \`"test": true\` and a fake \`job\`\
> whose ids refer to nothing.\
> \
> It records a delivery row like any other event. At most\
> \`webhook\_test\_hourly\_limit\` tests an hour per account; more answer\
> \`429\`.

```json
{"openapi":"3.1.0","info":{"title":"projectsim API","version":"1.0.0-beta"},"security":[{"ProjectSimToken":[]}],"components":{"securitySchemes":{"ProjectSimToken":{"bearerFormat":"kps_live_...","description":"A per-user API token. The tenant is the user, so no request body carries a user id. Tokens without the `kps_` prefix are rejected before any lookup.","scheme":"bearer","type":"http"}},"schemas":{"TestWebhookResponse":{"properties":{"delivery_id":{"title":"Delivery Id","type":"string"},"status":{"title":"Status","type":"string"}},"required":["delivery_id","status"],"title":"TestWebhookResponse","type":"object"},"Problem":{"description":"An RFC 7807 error, the shape every failure uses.\n\nDeclared explicitly because the handlers replace FastAPI's default\nvalidation response; without this the spec would advertise\n`HTTPValidationError`, whose `detail` is a list rather than a string,\nand every generated client would crash parsing a 422.","properties":{"detail":{"description":"Human-readable explanation of this occurrence.","title":"Detail","type":"string"},"errors":{"anyOf":[{"items":{"$ref":"#/components/schemas/ValidationDetail"},"type":"array"},{"type":"null"}],"description":"Present on a 422; one entry per failing field.","title":"Errors"},"instance":{"description":"Identifies this occurrence; quote it to support.","title":"Instance","type":"string"},"path":{"title":"Path","type":"string"},"request_id":{"description":"Also returned in the X-Request-Id response header.","title":"Request Id","type":"string"},"status":{"title":"Status","type":"integer"},"title":{"title":"Title","type":"string"},"type":{"description":"Stable URI identifying the error class.","title":"Type","type":"string"}},"required":["type","title","status","detail","instance","path","request_id"],"title":"Problem","type":"object"},"ValidationDetail":{"description":"One field-level validation failure.","properties":{"loc":{"description":"Dotted path to the offending field.","title":"Loc","type":"string"},"msg":{"title":"Msg","type":"string"},"type":{"title":"Type","type":"string"}},"required":["loc","msg","type"],"title":"ValidationDetail","type":"object"}}},"paths":{"/v1/webhooks/{webhook_id}/test":{"post":{"description":"Send a synthetic event to a registered endpoint.\n\nUse this to verify your handler end to end before real traffic\narrives. The envelope and HMAC signature are identical to a real\ndelivery, and the payload has the shape of an `asset.completed`\nevent, with `\"event\": \"test\"`, `\"test\": true` and a fake `job`\nwhose ids refer to nothing.\n\nIt records a delivery row like any other event. At most\n`webhook_test_hourly_limit` tests an hour per account; more answer\n`429`.","operationId":"test_webhook","parameters":[{"in":"path","name":"webhook_id","required":true,"schema":{"title":"Webhook Id","type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestWebhookResponse"}}},"description":"Successful Response"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Missing or invalid API token"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request body too large"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}},"description":"Request failed validation"}},"summary":"Test Webhook","tags":["webhooks"]}}}}
```
