# 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.UPDATED` or `AUTHENTICATION.CREATED` events?
* 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.

```javascript
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 `id` if replays matter.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/pingone-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/pingone-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/pingone-webhooks/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](https://developer.pingidentity.com/pingone-api/platform/reference/audit-reporting-events.html)
reference for the full list.

## Environment Variables

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/pingone-webhooks/references/overview.md) - How PingOne subscriptions work, event shape, example events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pingone-webhooks/references/setup.md) - Create the subscription and its authentication header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/pingone-webhooks/references/verification.md) - Header check in Node and Python, limits, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/pingone-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests