Event subscriptions
Event subscriptions (REST hooks) send check and incident events to an HTTPS URL you register through the API. They power the Zapier integration. They are separate from webhook notification channels, which are configured in notification groups.
All subscription operations require the webhooks:manage scope. A
subscription belongs to the API key or OAuth connection that created it.
Subscribe
Section titled “Subscribe”POST /api/v1/hooks
| Field | Required | Meaning |
|---|---|---|
target_url |
Yes | Public HTTPS URL, up to 2,000 characters, without credentials or a fragment |
event |
Yes | One of the events below |
check_id |
No | Only deliver events for this check |
{ "target_url": "https://hooks.example.com/pingstack", "event": "incident.opened", "check_id": "check_EXAMPLE"}The 201 response includes the subscription and its signing secret. The
secret is returned only here; store it privately.
{ "id": "hook_EXAMPLE", "event": "incident.opened", "target_url": "https://hooks.example.com/pingstack", "check_id": "check_EXAMPLE", "created_at": "2026-01-01T12:00:00Z", "secret": "YOUR_SIGNING_SECRET"}Plain HTTP, private or internal addresses and checks from another account are rejected. Each API key or connection can hold up to 100 subscriptions.
Unsubscribe
Section titled “Unsubscribe”DELETE /api/v1/hooks/:id deletes one of this key’s or connection’s
subscriptions and returns it without the secret. Another key’s subscription
returns 404.
Subscriptions are also deleted immediately when their API key is revoked or
their OAuth connection is disconnected, within a day of the key or connection
expiring, and when the receiver answers 410 Gone.
Events
Section titled “Events”| Event | Sent when |
|---|---|
check.down |
A check goes down |
check.degraded |
A check becomes degraded |
check.recovered |
A down or degraded check is back up |
incident.opened |
An incident opens |
incident.updated |
An open incident changes severity, or a status update is posted to it |
incident.resolved |
An incident resolves |
check.late is accepted as a subscription event but is not sent at present.
Payload
Section titled “Payload”Each delivery is a POST with a JSON body:
{ "id": "4f1c2a6e-0000-4000-8000-000000000000", "event": "incident.updated", "occurred_at": "2026-01-01T12:05:00Z", "alert": "Public website posted an incident update.", "check": { "id": "check_EXAMPLE", "name": "Public website", "kind": "website", "target": "https://example.com/health", "status": "down", "previous_status": "down", "url": "https://app.pingstack.io/checks/check_EXAMPLE" }, "incident": { "id": "123", "cause": "Human-readable cause", "started_at": "2026-01-01T12:00:00Z", "resolved_at": null }, "update": { "id": "iupd_EXAMPLE", "body": "We are rolling back the deploy.", "author_name": "Example User", "created_at": "2026-01-01T12:05:00Z" }}| Field | Meaning |
|---|---|
id |
Unique delivery event ID; also sent as X-Pingstack-Delivery |
alert |
Display text; don’t parse it |
check.target |
Hostname, host:port, or website URL without credentials, query or fragment; null for heartbeat checks |
check.status, check.previous_status |
Status after and before the change |
incident |
Null when the event has no incident |
update |
Present only for incident.updated caused by a posted status update |
Unlike incident reads, payloads include the incident cause and status update text.
Verify signatures
Section titled “Verify signatures”Each delivery has these headers:
| Header | Value |
|---|---|
X-Pingstack-Event |
The event name |
X-Pingstack-Delivery |
The payload id |
X-Pingstack-Timestamp |
Unix time in seconds when the delivery was sent |
X-Pingstack-Signature |
sha256= and the hex HMAC-SHA256 of TIMESTAMP.BODY with the subscription secret |
Compute the HMAC over the timestamp, a period and the exact raw request body, compare it in constant time, and reject old timestamps to limit replays:
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", ENV.fetch("PINGSTACK_HOOK_SECRET"), "#{timestamp}.#{raw_body}")valid = Rack::Utils.secure_compare(expected, signature_header) && (Time.now.to_i - timestamp.to_i).abs < 300Delivery and retries
Section titled “Delivery and retries”Return any 2xx response within 5 seconds. Redirects are not followed and
count as failures. Failed deliveries are retried up to eight attempts with
increasing backoff, then dropped. Return 410 Gone to delete the subscription.
Deliveries can repeat and arrive out of order; use id to deduplicate and
occurred_at to order. Events stop while the key or connection has expired or
its approving user lacks access to the account.
Sample events
Section titled “Sample events”GET /api/v1/hooks/samples?event=EVENT returns recent real incidents
from the account shaped exactly like live deliveries, for testing receivers
and building field mappings.
| Query | Meaning |
|---|---|
event |
Required event name |
check_id |
Optional check filter |
limit |
One to 10; default 3 |
The response is {"items": [...]}, where each item is a payload as above with
an id beginning sample_.
Sample statuses are the ones the event implies, so they can differ from the
check’s current status. An account without matching incidents returns an
empty list.