Webhooks
Webhooks deliver real-time notifications to your server when a payment changes status. Instead of polling the API for updates, your system receives a POST request describing the change.
Webhook Configuration
Webhook endpoints are configured during merchant onboarding. Contact the Inyo team to:
- Provide your webhook URL(s)
- Configure authentication (Basic auth)
Event Payloads
Every status change is sent as a POST request to your webhook URL with Content-Type: application/json. The event field tells you which kind of payment changed β PULL, PUSH or CHECK. The fields of every event:
| Field | Type | Notes |
|---|---|---|
agentId | string | Your agent identifier |
event | string | PULL, PUSH or CHECK |
status | string | Result of the operation. Same value as newStatus, except when a capture or refund is declined: then status is DECLINED and newStatus keeps the payment's current status |
statusCode | string | 00 when approved; 99 or a decline code such as PAY_022 when declined. See Status codes |
statusDescription | string | Human-readable description of the change or decline reason |
paymentId | uuid | Inyo payment identifier |
externalPaymentId | string | The externalPaymentId you sent when creating the payment |
amount | object | { "total": number, "currency": string } β source amount and currency (ISO 4217) |
recipientAmount | object | PUSH only. { "total": number, "currency": string } β amount delivered to the recipient, present when the payout has currency conversion |
eventDate | string | When the notification was generated, ISO 8601 in UTC |
oldStatus | string | null | Payment status before the change |
newStatus | string | Payment status after the change |
Status codes
statusCode | Meaning |
|---|---|
00 | Approved |
PAY_xxx | Declined by the issuer or provider. The code is the same responseCode the API returns for the payment; see Response codes for its meaning |
99 | Declined, with no specific reason. Sent when a capture, void or refund is declined, or when no decline code is available |
Decide on status / newStatus, not on statusCode; use the code to tell the customer why a payment was declined.
1. PULL
Fires when a pull payment is authorized, captured, challenged, declined, voided or refunded.
{
"agentId": "YOUR.AGENT",
"event": "PULL",
"status": "AUTHORIZED",
"statusCode": "00",
"statusDescription": "Payment authorized successfully",
"paymentId": "469670e2-31e3-4700-b9f2-abee99418dc4",
"externalPaymentId": "order-12345",
"amount": {
"total": 95.00,
"currency": "USD"
},
"eventDate": "2026-10-08T16:31:02.120Z",
"oldStatus": "PENDING",
"newStatus": "AUTHORIZED"
}
Status values: PENDING, AUTHORIZED, CAPTURED, CHALLENGE, DECLINED, VOIDED, REFUNDED.
flowchart TD
P[PENDING] -->|3DS| C[CHALLENGE]
P --> A[AUTHORIZED]
P -->|capture: true| K[CAPTURED]
P -->|rejected| D[DECLINED]
C --> A
C -->|rejected| D
A -->|capture| K
A -->|void| V[VOIDED]
K -->|refund| R[REFUNDED]
With capture: true the payment goes straight to CAPTURED; otherwise it stops at AUTHORIZED until you capture or void it. DECLINED, VOIDED and REFUNDED are final.
A declined capture or refund does not move the payment: the webhook carries status: "DECLINED" with oldStatus and newStatus both at the current status (AUTHORIZED for a capture, CAPTURED for a refund).
{
"event": "PULL",
"status": "DECLINED",
"statusCode": "99",
"statusDescription": "Transaction Declined",
"oldStatus": "AUTHORIZED",
"newStatus": "AUTHORIZED"
}
2. PUSH
Fires when a push payout is accepted, paid or rejected. The create call answers PENDING, and the outcome arrives later by webhook.
{
"agentId": "YOUR.AGENT",
"event": "PUSH",
"status": "CAPTURED",
"statusCode": "00",
"statusDescription": "Payment captured successfully",
"paymentId": "c9036407-555a-46ed-a602-e8179857b28f",
"externalPaymentId": "payout-12345",
"amount": {
"total": 98.20,
"currency": "USD"
},
"recipientAmount": {
"total": 1775.00,
"currency": "MXN"
},
"eventDate": "2026-10-08T18:30:10.935Z",
"oldStatus": "AUTHORIZED",
"newStatus": "CAPTURED"
}
Status values: PENDING, AUTHORIZED, CAPTURED, DECLINED.
flowchart TD
A[PENDING<br/>payout accepted] -->|recipient paid| B[AUTHORIZED]
B --> C[CAPTURED<br/>funds delivered]
A -->|rejected| D[DECLINED]
A successful payout sends two webhooks in sequence, AUTHORIZED then CAPTURED. Treat CAPTURED as the final success and DECLINED as the final failure; for a decline, statusDescription carries the reason.
3. CHECK
Fires when a Check Card Account verification completes. No funds are moved.
{
"agentId": "YOUR.AGENT",
"event": "CHECK",
"status": "AUTHORIZED",
"statusCode": "00",
"statusDescription": "Payment authorized successfully",
"paymentId": "5b0e2f4c-8d1a-4c7e-9a33-2f6b1d0e7c58",
"externalPaymentId": "check-12345",
"amount": {
"total": 0.00,
"currency": "USD"
},
"eventDate": "2026-10-08T14:02:44.508Z",
"oldStatus": "CHALLENGE",
"newStatus": "AUTHORIZED"
}
Status values: AUTHORIZED (card verified), CHALLENGE, DECLINED, VOIDED.
Delivery & Reliability
- Authentication: when Basic auth is configured, every delivery carries
Authorization: Basic <credentials>. Validate it on every request. Deliveries are not signed. - Retries: non-2xx responses and timeouts are retried up to the attempt limit configured for your account. Return a
2xximmediately and process asynchronously. - Duplicates: retries can deliver the same notification more than once. Key your handler on
(paymentId, newStatus)and treat repeats as no-ops. - Ordering: not guaranteed, and
eventDateis set when the notification is generated, not when the status changed. If sequence matters, compare the incomingoldStatuswith the state you have stored; if they don't match, ignore the delivery and reconcile with a poll. - Source of truth: a webhook is a notification, not the full payment. Confirm the status β and read card, billing or fee details β with the GET payment endpoint before fulfilling orders.
Example Handler (Node.js)
app.post('/webhooks/inyo', async (req, res) => {
// Acknowledge immediately
res.status(200).send('OK');
const notification = req.body;
const { externalPaymentId, paymentId, event, newStatus } = notification;
if (await alreadyProcessed(paymentId, newStatus)) return;
if (notification.status === 'DECLINED' && newStatus !== 'DECLINED') {
// A capture or refund was declined; the payment keeps its current status.
flagFailedOperation(externalPaymentId, notification.statusDescription);
return;
}
switch (newStatus) {
case 'AUTHORIZED':
markAsAuthorized(externalPaymentId);
break;
case 'CAPTURED':
// PULL: funds settled β fulfill the order. PUSH: recipient paid.
event === 'PUSH'
? markPayoutDelivered(externalPaymentId)
: fulfillOrder(externalPaymentId);
break;
case 'DECLINED':
notifyDecline(externalPaymentId, notification.statusDescription);
break;
case 'VOIDED':
cancelOrder(externalPaymentId);
break;
case 'REFUNDED':
processRefund(externalPaymentId, notification.amount.total);
break;
}
await markProcessed(paymentId, newStatus);
});
