---
description: >-
  Webhook notifications for the Payment Gateway — PULL, PUSH and CHECK status
  changes, with payload references, the push payout lifecycle, Basic auth,
  retry behavior and handler best practices.
---

# 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:

| Field | Type | Notes |
| ----- | ---- | ----- |
| `agentId` | string | Your agent identifier |
| `event` | string | `PULL`, `PUSH` or `CHECK` |
| `status` | string | Payment status after the change. Same value as `newStatus` |
| `statusCode` | string | `00` approved, `99` declined |
| `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 |

### 1. `PULL`

Fires when a pull payment is authorized, captured, challenged, declined, voided or refunded.

```json
{
  "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`.

```mermaid
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.

### 2. `PUSH`

Fires when a push payout is accepted, paid or rejected. The create call answers `PENDING`, and the outcome arrives later by webhook.

```json
{
  "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`.

```mermaid
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](check-card-account.md) verification completes. No funds are moved.

```json
{
  "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](payment/pulling-funds/listing-payments/by-external-id.md) before fulfilling orders.

### Example Handler (Node.js)

```javascript
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;

  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);
});
```
