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

# Payment orders

> Collect a debt right away from the bank account the user already registered, in a single request.

A **payment order** collects a debt **right away** from the bank account the user registered with TapiPay. In a single request, `POST /v2/debts/orders` registers the debt and requests the charge to their account.

## When to use a payment order

> **Payment order**
>
> A one-off charge you want to trigger now, with the account the user already registered. For example, an extra charge or a payment the user approved in your app.

> **[Direct debit](/en/resources/direct-debit.md)**
>
> If the debts of a product should be collected automatically every time they are due.

> **[Debt](/en/resources/debts.md)**
>
> If the user pays whenever they want, through its `paymentUrl`, with the method they choose.

> [!NOTE]
>
> A payment order does **not** require the user to be subscribed to a product (`POST /adhesions`). It only requires them to have their bank account registered.

## Prerequisites

* Your company must have payment orders and direct debit enabled (configured with your Tapi account executive).
* The user must exist as a [contact](/en/resources/contacts.md). This endpoint does not create contacts inline.
* The user must have registered their bank account on the [enrollment page](/en/payment-portal/autopay-sdk.md) (not through the API). You can check it with [`GET /payment-methods`](/en/resources/direct-debit#query-a-users-payment-methods).

## Create a payment order

```bash theme={null}
curl --request POST \
  --url 'https://tapipay-facade.homo.tapila.cloud/v2/debts/orders' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'x-authorization-token: YOUR_TAPI_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "externalRequestId": "CARGO-2026-00042",
    "externalClientId": "CLI-00042",
    "amount": 350.00,
    "paymentMethodType": "BANK_ACCOUNT",
    "externalPaymentMethodId": "b7e4a0c2-9f13-4e6d-8a25-71c3d9e0b4f8",
    "description": "School supplies charge"
  }'
```

### Fields

**externalRequestId** `string` *required*

Your unique identifier for the order, up to 250 characters. It is the idempotency key: if you retry, use the same value. The `debtId` of the resulting debt is `debt_<externalRequestId>`. See [Retries](#retries-and-idempotency).

**externalClientId** `string` *required*

The user's identifier, the same one used when they registered their account. It must match an existing contact.

**amount** `number` *required*

Amount to charge in **pesos**, with up to 2 decimal places (minimum `0.01`), same as in [debts](/en/resources/debts.md).

**paymentMethodType** `string` *required*

Which method is charged. For now the only value is `BANK_ACCOUNT` (the registered bank account).

**externalPaymentMethodId** `string`

The account to charge, as returned by [`GET /payment-methods`](/en/resources/direct-debit#query-a-users-payment-methods). It must be an active account of the user and of the type set in `paymentMethodType`. If you do not send it, the user's **default** account is charged.

**dueDate** `string`

Due date of the debt, in `YYYY-MM-DD` format. If you do not send it, today's date in Mexico City plus 7 days is used.

**description** `string`

Free text of up to 255 characters. It cannot be empty.

**additionalData** `object`

Plain object with your own data. It follows the same rules as in debts. See [API conventions](/en/concepts/api-conventions.md#additionaldata).

**companyCode** `string`

Only if your client has several companies. See [API conventions](/en/concepts/api-conventions.md#companycode).

> [!WARNING]
>
> The endpoint only accepts these fields. Any other one returns `400 INVALID_REQUEST`, with the rejected field in `details[].field`.

### The response

```json 201 response theme={null}
{
  "data": {
    "debt": {
      "debtId": "debt_CARGO-2026-00042",
      "externalRequestId": "CARGO-2026-00042",
      "status": "PENDING",
      "amount": 350.00,
      "amountPaid": 0,
      "currency": "MXN",
      "dueDate": "2026-10-29",
      "createdAt": "2026-10-22T15:20:00.000Z",
      "allowOverduePayment": true,
      "contact": {
        "contactId": "con_cmruu1lx2000002l4ctm7b0et",
        "externalClientId": "CLI-00042",
        "createdInline": false
      }
    },
    "order": {
      "orderId": "6f1c2b9e-4d3a-4b8e-9a71-2c5d8e0f1a23",
      "status": "PROCESSING",
      "amount": 350.00,
      "currency": "MXN",
      "paymentMethod": {
        "externalPaymentMethodId": "b7e4a0c2-9f13-4e6d-8a25-71c3d9e0b4f8",
        "paymentMethodType": "BANK_ACCOUNT"
      },
      "estimatedProcessingDate": null
    }
  },
  "requestId": "req_a1b2c3d4e5f6"
}
```

The response has two objects:

* `debt`: the debt that was registered. It is a debt like any other: you can read it with `GET /debts/{debtId}`. It does not allow partial payments.
* `order`: the charge to the user's account. `estimatedProcessingDate` is the estimated charge date (`YYYY-MM-DD`). For now it is always `null`.

## Order statuses

The charge is processed **asynchronously**: the order starts in `PROCESSING` and changes status when the bank responds.

| `status` | Meaning |
| - | - |
| `PROCESSING` | The charge was requested and is not confirmed yet. |
| `PAID` | The charge was collected. |
| `FAILED` | The charge could not be collected. |
| `CHARGED_BACK` | The charge was collected and then reversed (chargeback). |

The result of the charge reaches you through a webhook: see [Order payment](/en/webhooks/order-payment.md). You can also query the order with `GET /v2/debts/orders`.

## Query an order

```bash theme={null}
curl --request GET \
  --url 'https://tapipay-facade.homo.tapila.cloud/v2/debts/orders?externalRequestId=CARGO-2026-00042' \
  --header 'x-api-key: YOUR_API_KEY' \
  --header 'x-authorization-token: YOUR_TAPI_TOKEN'
```

```json 200 response theme={null}
{
  "data": {
    "orderId": "6f1c2b9e-4d3a-4b8e-9a71-2c5d8e0f1a23",
    "status": "PAID",
    "amount": 350.00,
    "currency": "MXN",
    "paymentMethod": {
      "externalPaymentMethodId": "b7e4a0c2-9f13-4e6d-8a25-71c3d9e0b4f8",
      "paymentMethodType": "BANK_ACCOUNT"
    },
    "estimatedProcessingDate": null
  },
  "requestId": "req_a1b2c3d4e5f6"
}
```

`externalRequestId` is required and is the same one you sent when creating the order. `data` has the same shape as `order` in the creation response. If there is no order with that `externalRequestId`, the response is `404 RESOURCE_NOT_FOUND`.

## Retries and idempotency

`externalRequestId` is the idempotency key of the order. This is how a retry with the same value behaves:

| Situation | Response |
| - | - |
| The previous attempt did not get to register the debt | It is processed as a new request. |
| The debt was registered, is still `PENDING`, and the retry has the same `externalClientId`, `amount`, and `externalPaymentMethodId` | That debt is reused and the charge is requested. |
| The key was already used with other data (including a different `externalPaymentMethodId`, or sending it in one attempt and not in the other), or already has an order | `409 RESOURCE_CONFLICT`. Query the order with `GET /v2/debts/orders` to know its status. |

> [!WARNING]
>
> On a `500 INTERNAL_ERROR` from `POST /v2/debts/orders`, you do **not** know whether the charge was requested. Always retry with the **same** `externalRequestId`, never with a new one: this way you avoid charging the user twice. If the retry responds `409`, query the order.

## Errors

| Cause | Status and `error.code` |
| - | - |
| Field outside the contract, missing required field or invalid field, `paymentMethodType` other than `BANK_ACCOUNT` | `400 INVALID_REQUEST` |
| The debt data does not meet a business rule. Nothing was created | `400 INVALID_REQUEST` |
| `GET` without `externalRequestId` | `400 INVALID_REQUEST` |
| Your client has several companies and you did not send `companyCode` | `400 BAD_REQUEST` |
| `companyCode` that does not belong to your client | `403 RESOLUTION_ERROR` |
| The `externalClientId` does not match a contact | `404 CONTACT_NOT_FOUND` |
| `GET` of an order that does not exist | `404 RESOURCE_NOT_FOUND` |
| The key was already used with other data (including the `externalPaymentMethodId`) or already has an order | `409 RESOURCE_CONFLICT` |
| Your company does not have payment orders enabled | `422 COMPANY_WITHOUT_DEBT_ID_MODALITY` |
| Your company does not have direct debit enabled | `422 PAYMENT_METHODS_NOT_ENABLED` |
| You did not send `externalPaymentMethodId` and the user has no active default account | `422 NO_ACTIVE_PAYMENT_METHOD` |
| The `externalPaymentMethodId` does not exist, does not belong to the user, is not active or is not of the `paymentMethodType` type. Nothing was created | `422 PAYMENT_METHOD_NOT_AVAILABLE` |
| The charge could not be created. The debt was cancelled and nothing was charged | `422 ORDER_NOT_CREATED` |
| `POST` with an uncertain result: it is not known whether the charge was requested. Retry with the same `externalRequestId` | `500 INTERNAL_ERROR` |
| `GET` that could not be resolved right now. Retry the query | `500 INTERNAL_ERROR` |

> [!NOTE]
>
> In these two endpoints, a `companyCode` that is not yours responds `403` with the error envelope (`error.code: RESOLUTION_ERROR` and `requestId`), not `422` as in the rest of the API. See [Error handling](/en/concepts/errors.md).


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