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

# Outgoing Webhooks

Manage outgoing webhook subscriptions for organization events.

### Webhook signing

When an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every delivery carries the following headers:

| Header                  | Value                                                                            |
| ----------------------- | -------------------------------------------------------------------------------- |
| `Content-Type`          | `application/json`                                                               |
| `User-Agent`            | `StatusPal-Webhooks/1.0`                                                         |
| `X-StatusPal-Event`     | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |
| `X-StatusPal-Delivery`  | A per-attempt UUID, use it to dedupe retried deliveries                          |
| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>`                              |

The signature covers `"#{t}.#{raw_body}"`, signed with HMAC-SHA256 using the webhook's plaintext signing secret (the `whs_…` value returned **once** when the webhook was created or its secret was rotated). Verify it like this:

```
parsed = parse_header("X-StatusPal-Signature")  # → { t: <int>, v1: <hex> }
signed = `${parsed.t}.${raw_body}`
digest = HMAC_SHA256(secret, signed)
if (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended
if (!constant_time_eq(digest, parsed.v1)) reject
```

### Delivery retries

Each event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts. Any 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network error triggers the next retry. After the third failure the delivery is dropped.

## GET /outgoing\_webhooks

> List outgoing webhooks

````json
{"openapi":"3.1.0","info":{"title":"StatusPal Public API","version":"1"},"tags":[{"name":"Outgoing Webhooks","description":"Manage outgoing webhook subscriptions for organization events.\n\n## Webhook signing\n\nWhen an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every\ndelivery carries the following headers:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `User-Agent` | `StatusPal-Webhooks/1.0` |\n| `X-StatusPal-Event` | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |\n| `X-StatusPal-Delivery` | A per-attempt UUID, use it to dedupe retried deliveries |\n| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>` |\n\nThe signature covers `\"#{t}.#{raw_body}\"`, signed with HMAC-SHA256 using the webhook's plaintext\nsigning secret (the `whs_…` value returned **once** when the webhook was created or its secret\nwas rotated). Verify it like this:\n\n```\nparsed = parse_header(\"X-StatusPal-Signature\")  # → { t: <int>, v1: <hex> }\nsigned = `${parsed.t}.${raw_body}`\ndigest = HMAC_SHA256(secret, signed)\nif (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended\nif (!constant_time_eq(digest, parsed.v1)) reject\n```\n\n## Delivery retries\n\nEach event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts.\nAny 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network\nerror triggers the next retry. After the third failure the delivery is dropped.\n"}],"servers":[{"url":"https://next.statuspal.io/api/v1","description":"Production"},{"url":"http://spage.test:7070/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organization API key — issue one via the Rails console (management UI coming soon)"}},"parameters":{"Page":{"in":"query","name":"page","schema":{"type":"integer","default":1,"minimum":1}},"PerPage":{"in":"query","name":"per_page","schema":{"type":"integer","default":20,"maximum":100,"minimum":1}}},"schemas":{"OutgoingWebhook":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"description":"TypeID-prefixed identifier (e.g. `wbk_01h2x...`)"},"name":{"type":"string"},"url":{"type":"string","format":"uri","description":"HTTPS URL that will receive `POST` deliveries."},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean","default":true},"all_status_pages":{"type":"boolean","default":true,"description":"When `true`, the webhook fires for events on every status page in\nthe organization, including pages created later. When `false`, only\nevents on pages listed in `status_page_subdomains` fire.\n"},"status_page_subdomains":{"type":"array","items":{"type":"string"},"description":"Only present when `all_status_pages` is `false`. Subdomains of the\nspecific status pages this webhook is scoped to.\n"},"last_triggered_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent **successful** delivery."},"last_attempted_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent delivery attempt (success or failure)."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"PaginationMeta":{"type":"object","properties":{"current_page":{"type":"integer"},"per_page":{"type":"integer"},"total_count":{"type":"integer"},"total_pages":{"type":"integer"}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AccountPendingActivation":{"description":"The account is pending activation. A newly created account must be activated by an administrator before its API key can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/outgoing_webhooks":{"get":{"summary":"List outgoing webhooks","operationId":"listOutgoingWebhooks","tags":["Outgoing Webhooks"],"parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PerPage"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OutgoingWebhook"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AccountPendingActivation"}}}}}}
````

## Create an outgoing webhook

> Creates a new outgoing webhook. The response includes the plaintext\
> signing \`secret\` exactly \*\*once\*\* — store it immediately, it cannot be\
> retrieved later. Use \`POST /outgoing\_webhooks/{id}/regenerate\_secret\`\
> to rotate.<br>

````json
{"openapi":"3.1.0","info":{"title":"StatusPal Public API","version":"1"},"tags":[{"name":"Outgoing Webhooks","description":"Manage outgoing webhook subscriptions for organization events.\n\n## Webhook signing\n\nWhen an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every\ndelivery carries the following headers:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `User-Agent` | `StatusPal-Webhooks/1.0` |\n| `X-StatusPal-Event` | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |\n| `X-StatusPal-Delivery` | A per-attempt UUID, use it to dedupe retried deliveries |\n| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>` |\n\nThe signature covers `\"#{t}.#{raw_body}\"`, signed with HMAC-SHA256 using the webhook's plaintext\nsigning secret (the `whs_…` value returned **once** when the webhook was created or its secret\nwas rotated). Verify it like this:\n\n```\nparsed = parse_header(\"X-StatusPal-Signature\")  # → { t: <int>, v1: <hex> }\nsigned = `${parsed.t}.${raw_body}`\ndigest = HMAC_SHA256(secret, signed)\nif (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended\nif (!constant_time_eq(digest, parsed.v1)) reject\n```\n\n## Delivery retries\n\nEach event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts.\nAny 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network\nerror triggers the next retry. After the third failure the delivery is dropped.\n"}],"servers":[{"url":"https://next.statuspal.io/api/v1","description":"Production"},{"url":"http://spage.test:7070/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organization API key — issue one via the Rails console (management UI coming soon)"}},"schemas":{"OutgoingWebhookCreate":{"type":"object","required":["name","url","events"],"properties":{"name":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean","default":true},"all_status_pages":{"type":"boolean","default":true},"status_page_subdomains":{"type":"array","description":"Required when `all_status_pages` is `false`. Subdomains of the\nstatus pages this webhook should fire for. Unknown subdomains\ncause a `422`.\n","items":{"type":"string"}}}},"OutgoingWebhookWithSecret":{"allOf":[{"$ref":"#/components/schemas/OutgoingWebhook"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"Plaintext signing secret (`whs_…`). Returned **only** in responses\nto create and regenerate_secret. Store it immediately — it is not\nretrievable later.\n"}}}]},"OutgoingWebhook":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"description":"TypeID-prefixed identifier (e.g. `wbk_01h2x...`)"},"name":{"type":"string"},"url":{"type":"string","format":"uri","description":"HTTPS URL that will receive `POST` deliveries."},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean","default":true},"all_status_pages":{"type":"boolean","default":true,"description":"When `true`, the webhook fires for events on every status page in\nthe organization, including pages created later. When `false`, only\nevents on pages listed in `status_page_subdomains` fire.\n"},"status_page_subdomains":{"type":"array","items":{"type":"string"},"description":"Only present when `all_status_pages` is `false`. Subdomains of the\nspecific status pages this webhook is scoped to.\n"},"last_triggered_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent **successful** delivery."},"last_attempted_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent delivery attempt (success or failure)."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AccountPendingActivation":{"description":"The account is pending activation. A newly created account must be activated by an administrator before its API key can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UnprocessableEntity":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/outgoing_webhooks":{"post":{"summary":"Create an outgoing webhook","description":"Creates a new outgoing webhook. The response includes the plaintext\nsigning `secret` exactly **once** — store it immediately, it cannot be\nretrieved later. Use `POST /outgoing_webhooks/{id}/regenerate_secret`\nto rotate.\n","operationId":"createOutgoingWebhook","tags":["Outgoing Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutgoingWebhookCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OutgoingWebhookWithSecret"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AccountPendingActivation"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}}}}
````

## GET /outgoing\_webhooks/{id}

> Get an outgoing webhook

````json
{"openapi":"3.1.0","info":{"title":"StatusPal Public API","version":"1"},"tags":[{"name":"Outgoing Webhooks","description":"Manage outgoing webhook subscriptions for organization events.\n\n## Webhook signing\n\nWhen an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every\ndelivery carries the following headers:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `User-Agent` | `StatusPal-Webhooks/1.0` |\n| `X-StatusPal-Event` | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |\n| `X-StatusPal-Delivery` | A per-attempt UUID, use it to dedupe retried deliveries |\n| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>` |\n\nThe signature covers `\"#{t}.#{raw_body}\"`, signed with HMAC-SHA256 using the webhook's plaintext\nsigning secret (the `whs_…` value returned **once** when the webhook was created or its secret\nwas rotated). Verify it like this:\n\n```\nparsed = parse_header(\"X-StatusPal-Signature\")  # → { t: <int>, v1: <hex> }\nsigned = `${parsed.t}.${raw_body}`\ndigest = HMAC_SHA256(secret, signed)\nif (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended\nif (!constant_time_eq(digest, parsed.v1)) reject\n```\n\n## Delivery retries\n\nEach event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts.\nAny 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network\nerror triggers the next retry. After the third failure the delivery is dropped.\n"}],"servers":[{"url":"https://next.statuspal.io/api/v1","description":"Production"},{"url":"http://spage.test:7070/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organization API key — issue one via the Rails console (management UI coming soon)"}},"schemas":{"OutgoingWebhook":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"description":"TypeID-prefixed identifier (e.g. `wbk_01h2x...`)"},"name":{"type":"string"},"url":{"type":"string","format":"uri","description":"HTTPS URL that will receive `POST` deliveries."},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean","default":true},"all_status_pages":{"type":"boolean","default":true,"description":"When `true`, the webhook fires for events on every status page in\nthe organization, including pages created later. When `false`, only\nevents on pages listed in `status_page_subdomains` fire.\n"},"status_page_subdomains":{"type":"array","items":{"type":"string"},"description":"Only present when `all_status_pages` is `false`. Subdomains of the\nspecific status pages this webhook is scoped to.\n"},"last_triggered_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent **successful** delivery."},"last_attempted_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent delivery attempt (success or failure)."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AccountPendingActivation":{"description":"The account is pending activation. A newly created account must be activated by an administrator before its API key can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/outgoing_webhooks/{id}":{"get":{"summary":"Get an outgoing webhook","operationId":"getOutgoingWebhook","tags":["Outgoing Webhooks"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OutgoingWebhook"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AccountPendingActivation"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
````

## DELETE /outgoing\_webhooks/{id}

> Delete an outgoing webhook

````json
{"openapi":"3.1.0","info":{"title":"StatusPal Public API","version":"1"},"tags":[{"name":"Outgoing Webhooks","description":"Manage outgoing webhook subscriptions for organization events.\n\n## Webhook signing\n\nWhen an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every\ndelivery carries the following headers:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `User-Agent` | `StatusPal-Webhooks/1.0` |\n| `X-StatusPal-Event` | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |\n| `X-StatusPal-Delivery` | A per-attempt UUID, use it to dedupe retried deliveries |\n| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>` |\n\nThe signature covers `\"#{t}.#{raw_body}\"`, signed with HMAC-SHA256 using the webhook's plaintext\nsigning secret (the `whs_…` value returned **once** when the webhook was created or its secret\nwas rotated). Verify it like this:\n\n```\nparsed = parse_header(\"X-StatusPal-Signature\")  # → { t: <int>, v1: <hex> }\nsigned = `${parsed.t}.${raw_body}`\ndigest = HMAC_SHA256(secret, signed)\nif (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended\nif (!constant_time_eq(digest, parsed.v1)) reject\n```\n\n## Delivery retries\n\nEach event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts.\nAny 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network\nerror triggers the next retry. After the third failure the delivery is dropped.\n"}],"servers":[{"url":"https://next.statuspal.io/api/v1","description":"Production"},{"url":"http://spage.test:7070/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organization API key — issue one via the Rails console (management UI coming soon)"}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AccountPendingActivation":{"description":"The account is pending activation. A newly created account must be activated by an administrator before its API key can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}}},"paths":{"/outgoing_webhooks/{id}":{"delete":{"summary":"Delete an outgoing webhook","operationId":"deleteOutgoingWebhook","tags":["Outgoing Webhooks"],"responses":{"204":{"description":"Deleted"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AccountPendingActivation"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
````

## PATCH /outgoing\_webhooks/{id}

> Update an outgoing webhook

````json
{"openapi":"3.1.0","info":{"title":"StatusPal Public API","version":"1"},"tags":[{"name":"Outgoing Webhooks","description":"Manage outgoing webhook subscriptions for organization events.\n\n## Webhook signing\n\nWhen an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every\ndelivery carries the following headers:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `User-Agent` | `StatusPal-Webhooks/1.0` |\n| `X-StatusPal-Event` | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |\n| `X-StatusPal-Delivery` | A per-attempt UUID, use it to dedupe retried deliveries |\n| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>` |\n\nThe signature covers `\"#{t}.#{raw_body}\"`, signed with HMAC-SHA256 using the webhook's plaintext\nsigning secret (the `whs_…` value returned **once** when the webhook was created or its secret\nwas rotated). Verify it like this:\n\n```\nparsed = parse_header(\"X-StatusPal-Signature\")  # → { t: <int>, v1: <hex> }\nsigned = `${parsed.t}.${raw_body}`\ndigest = HMAC_SHA256(secret, signed)\nif (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended\nif (!constant_time_eq(digest, parsed.v1)) reject\n```\n\n## Delivery retries\n\nEach event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts.\nAny 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network\nerror triggers the next retry. After the third failure the delivery is dropped.\n"}],"servers":[{"url":"https://next.statuspal.io/api/v1","description":"Production"},{"url":"http://spage.test:7070/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organization API key — issue one via the Rails console (management UI coming soon)"}},"schemas":{"OutgoingWebhookUpdate":{"type":"object","properties":{"name":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean"},"all_status_pages":{"type":"boolean"},"status_page_subdomains":{"type":"array","items":{"type":"string"}}}},"OutgoingWebhook":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"description":"TypeID-prefixed identifier (e.g. `wbk_01h2x...`)"},"name":{"type":"string"},"url":{"type":"string","format":"uri","description":"HTTPS URL that will receive `POST` deliveries."},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean","default":true},"all_status_pages":{"type":"boolean","default":true,"description":"When `true`, the webhook fires for events on every status page in\nthe organization, including pages created later. When `false`, only\nevents on pages listed in `status_page_subdomains` fire.\n"},"status_page_subdomains":{"type":"array","items":{"type":"string"},"description":"Only present when `all_status_pages` is `false`. Subdomains of the\nspecific status pages this webhook is scoped to.\n"},"last_triggered_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent **successful** delivery."},"last_attempted_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent delivery attempt (success or failure)."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AccountPendingActivation":{"description":"The account is pending activation. A newly created account must be activated by an administrator before its API key can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"UnprocessableEntity":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/outgoing_webhooks/{id}":{"patch":{"summary":"Update an outgoing webhook","operationId":"updateOutgoingWebhook","tags":["Outgoing Webhooks"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutgoingWebhookUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OutgoingWebhook"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AccountPendingActivation"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/UnprocessableEntity"}}}}}}
````

## Rotate the signing secret

> Replaces the webhook's signing secret with a freshly generated value.\
> The new plaintext \`secret\` is returned in this response \*\*only\*\* — it\
> is not stored in cleartext and cannot be retrieved later.<br>

````json
{"openapi":"3.1.0","info":{"title":"StatusPal Public API","version":"1"},"tags":[{"name":"Outgoing Webhooks","description":"Manage outgoing webhook subscriptions for organization events.\n\n## Webhook signing\n\nWhen an event fires, StatusPal POSTs a JSON payload to your configured `outgoing_webhook.url`. Every\ndelivery carries the following headers:\n\n| Header | Value |\n|---|---|\n| `Content-Type` | `application/json` |\n| `User-Agent` | `StatusPal-Webhooks/1.0` |\n| `X-StatusPal-Event` | The event name (`service.status_changed`, `notice.created`, or `notice.updated`) |\n| `X-StatusPal-Delivery` | A per-attempt UUID, use it to dedupe retried deliveries |\n| `X-StatusPal-Signature` | Stripe-style signature: `t=<unix_ts>,v1=<hex_hmac>` |\n\nThe signature covers `\"#{t}.#{raw_body}\"`, signed with HMAC-SHA256 using the webhook's plaintext\nsigning secret (the `whs_…` value returned **once** when the webhook was created or its secret\nwas rotated). Verify it like this:\n\n```\nparsed = parse_header(\"X-StatusPal-Signature\")  # → { t: <int>, v1: <hex> }\nsigned = `${parsed.t}.${raw_body}`\ndigest = HMAC_SHA256(secret, signed)\nif (Date.now() / 1000 - parsed.t > 300) reject  # 5-minute tolerance recommended\nif (!constant_time_eq(digest, parsed.v1)) reject\n```\n\n## Delivery retries\n\nEach event is delivered up to **3 times** with exponential backoff: **1s, 2s, 4s** between attempts.\nAny 2xx response is treated as success; any other status, a timeout (5s open / 10s read), or a network\nerror triggers the next retry. After the third failure the delivery is dropped.\n"}],"servers":[{"url":"https://next.statuspal.io/api/v1","description":"Production"},{"url":"http://spage.test:7070/api/v1","description":"Local development"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organization API key — issue one via the Rails console (management UI coming soon)"}},"schemas":{"OutgoingWebhookWithSecret":{"allOf":[{"$ref":"#/components/schemas/OutgoingWebhook"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"Plaintext signing secret (`whs_…`). Returned **only** in responses\nto create and regenerate_secret. Store it immediately — it is not\nretrievable later.\n"}}}]},"OutgoingWebhook":{"type":"object","properties":{"id":{"type":"string","readOnly":true,"description":"TypeID-prefixed identifier (e.g. `wbk_01h2x...`)"},"name":{"type":"string"},"url":{"type":"string","format":"uri","description":"HTTPS URL that will receive `POST` deliveries."},"events":{"type":"array","items":{"type":"string","enum":["service.status_changed","notice.created","notice.update_posted","notice.updated"]}},"enabled":{"type":"boolean","default":true},"all_status_pages":{"type":"boolean","default":true,"description":"When `true`, the webhook fires for events on every status page in\nthe organization, including pages created later. When `false`, only\nevents on pages listed in `status_page_subdomains` fire.\n"},"status_page_subdomains":{"type":"array","items":{"type":"string"},"description":"Only present when `all_status_pages` is `false`. Subdomains of the\nspecific status pages this webhook is scoped to.\n"},"last_triggered_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent **successful** delivery."},"last_attempted_at":{"type":"string","format":"date-time","nullable":true,"readOnly":true,"description":"Timestamp of the most recent delivery attempt (success or failure)."},"created_at":{"type":"string","format":"date-time","readOnly":true},"updated_at":{"type":"string","format":"date-time","readOnly":true}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"AccountPendingActivation":{"description":"The account is pending activation. A newly created account must be activated by an administrator before its API key can be used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Resource not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/outgoing_webhooks/{id}/regenerate_secret":{"post":{"summary":"Rotate the signing secret","description":"Replaces the webhook's signing secret with a freshly generated value.\nThe new plaintext `secret` is returned in this response **only** — it\nis not stored in cleartext and cannot be retrieved later.\n","operationId":"regenerateOutgoingWebhookSecret","tags":["Outgoing Webhooks"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OutgoingWebhookWithSecret"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/AccountPendingActivation"},"404":{"$ref":"#/components/responses/NotFound"}}}}}}
````
