Inyo

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:

  1. Provide your webhook URL(s)
  2. 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:

FieldTypeNotes
agentIdstringYour agent identifier
eventstringPULL, PUSH or CHECK
statusstringResult 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
statusCodestring00 when approved; 99 or a decline code such as PAY_022 when declined. See Status codes
statusDescriptionstringHuman-readable description of the change or decline reason
paymentIduuidInyo payment identifier
externalPaymentIdstringThe externalPaymentId you sent when creating the payment
amountobject{ "total": number, "currency": string } β€” source amount and currency (ISO 4217)
recipientAmountobjectPUSH only. { "total": number, "currency": string } β€” amount delivered to the recipient, present when the payout has currency conversion
eventDatestringWhen the notification was generated, ISO 8601 in UTC
oldStatusstring | nullPayment status before the change
newStatusstringPayment status after the change

Status codes

statusCodeMeaning
00Approved
PAY_xxxDeclined by the issuer or provider. The code is the same responseCode the API returns for the payment; see Response codes for its meaning
99Declined, 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 2xx immediately 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 eventDate is set when the notification is generated, not when the status changed. If sequence matters, compare the incoming oldStatus with 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);
});