Skip to content

PagerDuty webhooks ​

PagerDuty is one primary paging source. This page covers the manual Generic Webhooks v3 setup and verification. Other paging tools are listed on Sources. Manual webhook covers custom JSON ingest.

Each workspace has its own URL. Exhale accepts PagerDuty v3 webhook payloads at:

POST https://exhaleoncall.com/webhooks/pagerduty/{token}

{token} is the workspace token in the URL Connections copies. On the hosted app that URL starts with https://exhaleoncall.com/webhooks/pagerduty/. The customer host does not publish /v1/webhooks/....

A signed delivery returns 202 Accepted with pulse_id, status, created, and message. Send Test Event in PagerDuty is a pagey.ping. For an active connection with the pasted signing secret, that ping returns 202, pulse_id is null, created is false, and message is PagerDuty event ignored. It does not create a Pulse. Exhale records Last verified on the PagerDuty card.


Customer setup ​

Manual (webhook URL + signing secret) ​

  1. Sign in as admin → Connections → Sources → PagerDuty.
  2. Choose Save PagerDuty settings. The first save mints a random URL token for this workspace. That token is never an account id. Exhale stores a hash of it for lookup, separate from any account id, and an encrypted copy so the card can show the URL. The card then shows Webhook URL (POST) as read-only text, with Copy webhook URL. Copy that address into PagerDuty. It is https://exhaleoncall.com/webhooks/pagerduty/{token}. If the card is already set up and Webhook URL (POST) is not shown, choose Generate webhook URL. That issues the workspace token without asking you to change another setting first. Save & close in Edit settings stays available in that case too, so a save with no other field changes still issues the URL.
  3. Saving again keeps the same token. The revealed URL stays read-only. Saving or rotating does not change how the shared POST /webhooks/pagerduty URL routes.
  4. In PagerDuty, create a Generic Webhooks v3 subscription under Integrations → Generic Webhooks v3 and set the endpoint to the URL you copied. Sign deliveries with the signing secret you paste on the card. The workspace URL does not fall back to a platform secret. It checks the secret saved on the card. Any other value is 401.
  5. PagerDuty shows a signing secret once. Paste it into PagerDuty signing secret on the Exhale card and save again. Do this before Send Test Event in PagerDuty. PagerDuty turns a webhook off for 24 hours after three rejected deliveries.
  6. In PagerDuty, choose Send Test Event. A signed pagey.ping returns 202 and Exhale sets Last verified. Send test alert in Exhale creates a synthetic Pulse and does not confirm the PagerDuty signature.
  7. Optionally store a PagerDuty REST API token for on-call schedule sync. Test token checks that REST token. It is separate from Send Test Event.

See PagerDuty's official guide: Generic Webhooks.

Existing subscription ​

Use this when PagerDuty already sends events to Exhale and you are moving that subscription onto the workspace URL.

  1. On the PagerDuty card, paste the signing secret that subscription already uses into PagerDuty signing secret and choose Save PagerDuty settings. Do not Disconnect, Rotate webhook token, or Send Test Event yet. After save, the card shows Webhook URL (POST). The workspace URL checks that saved secret. It does not fall back to a different platform secret. Pasting the secret the subscription already uses is what lets the old address and the new address accept the same signature.
  2. In PagerDuty, change only the subscription endpoint to the URL you copied. Keep the same signing secret. Deliveries signed with that secret succeed on both addresses while you switch.
  3. Leave the secret alone after that. If you change it in PagerDuty or in Exhale and not the other, the workspace URL returns 401.

Do not point PagerDuty at the workspace URL, and do not Send Test Event to that URL, until the secret is saved. That delivery is 401 until then. Do not try other URLs. A rejected delivery counts toward PagerDuty turning the subscription off for 24 hours. Disconnect stops the previous address as well as the workspace URL.

Rotate webhook token mints a new token. The previous workspace URL returns 404. The shared URL keeps routing. Update the workspace subscription in PagerDuty after rotating. Exhale clears Last verified until the new URL receives a signed delivery.

Disconnect clears the workspace token and the account id the shared URL uses, so both URLs stop. Saving again mints a new workspace token.

Saving from the Add flow continues to Send a test alert (or skip). That step checks Exhale ingest. It does not check the PagerDuty signature.

See PagerDuty's official guide: Generic Webhooks.

