Skip to content

Platform

Webhooks

A webhook tells your application that something changed on the other side of a customer’s identity system — a person joined, a group lost a member, a connection went live. Register an endpoint, verify the signature, and treat every delivery as something you may see twice.

HTTPS onlySigned with HMAC-SHA256At-least-once delivery

Overview

Endpoints are registered per environment, either in the dashboard or through the API. Each endpoint has its own signing secret and its own subscription list — you can send directory events to one service and session events to another.

register an endpoint
1curl -X POST https://api.paycux.com/webhooks \
2 -H "Authorization: Bearer sk_example_123456789" \
3 -H "Content-Type: application/json" \
4 -d '{
5 "endpoint_url": "https://api.yourapp.example/paycux/webhook",
6 "events": ["dsync.user.created", "dsync.user.deleted", "dsync.group.user_added"]
7 }'
  • Your endpoint must answer with a 2xx status within 10 seconds
  • Acknowledge first, process afterwards — slow handlers cause retries, not lost work
  • A staging endpoint never receives production events, and the reverse

Event types

Subscribe only to what you handle. An endpoint that receives events it ignores is an endpoint that will one day time out on a busy sync.

Directory Sync

Emitted when a customer's directory changes. This is the group most applications subscribe to first.

dsync.activated
dsync.deactivated
dsync.user.created
dsync.user.updated
dsync.user.deleted
dsync.group.created
dsync.group.updated
dsync.group.deleted
dsync.group.user_added
dsync.group.user_removed

Connections

Emitted while a customer sets up single sign-on, usually through the Admin Portal.

connection.activated
connection.deactivated
connection.deleted

Users and organizations

Emitted by user management, whether the change came from your API calls or from a self-serve action.

user.created
user.updated
user.deleted
organization.created
organization.updated
organization_membership.created
organization_membership.updated
organization_membership.deleted

Sessions and risk

Emitted around authentication itself. Useful for security tooling and for your own audit trail.

session.created
session.revoked
authentication.failed
radar.risk_detected

Delivery payload

Every delivery has the same envelope: an id, an event name, a timestamp and the object the event is about. The object matches the shape you would get from the corresponding endpoint in the API reference.

1{
2 "id": "event_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
3 "event": "dsync.group.user_added",
4 "created_at": "2026-06-18T08:14:03.220Z",
5 "data": {
6 "object": "directory_user",
7 "id": "directory_user_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
8 "directory_id": "directory_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
9 "organization_id": "org_01HQ8ZK3M4N5P6R7S8T9V0W1X2",
10 "emails": [{ "primary": true, "value": "alan@foo-corp.example" }],
11 "group": { "id": "directory_group_01HQ8ZK3M4N5P6R7S8T9V0W1X2", "name": "Engineering" },
12 "state": "active"
13 }
14}

Verifying signatures

The Paycux-Signature header carries a timestamp and an HMAC-SHA256 of timestamp + "." + rawBody, keyed with the endpoint’s signing secret. Verify it against the raw request body — parsing the JSON first and re-serialising it will change the bytes and break the comparison.

1import express from 'express';
2import { paycux } from './lib/paycux';
3
4const app = express();
5
6// raw body, not the JSON parser
7app.post('/paycux/webhook', express.raw({ type: 'application/json' }), (req, res) => {
8 let event;
9
10 try {
11 event = paycux.webhooks.constructEvent({
12 payload: req.body,
13 signature: req.headers['paycux-signature'],
14 secret: process.env.PAYCUX_WEBHOOK_SECRET,
15 tolerance: 300,
16 });
17 } catch {
18 return res.status(400).send('invalid signature');
19 }
20
21 // acknowledge first, work afterwards
22 res.sendStatus(202);
23 queue.enqueue(event);
24});

The signing secret is shown once when the endpoint is created and can be rotated at any time. During a rotation both secrets are accepted for one hour, so you can deploy the new value without dropping a delivery.

Retries and ordering

A delivery is successful when your endpoint answers 2xx within ten seconds. Anything else — a 5xx, a timeout, a connection reset — is retried with exponential backoff and jitter across roughly three days.

AttemptDelay after previous
1immediate
230 seconds
35 minutes
430 minutes
52 hours
68 hours
724 hours
836 hours

After the last attempt the endpoint is marked degraded and you are notified. The events are not lost: read them back from GET /events and replay at your own pace.

Ordering is not guaranteed. A retry can arrive after a newer event. Compare created_at, or refetch the object from the API, before overwriting state you already hold.

Idempotency

Delivery is at least once. A network hiccup after your handler committed but before the response reached us produces a duplicate, and duplicates are normal rather than exceptional. Key your processing on Paycux-Event-Id.

handler.ts
1const eventId = request.headers['paycux-event-id'];
2
3// unique index on event_id turns a duplicate into a no-op
4const inserted = await db.processedEvents.insertIfAbsent(eventId);
5if (!inserted) return reply.status(202).send();
6
7await applyEvent(event);

Testing locally

Every endpoint has a delivery log in the dashboard with the exact bytes we sent, the status we got back and a replay button. For local work, expose your development server over a tunnel and register that URL in your staging environment.

  • Replay any past delivery from the dashboard without touching production data
  • Send a synthetic test event for any type you subscribe to
  • Delivery logs keep the request and response bodies for 30 days

Where events come from

Most webhook traffic originates in a customer’s directory. If you are still deciding what to subscribe to, start from what Directory Sync produces.