Agent skill
PingOne Webhooks Skill
Receive and authenticate PingOne subscription webhooks. Use when building an endpoint for PingOne audit events, securing it with the header you set on the subscription (httpEndpoint.headers), handling batched JSON_ARRAY or ND_JSON bodies, debugging 401s, or handling events like USER.CREATED, USER.UPDATED and AUTHENTICATION.CREATED.
Install this skill
npx skills add hookdeck/webhook-skills --skill pingone-webhooks
PingOne (Ping Identity) pushes audit events to your app through subscriptions (webhooks). PingOne does not sign these requests. Instead, you add a header to the subscription (httpEndpoint.headers), PingOne sends it on every delivery, and your endpoint checks it.
When to Use This Skill
- How do I receive PingOne webhooks?
- How do I authenticate PingOne subscription requests? (There is no signature: use a header you set)
- How do I handle a batch of events (JSON array or ND_JSON) from PingOne?
- How do I handle
USER.CREATED,USER.UPDATEDorAUTHENTICATION.CREATEDevents? - Why does PingOne keep retrying my webhook endpoint?
Verification (core)
Paste the exact header value you set on the subscription into PINGONE_WEBHOOK_SECRET, and the header name into PINGONE_WEBHOOK_HEADER (default Authorization). The value is whatever you typed, so compare the whole value; do not strip or parse a Bearer or Basic prefix.
const crypto = require('crypto');
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb); // equal-length buffers: never throws
}
function verifyPingOneRequest(headers, config = process.env) {
const secret = config.PINGONE_WEBHOOK_SECRET;
if (!secret) return false; // never accept when unconfigured
const name = (config.PINGONE_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const received = headers[name]; // Node lowercases header names
return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}
Return 401 for a missing header or a mismatch, 400 for a body that isn't a JSON array or ND_JSON, and 200 once handled. In Python use hmac.compare_digest on bytes (a.encode()), never str, which raises on non-ASCII.
A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the header value on the subscription if it leaks. Dedupe on each event's
idif replays matter.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Body Format
With the ACTIVITY format, the body is a batch of audit events (up to 500) in one of two shapes:
JSON_ARRAY:[{...}, {...}]ND_JSON: one JSON event per line
The examples handle both. Read the raw body (in Express, express.raw({ type: '*/*' }) so any content type works) and parse it only after the credential check. PingOne retries the current event until it gets a 200, so return 200 only once you've handled the batch.
Event Types
Each event has action.type. PingOne documents many; the examples handle these:
action.type | Sent When |
|---|---|
USER.CREATED | A user is created |
USER.UPDATED | A user is updated |
USER.DELETED | A user is deleted |
AUTHENTICATION.CREATED | An authentication is recorded |
PASSWORD.RESET | A password is reset |
Choose the ones you receive with filterOptions.includedActionTypes on the subscription. See the Audit Reporting Events reference for the full list.
Environment Variables
PINGONE_WEBHOOK_SECRET=replace-with-the-exact-header-value # whole value, incl. any "Bearer " prefix
PINGONE_WEBHOOK_HEADER=Authorization # header name you set (case-insensitive)
Local Development
PingOne can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 pingone --path /webhooks/pingone
When using Hookdeck's PingOne source, the setup note is the same: paste the exact header value you set on the subscription.
Resources
- references/overview.md - How PingOne subscriptions work, event shape, example events
- references/setup.md - Create the subscription and its authentication header
- references/verification.md - Header check in Node and Python, limits, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests