Webhooks
Endpoints PaySigna sends signed events to, and what was delivered.
| Endpoint | What it does |
|---|---|
GET /v1/webhook-deliveries |
List webhook deliveries |
GET /v1/webhook-deliveries/{id} |
Retrieve a webhook delivery |
GET /v1/webhook-deliveries/{id}/attempts |
List a delivery’s attempts |
POST /v1/webhook-deliveries/{id}/replay |
Replay a webhook delivery |
POST /v1/webhook-endpoints |
Create a webhook endpoint |
GET /v1/webhook-endpoints |
List webhook endpoints |
GET /v1/webhook-endpoints/{id} |
Retrieve a webhook endpoint |
PATCH /v1/webhook-endpoints/{id} |
Update a webhook endpoint |
DELETE /v1/webhook-endpoints/{id} |
Delete a webhook endpoint |
POST /v1/webhook-endpoints/{id}/rotate-secret |
Rotate a webhook signing secret |
Try any of them in the API tester.
List webhook deliveries
Section titled “List webhook deliveries”GET /v1/webhook-deliveries
One row per (event, endpoint) pair. Keyset-paginated, newest first: pass the paging.next_cursor from one response as cursor on the next.
A date window is always applied: omitting one defaults to the last 7 days, and a window wider than 90 days is refused rather than silently truncated. The applied window is echoed back, so a short result is distinguishable from a short history.
Filter on status=exhausted to find what we gave up on. That is also the set a replay accepts.
Authentication: a secret key (server-side only) with the scope webhook.read.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
limit |
integer | no | min 1; max 100; default 25 |
Rows per page. Clamped to 100. |
cursor |
string | no | The paging.next_cursor from the previous page. Omit for the first page. | |
created_after |
string | no | RFC 3339, inclusive. Defaults to 7 days ago. The window may not exceed 90 days. | |
created_before |
string | no | RFC 3339, exclusive. Defaults to now. | |
endpoint_id |
string (uuid) | no | Only deliveries to this endpoint. | |
event_type |
string | no | at most 60 characters; e.g. invoice.paid |
Only deliveries of this event type, exact match. Codes are listed at /v1/event-types. |
status |
string | no | One of: queued, delivering, delivered, failed, exhausted, skipped |
Only deliveries in this state. queued: waiting to be sent. delivering: being sent. delivered: your endpoint accepted it. failed: the last attempt failed and a retry is scheduled. exhausted: every attempt failed. skipped: the endpoint was disabled before it was sent. |
Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
data |
array | |
object |
string | |
paging |
object | |
paging.has_more |
boolean | |
paging.limit |
integer | The page size actually applied, after clamping. |
paging.next_cursor |
string | Pass as ?cursor= to fetch the next page. Absent when has_more is false. |
window |
object | |
window.created_after |
string (date-time) | |
window.created_before |
string (date-time) |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Retrieve a webhook delivery
Section titled “Retrieve a webhook delivery”GET /v1/webhook-deliveries/{id}
The current state of one delivery: its status, how many attempts it has had, when the next one is due, and the status your endpoint returned last.
Authentication: a secret key (server-side only) with the scope webhook.read.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook delivery’s id. |
Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
attempt_count |
integer | |
created_at |
string (date-time) | |
delivered_at |
string (date-time) | |
endpoint_id |
string | |
endpoint_url |
string | |
event_id |
string | The event. Several deliveries share one event when you have several endpoints. |
event_type |
string | |
id |
string | |
last_error |
string | Our description of why the last attempt failed — a timeout, a refused connection, an unexpected status. |
last_http_status |
integer | The HTTP status your endpoint returned on the most recent attempt. Absent when the attempt never got a response at all. |
next_attempt_at |
string (date-time) | When the next retry is due. Absent once the delivery is terminal. |
object |
string | |
resource_id |
string | |
resource_type |
string | |
sequence |
integer | |
status |
string | exhausted means we stopped retrying. That is the state a replay accepts. One of: queued, delivering, delivered, failed, exhausted, skipped |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
List a delivery’s attempts
Section titled “List a delivery’s attempts”GET /v1/webhook-deliveries/{id}/attempts
Every individual HTTP call we made, in order, with the status your endpoint returned, how long it took, and a bounded excerpt of your response body.
THIS IS THE ENDPOINT TO READ BEFORE OPENING A SUPPORT TICKET. “It failed four times with 502 then succeeded” is usually the whole answer.
response_excerpt is truncated and has control characters removed, and response_truncated tells you when there was more. Response HEADERS are not returned: they routinely carry your own session cookies and upstream tokens, and none of it helps diagnose a webhook.
Not paginated, and that is a bound rather than an omission: a delivery has at most max_attempts attempts and max_attempts is capped at 20.
Authentication: a secret key (server-side only) with the scope webhook.read.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook delivery whose attempts to list. |
Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
data |
array | |
delivery_id |
string | |
object |
string |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Replay a webhook delivery
Section titled “Replay a webhook delivery”POST /v1/webhook-deliveries/{id}/replay
Queues one failed or exhausted delivery for another run.
ONE DELIVERY PER REQUEST. There is no bulk replay, deliberately: a thousand deliveries re-queued at once against an endpoint that was failing minutes ago is an outage you inflict on yourself, and this way the work goes through your own rate-limit bucket where you can see it.
THE ATTEMPT COUNTER IS NOT RESET. A delivery that has used 7 of 8 attempts has one left. The attempt history is kept; this is a new run, not a fresh start.
Idempotent: only failed and exhausted deliveries are accepted, so replaying one that is already queued answers 409 and changes nothing. A delivery whose endpoint is disabled is also refused — fix the endpoint first, or the replay only produces another failure.
If you need the event rather than the delivery, read /v1/events. That does not re-send anything.
Authentication: a secret key (server-side only) with the scope webhook.write.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook delivery to send again. Only a failed or exhausted delivery can be replayed. |
Response 202
Section titled “Response 202”| Field | Type | What it is |
|---|---|---|
attempt_count |
integer | |
id |
string | |
next_attempt_at |
string (date-time) | |
notice |
string | |
object |
string | |
status |
string |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account409: it conflicts with the current state, or the idempotency key was used for a different request422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Create a webhook endpoint
Section titled “Create a webhook endpoint”POST /v1/webhook-endpoints
Registers a URL we deliver events to.
THE signing_secret IN THIS RESPONSE IS SHOWN ONCE AND NEVER AGAIN. Store it where your handler can read it. No endpoint will return it; if you lose it, rotate it.
The URL must be HTTPS and must be publicly routable. Loopback, private, link-local and internal-only hostnames are refused, because our delivery workers run inside our own network.
No Idempotency-Key is required: a repeated registration of the same URL in the same environment is a 409 rather than a second endpoint. If a create times out, LIST your endpoints — if it is there, rotate its secret rather than creating it again.
Subscribe explicitly with subscribed_events. An empty list means every event type, which costs you the deliveries and us the retries for events you ignore.
Authentication: a secret key (server-side only) with the scope webhook.write.
Request body
Section titled “Request body”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
description |
string | no | at most 200 characters | For your own records. Shown in the dashboard. |
max_attempts |
integer | no | min 1; max 20 | Default 8. |
subscribed_events |
array | no | Event type codes from /v1/event-types. Omit or send an empty array to receive everything, though subscribing explicitly is strongly preferred. | |
timeout_seconds |
integer | no | min 1; max 60 | Default 10. We count a slower response as a failed attempt; make your handler enqueue and return, not process and return. |
url |
string (uri) | yes | at most 2000 characters | HTTPS only, and must resolve to a publicly routable address. Private, loopback and link-local addresses are refused. |
curl -X POST https://sandbox.paysigna.com/v1/webhook-endpoints \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/…"}'Response 201
Section titled “Response 201”| Field | Type | What it is |
|---|---|---|
created_at |
string (date-time) | |
description |
string | |
environment |
string | One of: test, live |
health |
object | |
health.auto_disabled_at |
string (date-time) | We abandoned this endpoint after repeated failures. Events are no longer delivered to it. Fix the endpoint, then PATCH status back to active. |
health.consecutive_failures |
integer | |
health.last_failure_at |
string (date-time) | |
health.last_failure_reason |
string | |
health.last_success_at |
string (date-time) | |
id |
string | |
max_attempts |
integer | How many times one event is attempted before it is abandoned. |
object |
string | |
previous_secret_expires_at |
string (date-time) | Until this moment, deliveries are signed with BOTH the current and the previous secret and either verifies. After it, only the current one does. |
secret_rotated_at |
string (date-time) | |
signing_secret |
string | SAVE THIS NOW. It is shown once, in this response, and no endpoint will ever return it again. If you lose it, rotate. |
signing_secret_notice |
string | |
status |
string | disabled with auto_disabled_at set means we gave up on it. Re-enable it with a PATCH once the endpoint is fixed; that also clears the failure streak. One of: active, disabled, terminated |
subscribed_events |
array | Empty means every event type. Subscribe explicitly: an endpoint subscribed to everything that then ignores most of it still costs you the deliveries and us the retries. |
timeout_seconds |
integer | How long we wait for your response before counting the attempt as failed. |
updated_at |
string (date-time) | |
url |
string |
Errors
Section titled “Errors”400: the request is malformed401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this409: it conflicts with the current state, or the idempotency key was used for a different request422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
List webhook endpoints
Section titled “List webhook endpoints”GET /v1/webhook-endpoints
Your endpoints in the environment of the key used.
Never returns a signing secret. health is the part worth reading: an endpoint with a rising consecutive_failures is on its way to being auto-disabled, and one with auto_disabled_at set is no longer receiving events at all.
Authentication: a secret key (server-side only) with the scope webhook.read.
Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
data |
array | |
object |
string |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Retrieve a webhook endpoint
Section titled “Retrieve a webhook endpoint”GET /v1/webhook-endpoints/{id}
Adds previous_secret_expires_at to what the list returns: during a rotation that is the moment your old secret stops being accepted.
Authentication: a secret key (server-side only) with the scope webhook.read.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook endpoint’s id. |
Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
created_at |
string (date-time) | |
description |
string | |
environment |
string | One of: test, live |
health |
object | |
health.auto_disabled_at |
string (date-time) | We abandoned this endpoint after repeated failures. Events are no longer delivered to it. Fix the endpoint, then PATCH status back to active. |
health.consecutive_failures |
integer | |
health.last_failure_at |
string (date-time) | |
health.last_failure_reason |
string | |
health.last_success_at |
string (date-time) | |
id |
string | |
max_attempts |
integer | How many times one event is attempted before it is abandoned. |
object |
string | |
previous_secret_expires_at |
string (date-time) | Until this moment, deliveries are signed with BOTH the current and the previous secret and either verifies. After it, only the current one does. |
secret_rotated_at |
string (date-time) | |
status |
string | disabled with auto_disabled_at set means we gave up on it. Re-enable it with a PATCH once the endpoint is fixed; that also clears the failure streak. One of: active, disabled, terminated |
subscribed_events |
array | Empty means every event type. Subscribe explicitly: an endpoint subscribed to everything that then ignores most of it still costs you the deliveries and us the retries. |
timeout_seconds |
integer | How long we wait for your response before counting the attempt as failed. |
updated_at |
string (date-time) | |
url |
string |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Update a webhook endpoint
Section titled “Update a webhook endpoint”PATCH /v1/webhook-endpoints/{id}
Only the fields you send are changed. subscribed_events replaces the whole list; send [] to receive every event type.
Setting status to active on an endpoint we auto-disabled also clears the failure streak, so it is not one failure away from being disabled again. Fix the endpoint first — re-enabling a broken one just consumes retries.
A new url is revalidated exactly as on create. Use DELETE to remove an endpoint; terminated is not a status you can PATCH.
Authentication: a secret key (server-side only) with the scope webhook.write.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook endpoint to update. |
Request body
Section titled “Request body”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
description |
string | no | at most 200 characters | For your own records. Omit to leave it unchanged. |
max_attempts |
integer | no | min 1; max 20 | How many times each delivery is tried before it is marked exhausted. Omit to leave it unchanged. |
status |
string | no | One of: active, disabled |
Setting active on an endpoint we auto-disabled also clears the failure streak, so it is not one failure away from being disabled again. |
subscribed_events |
array of string | no | Replaces the whole list. Send [] to receive every event type. | |
timeout_seconds |
integer | no | min 1; max 60 | How long we wait for your endpoint to respond before counting the attempt as failed. Omit to leave it unchanged. |
url |
string (uri) | no | at most 2000 characters | Revalidated exactly as on create. |
curl -X PATCH https://sandbox.paysigna.com/v1/webhook-endpoints/{id} \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{}'Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
created_at |
string (date-time) | |
description |
string | |
environment |
string | One of: test, live |
health |
object | |
health.auto_disabled_at |
string (date-time) | We abandoned this endpoint after repeated failures. Events are no longer delivered to it. Fix the endpoint, then PATCH status back to active. |
health.consecutive_failures |
integer | |
health.last_failure_at |
string (date-time) | |
health.last_failure_reason |
string | |
health.last_success_at |
string (date-time) | |
id |
string | |
max_attempts |
integer | How many times one event is attempted before it is abandoned. |
object |
string | |
previous_secret_expires_at |
string (date-time) | Until this moment, deliveries are signed with BOTH the current and the previous secret and either verifies. After it, only the current one does. |
secret_rotated_at |
string (date-time) | |
status |
string | disabled with auto_disabled_at set means we gave up on it. Re-enable it with a PATCH once the endpoint is fixed; that also clears the failure streak. One of: active, disabled, terminated |
subscribed_events |
array | Empty means every event type. Subscribe explicitly: an endpoint subscribed to everything that then ignores most of it still costs you the deliveries and us the retries. |
timeout_seconds |
integer | How long we wait for your response before counting the attempt as failed. |
updated_at |
string (date-time) | |
url |
string |
Errors
Section titled “Errors”400: the request is malformed401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account409: it conflicts with the current state, or the idempotency key was used for a different request422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Delete a webhook endpoint
Section titled “Delete a webhook endpoint”DELETE /v1/webhook-endpoints/{id}
Stops delivery to the endpoint immediately and irreversibly. The record is kept as terminated so your delivery history still shows which URL past events went to.
If you only want to pause it, PATCH status to disabled instead — a terminated endpoint cannot be re-enabled, and the same URL can then be registered again as a new endpoint with a new secret.
Authentication: a secret key (server-side only) with the scope webhook.write.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook endpoint to delete. It stops receiving events; its delivery history is kept. |
Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
deleted |
boolean | |
id |
string | |
object |
string | |
status |
string |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.
Rotate a webhook signing secret
Section titled “Rotate a webhook signing secret”POST /v1/webhook-endpoints/{id}/rotate-secret
Mints a new signing secret and returns it. SHOWN ONCE, like the one from create.
THE OLD SECRET KEEPS WORKING FOR AN OVERLAP PERIOD, and previous_secret_expires_at in the response is when it stops. Until then every delivery is signed with BOTH secrets, comma-separated in X-PaySigna-Signature, and your verification should accept the request if ANY v1= element matches. That is what lets you rotate and deploy as two separate acts rather than having to make them simultaneous.
The default overlap is 24 hours. Send overlap_seconds: 0 if you are rotating because the old secret LEAKED — that invalidates it immediately, and any delivery in flight signed with it will fail your verification, which is the right trade in that case.
No Idempotency-Key: if this call times out you did not learn the new secret, so rotate again. The secret you are currently using stays valid for its overlap window throughout.
Authentication: a secret key (server-side only) with the scope webhook.write.
Parameters
Section titled “Parameters”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
{id} |
string (uuid) | yes | The webhook endpoint whose signing secret to replace. |
Request body
Section titled “Request body”| Field | Type | Required | Allowed values and limits | What it is |
|---|---|---|---|---|
overlap_seconds |
integer | no | min 0; max 604800 | How long the previous secret keeps verifying. Default 86400 (24h). Send 0 to invalidate it immediately, which is what you want if you are rotating because the old secret leaked. |
curl -X POST https://sandbox.paysigna.com/v1/webhook-endpoints/{id}/rotate-secret \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{}'Response 200
Section titled “Response 200”| Field | Type | What it is |
|---|---|---|
id |
string | |
object |
string | |
overlap_seconds |
integer | Seconds from now until the previous secret stops being accepted. Deploy the new secret before then; verification accepts both until it passes. |
previous_secret_expires_at |
string (date-time) | |
rotated_at |
string (date-time) | |
signing_secret |
string | The NEW secret. Shown once, in this response. |
signing_secret_notice |
string | |
url |
string |
Errors
Section titled “Errors”401: the key is missing, wrong or revoked403: the key lacks the scope, or your account may not do this404: no such resource on this account422: a field is invalid;errorsnames it429: too many requests; wait forRetry-After500: Internal Server Error503: the payment partner is unreachable; safe to retry
Every error is a JSON problem with a stable code. See Errors.

