Webhooks
Receive real-time event notifications.
Receive real-time notifications when events happen in your account. Configure webhook endpoints in your workspace settings.
Event types
| Event | Description |
|---|---|
| payment.completed | A payment was successfully processed |
| payment.refunded | A transaction was refunded (full or partial) |
| payment.voided | A pending transaction was voided |
| payment.failed | A pending payment was rejected by the network (Debit & Payouts) |
| payment.disputed | A settled payment was disputed — chargeback or ACH return (Debit & Payouts) |
| dispute.created | A dispute or pre-chargeback alert was received |
| dispute.updated | A dispute changed status |
| dispute.evidence_submitted | A chargeback response was submitted to the network |
| dispute.won | A fought chargeback was decided in your favor |
| dispute.lost | A chargeback was decided for the cardholder |
| payout.created | A payout was created and accepted for processing |
| payout.updated | A payout was approved and is in transit |
| payout.paid | A payout settled — the recipient has the funds |
| payout.failed | A payout was rejected by the network before settlement |
| payout.returned | A settled payout was sent back by the receiving bank (ACH return, e.g. R03) |
| subscription.created | A subscription was created via the API |
| subscription.renewed | A subscription was automatically renewed |
| subscription.cancel_scheduled | A subscription is scheduled to cancel at period end |
| subscription.cancelled | A subscription was cancelled |
| subscription.paused | A subscription was paused |
| subscription.resumed | A paused subscription was resumed |
| subscription.scheduled | A subscription was created with a future start date |
| subscription.activated | A scheduled subscription has activated and started billing |
Payload structure
All webhook payloads follow this envelope structure. The data object varies by event type.
{
"id": "event-uuid",
"type": "payment.completed",
"created": 1705312200,
"data": { ... }
}payment.completed
Fired when a payment is successfully processed, from all three ways to take one: a payment link, a checkout session, or a direct POST /charges. Includes the originating payment link, checkout session info (if created via API), metadata, and any subscription IDs created. A direct charge sends source: "api" with no payment_link_id or checkout_session_id, since it has neither. On Paysio Debit & Payouts workspaces this event fires at settlement — possibly minutes to days after the charge was accepted. Everywhere else it fires as soon as the charge is approved.
{
"id": "evt_abc123",
"type": "payment.completed",
"created": 1705312200,
"data": {
"transaction_id": "txn_def456",
"amount": 2700,
"currency": "USD",
"status": "pending_settlement",
"customer_email": "[email protected]",
"order_number": 1042,
"payment_link_id": "pl_ghi789",
"source": "api",
"checkout_session_id": "pl_ghi789",
"metadata": { "workspace_id": "ws_123", "plan": "basic" },
"subscription_ids": ["sub_jkl012"]
}
}transaction_idstring- The transaction ID
amountinteger- Amount in cents
currencystring- Currency code (e.g. "USD")
statusstring- Transaction status
customer_emailstring- Customer's email
order_numberinteger- Order number
customer_idstring- Customer ID, when the charge is linked to one
payment_link_idstring- The payment link ID (checkout only)
sourcestring- "dashboard", or "api" for a checkout session or a direct charge
checkout_session_idstring- Checkout session ID (checkout sessions only)
processorstring- Processor that took the payment (direct charges)
railstring- "card" or "ach" (direct charges)
metadataobject- Metadata from the checkout session or charge
subscription_idsstring[]- Created subscription IDs (if recurring)
payment.refunded
Fired when a transaction is refunded (full or partial).
{
"id": "evt_abc123",
"type": "payment.refunded",
"created": 1705312200,
"data": {
"transaction_id": "txn_def456",
"amount": 2700,
"currency": "USD",
"refunded_amount": 1000,
"total_refunded": 1000,
"payment_link_id": "pl_ghi789"
}
}payment.voided
Fired when a pending (unsettled) transaction is voided.
{
"id": "evt_abc123",
"type": "payment.voided",
"created": 1705312200,
"data": {
"transaction_id": "txn_def456",
"amount": 2700,
"currency": "USD",
"payment_link_id": "pl_ghi789"
}
}payment.failed
Fired on Debit & Payouts workspaces when a pending payment (card pull or ACH debit) is rejected by the network. On these workspaces payment.completed fires at settlement, so a charge that returned pending_settlement always ends in exactly one of the two events.
{
"id": "evt_abc123",
"type": "payment.failed",
"created": 1705312200,
"data": {
"transaction_id": "txn_def456",
"amount": 2999,
"currency": "USD",
"status": "failed",
"rail": "card",
"error_code": "T002",
"error_message": "The card was declined by the issuing bank.",
"processor": "aptpay"
}
}payment.disputed
Fired on Debit & Payouts workspaces when a settled payment is disputed — a card chargeback or an ACH return. dispute_kind is "chargeback" or "ach_return".
{
"id": "evt_abc123",
"type": "payment.disputed",
"created": 1705312200,
"data": {
"transaction_id": "txn_def456",
"amount": 2999,
"currency": "USD",
"status": "disputed",
"rail": "ach",
"dispute_kind": "ach_return",
"dispute_code": "R01",
"dispute_reason": "Insufficient funds",
"processor": "aptpay"
}
}dispute.created / updated / evidence_submitted / won / lost
The full dispute lifecycle, across every kind of case: pre-chargeback alerts (Ethoca, CDRN, Visa RDR), chargebacks, TC40 fraud reports, ACH returns, and reversals. dispute.created fires when a case is received, dispute.updated on any status change, dispute.evidence_submitted when a chargeback response goes to the network, and dispute.won / dispute.lost when the issuer decides. All five share the same data payload. Retrieve the full case with GET /disputes/:id. The legacy payment.disputed event is unchanged and still fires alongside these for processor-reported chargebacks and ACH returns.
{
"id": "evt_abc123",
"type": "dispute.created",
"created": 1705312200,
"data": {
"dispute_id": "dsp_def456",
"transaction_id": "txn_ghi789",
"kind": "chargeback",
"status": "new",
"network": "visa",
"reason": "Fraud — card absent environment",
"reason_code": "10.4",
"amount": 2999,
"currency": "USD",
"card_last4": "4242",
"due_at": "2026-02-01T00:00:00.000Z",
"sandbox": false
}
}dispute_idstring- The dispute ID — retrieve it with GET /disputes/:id
transaction_idstring- The matched transaction ID (null while unmatched)
kindstring- "alert_ethoca", "alert_cdrn", "alert_rdr", "chargeback", "tc40", "ach_return", or "reversal"
statusstring- "new", "matched", "unmatched", "refunded", "accepted", "fighting", "response_submitted", "won", "lost", or "expired"
networkstring- Card network (e.g. "visa"), when known
reasonstring- Human-readable dispute reason
reason_codestring- Network reason code (e.g. "10.4")
amountinteger- Disputed amount in cents
currencystring- Currency code
card_last4string- Last four digits of the disputed card
due_atstring- Response deadline (ISO 8601), when the network set one
sandboxboolean- Whether this event is from test mode
payout.created / updated / paid / failed / returned
The payout lifecycle on Debit & Payouts workspaces. payout.created fires when POST /payouts accepts the payout, payout.updated when it is approved and in transit, then exactly one of payout.paid or payout.failed. A settled ACH payout can later be sent back by the receiving bank, which fires payout.returned. All five share the same data payload — the payout object from the Payouts API.
{
"id": "evt_abc123",
"type": "payout.paid",
"created": 1705312200,
"data": {
"id": "po_uuid",
"object": "payout",
"payout_number": 17,
"amount": 5000,
"currency": "USD",
"status": "paid",
"rail": "card",
"rtp_network": null,
"card_last_four": "5702",
"card_brand": "mastercard",
"descriptor": "ACME PAY",
"customer_id": "cus_uuid",
"purpose": "payout",
"refund_for_transaction_id": null,
"reference_id": "payout-2026-001",
"error_code": null,
"error_message": null,
"returned": false,
"returned_at": null,
"return_code": null,
"return_reason": null,
"metadata": { "invoice": "inv_123" },
"livemode": true,
"created_at": "2026-01-15T10:30:00.000Z",
"updated_at": "2026-01-15T10:31:12.000Z"
}
}payout.failed carries the failure details:
{
"id": "evt_abc123",
"type": "payout.failed",
"created": 1705312200,
"data": {
"id": "po_uuid",
"object": "payout",
"amount": 5000,
"currency": "USD",
"status": "failed",
"rail": "card",
"error_code": "M009",
"error_message": "This card cannot receive push-to-card payments.",
"returned": false,
"returned_at": null,
"return_code": null,
"return_reason": null,
"customer_id": "cus_uuid",
"purpose": "payout",
"metadata": {},
"livemode": true
}
}payout.failed and payout.returned are not the same event
payout.failed means the money never left: the network rejected the payout before settlement and the reserved amount is back on your balance. payout.returned means it left, settled, and the receiving bank sent it back days later. Treating them alike is how a recipient gets credited twice.
payout.returned fires once, when the return is recorded. Note that status stays "paid" and error_code stays null — the return is reported in returned, returned_at, return_code and return_reason:
{
"id": "evt_abc123",
"type": "payout.returned",
"created": 1705312200,
"data": {
"id": "po_uuid",
"object": "payout",
"payout_number": 19,
"amount": 19600,
"currency": "USD",
"status": "paid",
"rail": "ach",
"error_code": null,
"error_message": null,
"returned": true,
"returned_at": "2026-08-13T22:52:00.000Z",
"return_code": "R03",
"return_reason": "NO ACCOUNT FOUND",
"customer_id": "cus_uuid",
"purpose": "payout",
"metadata": {},
"livemode": true
}
}subscription.created
Fired when a subscription is created via the POST /subscriptions API endpoint.
{
"id": "evt_abc123",
"type": "subscription.created",
"created": 1705312200,
"data": {
"subscription_id": "sub_new123",
"customer_id": "cus_abc456",
"product_id": "prod_def789",
"transaction_id": "txn_ghi012",
"amount": 2700,
"currency": "USD",
"metadata": { "plan": "basic" }
}
}subscription.renewed
Fired when a subscription is automatically renewed and the renewal charge succeeds.
{
"id": "evt_abc123",
"type": "subscription.renewed",
"created": 1705312200,
"data": {
"subscription_id": "sub_jkl012",
"transaction_id": "txn_mno345",
"amount": 2700,
"currency": "USD"
}
}subscription.cancel_scheduled
Fired when a subscription is scheduled to cancel at the end of the current billing period.
{
"id": "evt_abc123",
"type": "subscription.cancel_scheduled",
"created": 1705312200,
"data": {
"subscription_id": "sub_jkl012",
"reason": "Switching to basic plan",
"cancel_at": "2026-05-11T00:00:00.000Z"
}
}subscription.cancelled
Fired when a subscription is cancelled (immediately or at end of scheduled period).
{
"id": "evt_abc123",
"type": "subscription.cancelled",
"created": 1705312200,
"data": {
"subscription_id": "sub_jkl012",
"reason": "Customer requested cancellation"
}
}subscription.paused
Fired when a subscription is paused.
{
"id": "evt_abc123",
"type": "subscription.paused",
"created": 1705312200,
"data": {
"subscription_id": "sub_jkl012"
}
}subscription.resumed
Fired when a paused subscription is resumed.
{
"id": "evt_abc123",
"type": "subscription.resumed",
"created": 1705312200,
"data": {
"subscription_id": "sub_jkl012"
}
}subscription.scheduled
Fired when a subscription is created with a future start_date. The subscription will not be charged until the start date arrives.
{
"id": "evt_abc123",
"type": "subscription.scheduled",
"created": 1705312200,
"data": {
"subscription_id": "sub_new123",
"customer_id": "cus_abc456",
"product_id": "prod_def789",
"start_date": "2026-06-11T00:00:00.000Z",
"metadata": { "plan": "advanced" }
}
}subscription.activated
Fired when a scheduled subscription reaches its start date, is charged for the first time, and becomes active.
{
"id": "evt_abc123",
"type": "subscription.activated",
"created": 1705312200,
"data": {
"subscription_id": "sub_new123",
"customer_id": "cus_abc456",
"product_id": "prod_def789",
"transaction_id": "txn_first456",
"amount": 8700,
"currency": "USD",
"metadata": { "plan": "advanced" }
}
}Verifying signatures
Every delivery includes a Paysio-Signature header (and a Paysio-Event header). Verify it with HMAC-SHA256 over `${t}.${rawBody}` using your endpoint's signing secret (the whsec_… value).
Use the raw request body
Sign the bytes exactly as received — do not JSON.parse then re-stringify first, or the signature will never match. In Express, express.json() consumes the raw body before your handler runs, so capture it with express.raw() on the webhook route. A 401 from your endpoint is almost always this.
const crypto = require('crypto')
// Capture the RAW body ONLY on the webhook route (not app-wide express.json()).
app.post('/webhooks/paysio',
express.raw({ type: 'application/json' }),
(req, res) => {
const header = req.header('Paysio-Signature') || ''
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const raw = req.body // a Buffer — the exact bytes Paysio signed
const expected = crypto
.createHmac('sha256', process.env.PAYSIO_WEBHOOK_SECRET)
.update(`${parts.t}.${raw}`)
.digest('hex')
const ok = parts.v1 &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
if (!ok) return res.status(401).send('invalid signature')
const event = JSON.parse(raw.toString('utf8'))
// ... handle event.type / event.data ...
res.sendStatus(200) // ACK fast — only 2xx counts as delivered
})Retry policy
Failed deliveries (non-2xx response or timeout) are retried up to 3 times with exponential backoff:
- Attempt 1: immediate
- Attempt 2: after 5 minutes
- Attempt 3: after 30 minutes
Deliveries time out after 10 seconds. Your endpoint should return a 2xx status quickly and process the event asynchronously if needed.