Test alerts are free. They do not start your 14-day trial and do not count toward your monthly Pulse allowance.

Marketplace and connect ​

A PagerDuty App Directory listing is deferred until after launch. Connect at launch is the manual Generic Webhooks v3 path above. An optional triage panel inside the PagerDuty incident UI is not required.


Troubleshooting ​

What you seeWhat to do next
202 and Last verified updatesThe signed pagey.ping was accepted. No Pulse is created for that ping.
401 pagerduty_signing_secret_requiredThe URL is known and the signing secret is not saved yet. Paste PagerDuty signing secret, save, then send the test again.
401 after the secret is savedThe signature does not match the secret stored for this workspace. The workspace URL does not fall back to a platform secret. Paste the secret from this PagerDuty subscription again.
Card is set up and Webhook URL (POST) is missingChoose Generate webhook URL. That issues the workspace token. Save & close in Edit settings does the same when no other field has changed.
404This URL token is not current. Copy Webhook URL (POST) again. After Rotate webhook token, the previous workspace URL returns 404. The shared URL still routes until Disconnect.
403 while the connection is inactiveExhale rejects the delivery before it checks the signature. Turn the connection active, then send the test again.
403 while the workspace is past due, paused, or canceledExhale is not accepting webhook ingest, including Send Test Event.
400The body was not a JSON object, or (on the hosted API) event.id was missing. A normal PagerDuty pagey.ping includes event.id.
413The body is larger than the configured maximum (default 1 MiB).
429The client address is over the webhook rate limit. Retry-After says when to try again.
503The platform is in maintenance, or replay dedupe could not be stored.

An expired trial rejects incident deliveries with 403. Send Test Event (pagey.ping) uses the checks in the table above and does not apply that trial check.


Required fields ​

PagerDuty incident id is required on lifecycle and timeline-only deliveries: event.data.id when event.data.type is an incident (or omitted); otherwise event.data.incident.id (conference bridge, custom fields, notes, and status updates). Payloads without it receive 400 Bad Request (do not use event.id as external_id). The body must be a JSON object. On the hosted API, event.id (delivery id used for replay dedupe) is also required. pagey.ping has no incident id and is acknowledged with 202 when the checks above pass.

FieldSource
external_idevent.data.id when data is the incident; otherwise event.data.incident.id
titleevent.data.title or event.data.summary (skipped on later updates when the pulse has a user-edited title)
event_typeevent.event_type

The full JSON body is stored after scrubbing (secrets and injection-like strings redacted).

PagerDuty v3 incident.triggered does not include a reliable first-class maintenance-window flag. When nested signals are present (event.data.suppressed, service.status=maintenance, fully suppressed nested alerts, or alert_counts.suppressed with no triggered alerts), Exhale stores the pulse, sets suppressed=true, skips triage, and shows a Suppressed badge. service.updated is not used for maintenance and is acknowledged without creating or updating a pulse.


Tenancy ​

Each pulse gets tenant_id from the workspace token in POST /webhooks/pagerduty/{token}. Exhale checks X-PagerDuty-Signature with the signing secret pasted for that workspace. That URL does not fall back to a platform secret. The shared POST /webhooks/pagerduty URL is verified with the operator’s platform secret.


Signature verification ​

The hosted API requires X-PagerDuty-Signature: HMAC-SHA256 of the raw request body, formatted as v1=<hex>. Multiple comma-separated signatures are supported.

Unsigned or invalid requests receive 401 Unauthorized.

The workspace URL (POST /webhooks/pagerduty/{token}) is verified with the signing secret you saved on the PagerDuty card. It does not fall back to a platform secret. Pasting the secret the subscription already uses is what lets that URL accept the same signature. The shared POST /webhooks/pagerduty URL is verified with the operator’s platform secret.

Saving settings or rotating the URL on the PagerDuty card does not change how the shared URL routes. Before you point an existing subscription at your workspace URL, save that subscription’s signing secret in Exhale. A delivery signed with any other secret is rejected, and three rejections turn the subscription off for 24 hours. Customer steps are under Existing subscription.

The example below targets the operator shared URL. For your workspace URL, use https://exhaleoncall.com/webhooks/pagerduty/<token> and the signing secret you saved.

bash
SECRET="your-signing-secret"
BODY='{"event":{"id":"evt_demo_001","event_type":"incident.triggered","data":{"id":"P12345","title":"API latency is above SLO"}}}'
SIG="v1=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -i -X POST "https://exhaleoncall.com/webhooks/pagerduty" \
  -H "Content-Type: application/json" \
  -H "X-PagerDuty-Signature: $SIG" \
  -d "$BODY"

Dedupe and reliability ​

Replay dedupe: Exhale claims the PagerDuty delivery id (event.id) before scrub/ingest. A duplicate within the configured TTL is acknowledged with 202 and message Duplicate webhook event without re-running ingest. On that replay, pulse_id may be null when no pulse row was created for the delivery. Payloads missing event.id receive 400 on the hosted API. If Redis is unavailable, deliveries may receive 503. pagey.ping is acknowledged before that claim, so a repeated ping is another 202.

Incident upsert: Duplicate deliveries for the same PagerDuty incident id (event.data.id, or event.data.incident.id when the data object is not the incident) update the existing pulse row and still return 202.

Body size: Bodies larger than the configured maximum (default 1 MiB) receive 413. Invalid or missing Content-Length receives 400.

Rate limits: Per-IP limits apply (429 with Retry-After and X-RateLimit-* headers).

PagerDuty v3 signs the raw body only — there is no timestamp in the signature header. Rely on event.id dedupe for replay protection.


On-call schedule ​

On-call status uses the PagerDuty REST API token on the PagerDuty card. Do not add a second subscription for schedule events. PagerDuty’s current v3 catalog does not include those events.


Other event types ​

New pulses are created only for incident.triggered (and retrigger when no row exists). Other incident events update an existing pulse or are ignored. pagey.ping and service.* return 202 without a pulse.

Subscribe the Generic Webhooks v3 subscription to the incident events in the table below. A subscription that only sends incident.triggered still creates Pulses. It will not add timeline-only Activity rows. service.* events are ignored even when they arrive.

For the workspace URL, include event.id and sign with the secret you saved. The signature example above is the operator shared URL.

bash
SECRET="the-signing-secret-you-pasted"
TOKEN="the-token-from-webhook-url"
BODY='{"event":{"id":"evt_demo_ack_001","event_type":"incident.acknowledged","data":{"id":"P12345","title":"API latency"}}}'
SIG="v1=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"

curl -i -X POST "https://exhaleoncall.com/webhooks/pagerduty/${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-PagerDuty-Signature: $SIG" \
  -d "$BODY"
event.event_typePulse effect
incident.triggered (and incident.retriggered)Upsert pulse; enqueue triage when async triage is enabled, unless nested suppression/maintenance signals are present
incident.reopened / incident.unacknowledgedExisting pulse → received; enqueue triage
incident.acknowledgedStatus → acknowledged; signal re-triage keeps that status
incident.resolvedStatus → resolved; signal re-triage keeps that status
incident.annotatedExisting pulse; signal re-triage keeps current status; timeline source_signal
Other official incident.* (escalated, delegated, priority, responders, workflows, custom fields, conference bridge, service changed, …)Timeline source_signal on an existing pulse; no status or triage change
pagey.ping, service.*, schedule events on this URL202 ignored; no pulse

Auth API ​

Canonical paths are under /v1. The app calls them under /api. Writes need a session and CSRF. Connect, save, rotate, and disconnect are admin. Send test alert is member or higher. webhook_path is null for viewers.

MethodPathNotes
GET/v1/auth/sources/pagerdutyStatus, webhook_path (/webhooks/pagerduty/{token}), has_signing_secret, has_api_token, last_verified_at, last_ingest_test_at
PUT/v1/auth/sources/pagerdutyWrites the workspace URL token. A later save keeps the token. Optional signing_secret and REST api_token
POST/v1/auth/sources/pagerduty/rotate-tokenAdmin. Mints a new URL token. The previous workspace URL returns 404. The shared URL keeps routing
DELETE/v1/auth/sources/pagerdutyAdmin. Clears the account id and the URL token, so both URLs stop. The next save mints a new workspace token
POST/v1/auth/sources/pagerduty/test-tokenAdmin. Verify a typed or stored REST token
POST/v1/auth/sources/pagerduty/test-alertMember or higher. Synthetic ingest

Secrets and signing material are never returned.


Exhale by Kolstrom Systems LLC