Skip to content

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.

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.

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.
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)
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

Field Type Required Allowed values and limits What it is
{id} string (uuid) yes The webhook delivery’s id.
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
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

Field Type Required Allowed values and limits What it is
{id} string (uuid) yes The webhook delivery whose attempts to list.
Field Type What it is
data array
delivery_id string
object string
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

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.
Field Type What it is
attempt_count integer
id string
next_attempt_at string (date-time)
notice string
object string
status string
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 409: it conflicts with the current state, or the idempotency key was used for a different request
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

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.
Terminal window
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/…"
}'
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
  • 400: the request is malformed
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 409: it conflicts with the current state, or the idempotency key was used for a different request
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

Field Type What it is
data array
object string
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

Field Type Required Allowed values and limits What it is
{id} string (uuid) yes The webhook endpoint’s id.
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
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

Field Type Required Allowed values and limits What it is
{id} string (uuid) yes The webhook endpoint to update.
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.
Terminal window
curl -X PATCH https://sandbox.paysigna.com/v1/webhook-endpoints/{id} \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{}'
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
  • 400: the request is malformed
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 409: it conflicts with the current state, or the idempotency key was used for a different request
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

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.
Field Type What it is
deleted boolean
id string
object string
status string
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.


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.

Field Type Required Allowed values and limits What it is
{id} string (uuid) yes The webhook endpoint whose signing secret to replace.
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.
Terminal window
curl -X POST https://sandbox.paysigna.com/v1/webhook-endpoints/{id}/rotate-secret \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{}'
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
  • 401: the key is missing, wrong or revoked
  • 403: the key lacks the scope, or your account may not do this
  • 404: no such resource on this account
  • 422: a field is invalid; errors names it
  • 429: too many requests; wait for Retry-After
  • 500: Internal Server Error
  • 503: the payment partner is unreachable; safe to retry

Every error is a JSON problem with a stable code. See Errors.