Skip to content

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.

FieldTypeDescription
idstringUnique identifier, prefixed webhook_.
objectstringAlways "webhook_endpoint".
endpoint_urlstringWhere deliveries are POSTed. Must be https, and must answer within 10 seconds.
eventsarrayEvent 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_secretstringReturned in full only on creation and on rotation. Verify every delivery against it; an unverified webhook body is attacker-controlled input.
statestringactive, paused or disabled. An endpoint failing continuously is paused automatically.
failure_countintegerConsecutive failed deliveries. Resets to zero on the first success.
created_attimestampRFC 3339.
updated_attimestampRFC 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.

StateMeaningNotes
activeReceiving deliveries normallyThe state you want. failure_count is usually zero.
pausedPaused after sustained failuresDeliveries are queued rather than dropped for a limited window. Fix the endpoint, then resume it.
disabledTurned off by youNothing 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.

MethodPathWhat it does
GET/webhooksList the webhook endpoints registered in this environment.
POST/webhooksRegister a URL and the event types it should receive.
GET/webhooks/:idRetrieve one endpoint with its state and recent failure count.
PUT/webhooks/:idChange the URL, the subscribed event list or the state.
POST/webhooks/:id/rotate_secretIssue a new signing secret, with a grace period during which both are valid.
DELETE/webhooks/:idRemove 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.

GET /webhooks
1curl https://api.paycux.com/webhooks \
2 -H "Authorization: Bearer sk_example_123456789"
response
1HTTP 200
2{
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.

POST /webhooks
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 }'
response
1HTTP 201
2{
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.

GET /webhooks/:id
1curl https://api.paycux.com/webhooks/webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \
2 -H "Authorization: Bearer sk_example_123456789"
response
1HTTP 200
2{
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.

PUT /webhooks/:id
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 }'
response
1HTTP 200
2{
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.

POST /webhooks/:id/rotate_secret
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 }'
response
1HTTP 201
2{
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.

DELETE /webhooks/:id
1curl -X DELETE https://api.paycux.com/webhooks/webhook_01HQ8ZK3M4N5P6R7S8T9V0W1X2 \
2 -H "Authorization: Bearer sk_example_123456789"
response
1HTTP 204
2(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.

verify.ts
1import { createHmac, timingSafeEqual } from "node:crypto";
2
3export 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");
6
7 return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
8}