Gareth Wilson Gareth Wilson

Guide to PingOne Webhooks: Features and Best Practices

Published


PingOne webhooks stream your identity platform's audit trail to another system: users are created or deleted, people sign in, passwords are reset. If your organisation runs identity on PingOne, webhooks, which PingOne calls subscriptions, are how those audit events reach your SIEM or your own services as they happen.

This guide covers how PingOne subscriptions work, the event and payload options, how to authenticate deliveries with a header you add, how PingOne retries and queues, and the practices that keep a PingOne receiver reliable.

What are PingOne webhooks?

A PingOne subscription sends audit activity events to an HTTPS endpoint. You choose which audit events to include, optionally narrow them by application, population or tag, and choose the format and batching. PingOne then pushes matching events as they're recorded.

PingOne doesn't sign deliveries. Instead, a subscription has httpEndpoint.headers, "headers applied to the outbound request" whose purpose "is for the HTTPS endpoint to authenticate the PingOne service". You add a header with a secret value and your receiver compares it. PingOne also supports outbound mutual TLS with a client key, which authenticates at the connection rather than in a header.

PingOne webhook features

FeatureDetails
ConfigurationIntegrations > Webhooks in the admin console, or POST /environments/{envID}/subscriptions
EventsAny audit event in Audit Reporting Events, listed in filterOptions.includedActionTypes
FiltersApplications and populations (up to 10 IDs each), and tags
FormatsACTIVITY (generic JSON), SPLUNK or NEWRELIC
BatchingJSON_ARRAY (optionally pretty-printed) or ND_JSON, up to 500 events or 4096 KB per request
Event identifieraction.type, such as USER.CREATED
AuthenticationHeaders you set on the subscription, or outbound mTLS. No signature
RetriesThe current event is retried until it gets a 200
QueueingEvents are lost after the target is down for more than 7 days. A disabled subscription keeps events for up to two weeks
EndpointHTTPS, IPv4 only
Source IPsNot published

Common events

PingOne lets you subscribe to any event in its Audit Reporting Events reference, which lists hundreds of RESOURCE.ACTION types. Common choices include:

action.typeRecorded when
USER.CREATED, USER.UPDATED, USER.DELETEDA user is created, updated or deleted
AUTHENTICATION.CREATEDAn authentication is recorded
PASSWORD.RESET, PASSWORD.CHECK_FAILEDA password is reset, or a password check fails
MFA_SETTINGS.UPDATEDMFA settings change
SUBSCRIPTION.CREATED, SUBSCRIPTION.UPDATED, SUBSCRIPTION.DELETEDA subscription itself changes

With the ACTIVITY format, each event is a JSON object whose attributes are listed in the reference. The ones most receivers use are:

AttributeMeaning
idThe audit event's UUID
recordedAt, createdAtWhen the event was recorded and created, in ISO 8601
action.type, action.descriptionThe event type and a description
actors.user, actors.clientWho or what performed the action, with IDs and names
resourcesWhat the action affected, with type, ID and name
result.statussucceeded or failed
correlationIdTies together the messages in one transaction
source.ipAddress, source.userAgentPresent when ipAddressExposed or userAgentExposed is on

A request holds a batch of events: a JSON array with JSON_ARRAY, or one JSON object per line with ND_JSON. PingOne doesn't publish a full delivered example, so record a real delivery before relying on exact nesting. The SPLUNK and NEWRELIC formats wrap events differently and aren't described in detail.

Setting up PingOne webhooks

  1. Generate a long random secret, for example with openssl rand -hex 32.
  2. In the PingOne admin console, go to Integrations > Webhooks, or use the subscriptions API.
  3. Set the HTTPS URL, and add a header such as Authorization: Bearer <secret>.
  4. Choose the ACTIVITY format and JSON_ARRAY payloads.
  5. Choose the audit events, and any application, population or tag filters.
  6. Leave certificate verification on.

Through the API, a subscription looks like this:

{
  "name": "Audit events to Hookdeck",
  "enabled": true,
  "format": "ACTIVITY",
  "httpEndpoint": {
    "url": "https://example.com/webhooks/pingone",
    "headers": { "Authorization": "Bearer YOUR_LONG_RANDOM_SECRET" }
  },
  "filterOptions": {
    "includedActionTypes": ["USER.CREATED", "USER.UPDATED", "USER.DELETED"]
  },
  "payloadOptions": {
    "payloadFormat": { "https": { "format": "JSON_ARRAY" } }
  },
  "verifyTlsCertificates": true
}

PingOne's own header example is "Authorization": "Basic username:password", with the credentials not base64-encoded. Whatever you type is what PingOne sends, so a value like that isn't a valid HTTP Basic header. A Bearer-style random value avoids the confusion.

Use an IPv4 endpoint: PingOne says IPv6 addresses aren't fully supported. If verifyTlsCertificates is on and your endpoint uses a private CA or a self-signed certificate, upload it to PingOne's certificate service before creating the subscription. Turning verification off trusts every certificate, which PingOne flags as a security risk.

In Hookdeck. Create a source with the PingOne type, enter the header name in API Key Header and paste the exact header value you set on the subscription into API Key, prefix included. Use the source URL as httpEndpoint.url. Hookdeck matches the name case-insensitively and the value exactly. The source accepts POST. It doesn't check mutual TLS, so authenticate with a header when you route through Hookdeck.

For local development, hookdeck listen 3000 pingone --path /webhooks/pingone with the Hookdeck CLI gives you a public HTTPS URL, with no account required.

Securing PingOne webhooks

Look up your header and compare its whole value with the secret in constant time, then parse the batch.

const crypto = require('crypto');
const express = require('express');

const app = express();

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on buffers of different lengths
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

function parseEvents(text) {
  const trimmed = text.trim();
  if (trimmed.startsWith('[')) return JSON.parse(trimmed); // JSON_ARRAY
  return trimmed.split('\n').filter((line) => line.trim()).map((line) => JSON.parse(line)); // ND_JSON
}

app.post('/webhooks/pingone', express.raw({ type: '*/*', limit: '5mb' }), (req, res) => {
  const secret = process.env.PINGONE_WEBHOOK_SECRET; // the exact header value
  if (!secret) return res.sendStatus(500); // fail closed when unconfigured

  const name = process.env.PINGONE_WEBHOOK_HEADER || 'Authorization';
  const received = req.get(name); // case-insensitive lookup
  if (typeof received !== 'string' || !safeEqual(received, secret)) {
    return res.sendStatus(401);
  }

  let events;
  try {
    events = parseEvents(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Store the batch, keyed on each event's id, then acknowledge.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os


def verify_pingone_request(headers) -> bool:
    secret = os.environ.get("PINGONE_WEBHOOK_SECRET")
    if not secret:
        return False  # fail closed
    name = os.environ.get("PINGONE_WEBHOOK_HEADER", "Authorization")
    received = headers.get(name)  # case-insensitive in Starlette and Flask
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

A matching header shows the sender knows the value. It doesn't prove the events are unchanged, and a captured request can be replayed, since there's no signature. For audit data that feeds security decisions, use HTTPS, a long random value, rotate it if it may have leaked, and deduplicate on each event's id.

Make PingOne webhooks production-ready. Hookdeck Event Gateway checks your header on every PingOne delivery, answers straight away so the stream never stalls on your service, and keeps every batch for replay.

PingOne webhook limitations and pain points

One failing event blocks the stream

The Problem: A single delivery your endpoint rejects holds up every event behind it.

Why It Happens: "The webhook continues to retry the current event until a 200 response is returned. It doesn't move on to the next event until a 200 response has returned or the event has expired from Kafka." Only a 200 is described as moving the stream on.

Workarounds:

  • Return 200 once a batch is stored, even if some events in it fail later processing.
  • Never return an error for an event type you don't recognise.
  • Watch for a growing gap between recordedAt and the time events arrive.

How Hookdeck Can Help: Hookdeck answers PingOne with a 200 as soon as each request arrives, so the stream keeps moving. A failure in your service becomes a failed delivery in Hookdeck that you can retry or replay, without blocking newer events.

Seven days, then events are lost

The Problem: A long outage at your end loses audit history.

Why It Happens: "If the webhook target is down for longer than 7 days, PingOne starts to lose events." A disabled subscription keeps events for up to two weeks and delivers them when re-enabled, but that only helps if you disable it before the outage.

Workarounds:

  • Disable the subscription ahead of planned maintenance, and re-enable it within two weeks.
  • Backfill gaps from PingOne's audit reporting if you need complete history.

How Hookdeck Can Help: With Hookdeck accepting every request, your endpoint's downtime no longer counts against PingOne's 7-day window. Events wait in Hookdeck until your service is back.

Batches, not single events

The Problem: A handler that expects one event per request misses most of them.

Why It Happens: A request carries up to 500 events by default, as a JSON array or NDJSON lines.

Workarounds:

  • Always iterate. Use maximumPayloadLimit to reduce batch sizes if your handler needs it.
  • Choose JSON_ARRAY so the body is a single JSON document.

How Hookdeck Can Help: Hookdeck stores each batch with its full body. Transformations can reshape a batch before delivery, and filters can match on fields inside it.

A static header, not a signature

The Problem: Anyone who learns the header value can inject fake audit events into your SIEM.

Why It Happens: httpEndpoint.headers are fixed values. Nothing is computed over the body.

Workarounds:

  • Use a long random value, HTTPS only, and rotate it on a schedule.
  • For high-assurance cases, use outbound mTLS directly to your own endpoint.

How Hookdeck Can Help: Hookdeck's PingOne source compares the header you name with the exact value you enter. Hookdeck answers PingOne with a 200 either way, then records a request without the right header as failed verification, so it never reaches your destination.

Best practices

Subscribe to what you use

includedActionTypes is required and must not be empty. Listing only the events you process keeps batches small, and leaves fewer events that can hold up the stream.

Deduplicate on id

Retries can resend events. Each audit event has a UUID id. Store it and skip repeats. See our guide to webhook idempotency.

Check result.status

Many audit events record failures as well as successes. Branch on result.status before treating an event as something that happened.

Acknowledge fast and process asynchronously

Store the batch, respond with 200, then process. Because PingOne won't move on until it gets a 200, a slow handler delays every event after it. See why to process webhooks asynchronously.

Conclusion

PingOne subscriptions push batches of audit events, identified by action.type, with headers you set as the only in-request credential. Compare your header in constant time over HTTPS, use the ACTIVITY format with JSON_ARRAY, iterate over every event, deduplicate on id, and return 200 quickly, because one stuck event holds up the rest.

Hookdeck Event Gateway sits in front of your receiver, checks the header, acknowledges every batch straight away so the stream keeps flowing, and holds each one until your service has processed it.

Get started with Hookdeck for free and handle PingOne webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.