Reference
Webhooks
A webhook endpoint is a URL of yours that receives signed deliveries when something changes. Most teams register one from the dashboard and never touch it again; the endpoints here exist for the teams that provision environments programmatically, and for rotating a secret without a support ticket.
The endpoint object
An endpoint is a URL, a subscription list and a secret. The secret is the part that matters: a delivery is just an HTTP POST, and without verifying the signature there is nothing to distinguish a genuine one from anybody else's.
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier, prefixed webhook_. |
| object | string | Always "webhook_endpoint". |
| endpoint_url | string | Where deliveries are POSTed. Must be https, and must answer within 10 seconds. |
| events | array | Event types this endpoint receives. Subscribe to what you handle — an endpoint subscribed to everything is an endpoint that ignores most of what it gets. |
| signing_secret | string | Returned in full only on creation and on rotation. Verify every delivery against it; an unverified webhook body is attacker-controlled input. |
| state | string | active, paused or disabled. An endpoint failing continuously is paused automatically. |
| failure_count | integer | Consecutive failed deliveries. Resets to zero on the first success. |
| created_at | timestamp | RFC 3339. |
| updated_at | timestamp | RFC 3339. |
Verify before you parse
Compute the signature over the raw request body, before any JSON parsing, and compare with a constant-time function. A framework that hands you a parsed object has already thrown away the bytes the signature covers.
Endpoint states
An endpoint that fails continuously is paused rather than hammered, and pausing is recoverable. The window during which deliveries are retained is finite, so an endpoint left paused over a long weekend will lose events regardless of what you do afterwards.
| State | Meaning | Notes |
|---|---|---|
| active | Receiving deliveries normally | The state you want. failure_count is usually zero. |
| paused | Paused after sustained failures | Deliveries are queued rather than dropped for a limited window. Fix the endpoint, then resume it. |
| disabled | Turned off by you | Nothing is queued and nothing is delivered. |
Endpoints
Paths are relative to https://api.paycux.com. For payload shapes, signature verification and retry behaviour, see Webhooks.
| Method | Path | What it does |
|---|---|---|
| GET | /webhooks | List the webhook endpoints registered in this environment. |
| POST | /webhooks | Register a URL and the event types it should receive. |
| GET | /webhooks/:id | Retrieve one endpoint with its state and recent failure count. |
| PUT | /webhooks/:id | Change the URL, the subscribed event list or the state. |
| POST | /webhooks/:id/rotate_secret | Issue a new signing secret, with a grace period during which both are valid. |
| DELETE | /webhooks/:id | Remove an endpoint and discard anything queued for it. |
List endpoints
Signing secrets are redacted in list responses — you get a prefix, not the value. If you have lost a secret, rotate it rather than trying to read it back.
1curl https://api.paycux.com/webhooks \2 -H "Authorization: Bearer sk_example_123456789"
1HTTP 2002{3 "object": "list",4 "data": [5 {6 "object": "webhook_endpoint",7 "id": "webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2",8 "endpoint_url": "https://api.yourapp.example/paycux/webhook",9 "events": ["dsync.user.created", "dsync.user.deleted", "connection.activated"],10 "signing_secret": "whsec_example_...",11 "state": "active",12 "failure_count": 0,13 "created_at": "2026-06-02T07:55:12.400Z",14 "updated_at": "2026-06-02T07:55:12.400Z"15 }16 ],17 "list_metadata": { "before": null, "after": null }18}
Register an endpoint
This is the only response that contains the full signing secret. Store it in your secret manager as you would a database password — anyone holding it can forge a delivery that your handler will accept as genuine.
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", "connection.activated"]7 }'
1HTTP 2012{3 "object": "webhook_endpoint",4 "id": "webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2",5 "endpoint_url": "https://api.yourapp.example/paycux/webhook",6 "events": ["dsync.user.created", "dsync.user.deleted", "connection.activated"],7 "signing_secret": "whsec_example_123456789",8 "state": "active",9 "failure_count": 0,10 "created_at": "2026-06-02T07:55:12.400Z",11 "updated_at": "2026-06-02T07:55:12.400Z"12}
Retrieve an endpoint
failure_count is the field worth alerting on. A handler that started returning 500 an hour ago is not obvious from your own logs, because from your side nothing is happening at all.
1curl https://api.paycux.com/webhooks/webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \2 -H "Authorization: Bearer sk_example_123456789"
1HTTP 2002{3 "object": "webhook_endpoint",4 "id": "webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2",5 "endpoint_url": "https://api.yourapp.example/paycux/webhook",6 "events": ["dsync.user.created", "dsync.user.deleted", "connection.activated"],7 "signing_secret": "whsec_example_...",8 "state": "active",9 "failure_count": 0,10 "last_delivery_at": "2026-07-04T11:02:38.114Z",11 "created_at": "2026-06-02T07:55:12.400Z",12 "updated_at": "2026-06-02T07:55:12.400Z"13}
Update an endpoint
The events array replaces the previous subscription rather than adding to it. Changing endpoint_url does not rotate the secret, so a move between hosts keeps working without a deploy of both sides at once.
1curl -X PUT https://api.paycux.com/webhooks/webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \2 -H "Authorization: Bearer sk_example_123456789" \3 -H "Content-Type: application/json" \4 -d '{5 "events": [6 "dsync.user.created",7 "dsync.user.updated",8 "dsync.user.deleted",9 "dsync.group.user_added",10 "dsync.group.user_removed"11 ]12 }'
1HTTP 2002{3 "object": "webhook_endpoint",4 "id": "webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2",5 "endpoint_url": "https://api.yourapp.example/paycux/webhook",6 "events": [7 "dsync.user.created",8 "dsync.user.updated",9 "dsync.user.deleted",10 "dsync.group.user_added",11 "dsync.group.user_removed"12 ],13 "state": "active",14 "updated_at": "2026-07-05T08:31:19.775Z"15}
Rotate the signing secret
Both secrets verify for the grace period, so deploy the new one and let the old expire rather than coordinating a simultaneous switch. Rotate on a schedule, and immediately if a secret has ever been written to a log.
1curl -X POST https://api.paycux.com/webhooks/webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2/rotate_secret \2 -H "Authorization: Bearer sk_example_123456789" \3 -H "Content-Type: application/json" \4 -d '{ "grace_period_seconds": 86400 }'
1HTTP 2012{3 "object": "webhook_endpoint",4 "id": "webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2",5 "signing_secret": "whsec_example_987654321",6 "previous_secret_expires_at": "2026-07-06T08:31:19.775Z"7}
Delete an endpoint
Deleting is not the same as pausing. Queued deliveries are discarded rather than held, so if the endpoint is temporarily broken, set the state to paused instead and keep the queue.
1curl -X DELETE https://api.paycux.com/webhooks/webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \2 -H "Authorization: Bearer sk_example_123456789"
1HTTP 2042(no body)
Errors
A non-https endpoint_url returns 422, as does subscribing to an event type that does not exist — which catches the common typo of a singular where the event name is plural. Rotating a secret on a deleted endpoint returns 404.
Pagination
GET /webhooks is cursor paginated with limit, before and after, though most environments have one endpoint and some have three. Delivery history is read from the events resource, which is paginated the same way.
1import { createHmac, timingSafeEqual } from "node:crypto";23export function verify(rawBody: string, header: string, secret: string) {4 const [ts, sig] = header.split(",").map((p) => p.split("=")[1]);5 const expected = createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");67 return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));8}