> ## Documentation Index
> Fetch the complete documentation index at: https://devs.tapipay.la/llms.txt
> Use this file to discover all available pages before exploring further.

# Order payment

> The result of a payment order's charge: settled, declined or reversed.

When you create a [payment order](/en/resources/payment-orders.md), the charge to the user's account is processed later, in the batch TapiPay sends to the bank. The result reaches you through this webhook: one per movement of the charge.

| `type` | `paymentStatus` | When it arrives | Order status |
| - | - | - | - |
| `ORDER_PAYMENT_CONFIRMED` | `CONFIRMED` | The bank confirmed the charge: the money came in. | `PAID` |
| `ORDER_PAYMENT_FAILED` | `FAILED` | The bank declined the charge or the charge failed. Nothing was collected. | `FAILED` |
| `ORDER_PAYMENT_CHARGEBACK` | `CHARGED_BACK` | A collected charge was reversed. It can arrive even if you did not receive the confirmation. | `CHARGED_BACK` |

> [!NOTE]
>
> This webhook has **its own contract**, different from the direct debit events: it does not carry `operationId` or `companyName`, and its idempotency key is `id`. What [Common fields](/en/webhooks.md#common-fields) explains does not apply here, except for `hash`.

## Two notices for a settled order

`ORDER_PAYMENT_CONFIRMED` reports the **charge**, not the payment of the debt. After confirming it, TapiPay applies the payment to the order's debt and also sends you the [payment notification](/en/webhooks/payment-notification.md) (`type: SERVICE`, `status: confirmed`). So a settled order generates two notices, which can reach different URLs.

## Configuration

The URL for these events is defined during onboarding and can be different from the one for the other webhooks. If you did not set one for orders, they reach your general URL. The security mechanisms are the same as for the rest of the webhooks: see [Validating the request comes from TapiPay](/en/webhooks/payment-notification.md#validating-the-request-comes-from-tapipay).

`POST` with `Content-Type: application/json`. The `notificationType` header carries the same value as `type`, so you can route without reading the body.

## The payload

| Field | Type | Present | Description |
| - | - | - | - |
| `type` | String | Always | `ORDER_PAYMENT_CONFIRMED`, `ORDER_PAYMENT_FAILED` or `ORDER_PAYMENT_CHARGEBACK`. Use it to route. |
| `hash` | String \| null | Always | Verification hash. `null` if you have no public key configured. |
| `id` | String (uuid) | Always | Unique identifier of the notification. If it arrives again, it carries the same `id`. **Use it as the idempotency key.** |
| `orderId` | String (uuid) | Always | The `orderId` returned when the order was created. |
| `externalOrderId` | String | Always | The `externalRequestId` you sent when creating the order. |
| `paymentStatus` | String | Always | `CONFIRMED`, `FAILED` or `CHARGED_BACK`. |
| `companyCode` | String | Always | Code of the order's company. |
| `externalClientId` | String | Always | Your identifier for the user. |
| `amount` | Number | Always | Order amount, in pesos with up to 2 decimals. |
| `currency` | String | Always | Currency, in ISO 4217. For now `MXN`. |
| `paymentProcessor` | String | Always | For now `BANK_ACCOUNT`. |
| `paymentMethod` | Object | Always | The account that was charged: `{ type, externalPaymentMethodId }`. It never carries the account number or the CLABE. |
| `paymentProviderError` | Object | Only `ORDER_PAYMENT_FAILED` | Reason for the decline: `{ code, description }`. |
| `chargeback` | Object | Only `ORDER_PAYMENT_CHARGEBACK` | Reason for the chargeback: `{ code, description }`. |
| `additionalData` | Object | Always | For now it always arrives empty (`{}`). |
| `paymentAt` | String | Always | When TapiPay recorded the result reported by the bank, in ISO 8601 UTC. It is not the exact time of the movement at the bank. |

> [!WARNING]
>
> **In `paymentMethod`, the account type goes in `type`.** In the order response and in [`GET /payment-methods`](/en/resources/direct-debit#query-a-users-payment-methods) the same value is called `paymentMethodType`. `externalPaymentMethodId` is the same value in all three places.

> [!NOTE]
>
> For now `code` and `description` always arrive as `null` inside `paymentProviderError` and `chargeback`. The object itself always comes in its event: read it without checking that it exists, but handle the `null`.

## The hash

If you provided an RSA public key during onboarding, `hash` comes encrypted with it. Decrypt it with your private key: it contains your integration user or, in version `v2` of the security, `{ "clientUsername", "operationId" }`. In `v2`, check that `operationId` matches the `id` in the body.

## Examples

An order that is charged and then reversed, and another whose charge is declined. The examples belong to an integration without a public key, which is why `hash` is `null`.

```json Settled theme={null}
{
  "type": "ORDER_PAYMENT_CONFIRMED",
  "hash": null,
  "id": "5c0f7a3d-2b8e-5d14-9a6c-8e1f3b7d2a90",
  "orderId": "6f1c2b9e-4d3a-4b8e-9a71-2c5d8e0f1a23",
  "externalOrderId": "CARGO-2026-00042",
  "paymentStatus": "CONFIRMED",
  "companyCode": "MX-S-12345",
  "externalClientId": "CLI-00042",
  "amount": 350.00,
  "currency": "MXN",
  "paymentProcessor": "BANK_ACCOUNT",
  "paymentMethod": { "type": "BANK_ACCOUNT", "externalPaymentMethodId": "b7e4a0c2-9f13-4e6d-8a25-71c3d9e0b4f8" },
  "additionalData": {},
  "paymentAt": "2026-10-23T15:02:17.000Z"
}
```

```json Declined theme={null}
{
  "type": "ORDER_PAYMENT_FAILED",
  "hash": null,
  "id": "e7a2c94b-1d3f-5b68-8c2e-4f9a6d1b3c75",
  "orderId": "4e7b1a90-2c6d-4f3e-8a15-6b9d0c2e7f48",
  "externalOrderId": "CARGO-2026-00043",
  "paymentStatus": "FAILED",
  "companyCode": "MX-S-12345",
  "externalClientId": "CLI-00043",
  "amount": 980.00,
  "currency": "MXN",
  "paymentProcessor": "BANK_ACCOUNT",
  "paymentMethod": { "type": "BANK_ACCOUNT", "externalPaymentMethodId": "7d2c9e14-5a3b-4f86-b1e0-9c4a8f6d2b53" },
  "paymentProviderError": { "code": null, "description": null },
  "additionalData": {},
  "paymentAt": "2026-10-23T15:05:42.000Z"
}
```

```json Chargeback theme={null}
{
  "type": "ORDER_PAYMENT_CHARGEBACK",
  "hash": null,
  "id": "a41d8e6f-3c2b-5e97-b0d4-7c5f2e8a1b36",
  "orderId": "6f1c2b9e-4d3a-4b8e-9a71-2c5d8e0f1a23",
  "externalOrderId": "CARGO-2026-00042",
  "paymentStatus": "CHARGED_BACK",
  "companyCode": "MX-S-12345",
  "externalClientId": "CLI-00042",
  "amount": 350.00,
  "currency": "MXN",
  "paymentProcessor": "BANK_ACCOUNT",
  "paymentMethod": { "type": "BANK_ACCOUNT", "externalPaymentMethodId": "b7e4a0c2-9f13-4e6d-8a25-71c3d9e0b4f8" },
  "chargeback": { "code": null, "description": null },
  "additionalData": {},
  "paymentAt": "2026-11-04T16:00:00.000Z"
}
```

## Recommendations

* **Process each notification only once.** TapiPay retries if your endpoint does not answer 2xx in time, so the same notification can arrive more than once, always with the same `id`. Store the processed `id` values in persistent storage.
* **Do not assume an arrival order.** Use `paymentAt` to order the movements of the same order.
* **Correlate by `orderId` or `externalOrderId`.**
* **Ignore fields you do not know**: new fields can be added.
* **If a webhook does not arrive**, query the order with [`GET /v2/debts/orders`](/en/resources/payment-orders#query-an-order).

The requirements for your endpoint (answer 2xx in under 5 seconds and be reachable from TapiPay's IPs) are the same as for the rest of the webhooks: see [Requirements for your endpoint](/en/webhooks/payment-notification.md#requirements-for-your-endpoint).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.