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

# API conventions

> Behaviors common to every endpoint: IDs, companyCode, pagination, dates, strict types, additionalData, and validation errors.

These rules apply to every resource of the API. Each resource page only mentions the exceptions.

## IDs in the path

Endpoints that receive an `{id}` in the path accept the **public ID with its prefix** or the **value without the prefix**:

| Resource     | Prefix  | Example with prefix                        | Example without prefix                     |
| ------------ | ------- | ------------------------------------------ | ------------------------------------------ |
| Contact      | `con_`  | `con_cmruu1lx2000002l4ctm7b0et`            | `cmruu1lx2000002l4ctm7b0et`                |
| Debt         | `debt_` | `debt_COLE-OCT-00042`                      | `COLE-OCT-00042` (the `externalRequestId`) |
| Payment link | `plk_`  | `plk_61a837f5-3c67-423a-ade7-94b78e0007cb` | `61a837f5-3c67-423a-ade7-94b78e0007cb`     |
| Subscription | `sub_`  | `sub_839d28e1-a175-40c7-a54b-55f6c17b7964` | `839d28e1-a175-40c7-a54b-55f6c17b7964`     |
| Product      | `prd_`  | `prd_12985`                                | `12985`                                    |

An ID that does not match any resource returns `404 RESOURCE_NOT_FOUND`. For products, a prefix other than `prd_` or a non-numeric value also returns `404`.

<Tip>
  Always store and use the prefixed ID the API returns: it is the one that appears in every response.
</Tip>

## `companyCode`

If your client has **a single company**, you do not need to send `companyCode`: it is resolved from your token.

If it has **several companies**, indicate which one you operate on with `companyCode`, as a query param (`?companyCode=MX-S-04937`) or in the body of a `POST` or `PATCH`:

* If you do not send it, the API responds `400 BAD_REQUEST` asking for it.
* If you send a code that is not a company of your client, the API responds `422 RESOLUTION_ERROR`.
* In the `PATCH` endpoints (contacts, products, payment links, subscriptions, and debts), `companyCode` in the body is taken as context: it is not treated as a field to modify, nor rejected as a non-editable field.

## Malformed JSON

A body that is not valid JSON returns `400 INVALID_REQUEST`:

```json 400 Malformed JSON theme={null}
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Malformed JSON in request body",
    "details": [
      { "field": "body", "issue": "Invalid JSON syntax" }
    ]
  },
  "requestId": "req_3f8a1c9e2b"
}
```

## Pagination

List endpoints receive `page` and `limit` as query params and return the results in `data` together with a `meta` object:

| Parameter | Default | Rules                                                          |
| --------- | ------- | -------------------------------------------------------------- |
| `page`    | `1`     | Integer greater than or equal to 1.                            |
| `limit`   | `50`    | Integer between 1 and 500. A value above 500 is capped at 500. |

Invalid `page` or `limit` values do **not** return an error: they fall back to the default. `meta.limit` reflects the value that was applied.

```json theme={null}
{
  "data": [],
  "meta": {
    "page": 1,
    "limit": 50,
    "total": 0,
    "hasMore": false
  },
  "requestId": "req_a1b2c3d4e5f6"
}
```

<Note>
  Only `GET /payment-links` adds `meta.capped`: it is `true` when `meta.total` is approximate because the maximum number of items the service scans was reached. The other list endpoints do not carry that field.
</Note>

## Dates

* `date-time` fields in responses (`createdAt`, `updatedAt`, `expiresAt`, etc.) always come in **UTC with milliseconds and `Z`**: `2026-09-24T17:38:21.107Z`.
* Date fields (`dueDate`, `startDate`, `endDate`) use `YYYY-MM-DD` and must be valid calendar dates (`2026-02-30` returns `400`).
* The date-times you send (for example the `expiresAt` of a payment link) must be RFC 3339, with `Z` or an offset (`2026-12-31T23:59:59-06:00`). They are converted and stored in UTC, and returned in UTC even if you sent them with an offset.
* The `createdAtFrom` and `createdAtTo` filters of list endpoints accept a date (`YYYY-MM-DD`) or an RFC 3339 date-time. A date alone covers the full UTC day, included at both ends. The details of each list are in [Debts](/en/resources/debts) and [Payment links](/en/resources/payment-links).

## Strict types

The API does not coerce types: each field must arrive with the type the contract declares.

* **Booleans**: only accept the literal `true` or `false`. `"true"`, `1`, `"yes"`, etc. return `400 INVALID_REQUEST` with one entry in `details[]` per field. This applies to `allowOverduePayment` and `allowPartialPayments` in `POST /v2/debts`, to `autopay`, `allowOverduePayment`, `allowPartialPayments`, and `enforcePaymentOrder` in `POST /subscriptions`, and to the editable booleans of `PATCH /subscriptions/{id}`. The `active` query param of `GET /products` also accepts only `true` or `false`.
* **`description`**: must be a string. Any other type returns `400`.
* **Amounts**: numbers in pesos with up to 2 decimals.

```json 400 Non-literal boolean theme={null}
{
  "error": {
    "code": "INVALID_REQUEST",
    "message": "Validation failed: allowPartialPayments must be a boolean.",
    "details": [
      { "field": "allowPartialPayments", "issue": "allowPartialPayments must be a boolean." }
    ]
  },
  "requestId": "req_3f8a1c9e2b"
}
```

## `additionalData`

`additionalData` stores data from your own system alongside the charge. It is accepted in `POST /v2/debts`, `POST /payment-links`, and `POST`/`PATCH /subscriptions`, and it is persisted on the debt (for a payment link, on the debt associated with the link; for a subscription, on the debts it generates).

* It must be a **flat object**. A scalar (string, number, or boolean) returns `400 INVALID_REQUEST`.
* It cannot use **reserved keys**, which TapiPay writes internally: `description`, `paymentMethods`, `productName`, `externalProductId`, `debtExpirationDate`, `overduePayment`, `allowPartialPayments`, `recurringDebt`, and `debtReference`. If any of them appears, the API responds `400 INVALID_REQUEST` with `details[].field = "additionalData"`.

```json Valid theme={null}
{
  "additionalData": {
    "source": "crm",
    "folio": "F-2026-0042"
  }
}
```

## Undeclared fields

* In `POST /v2/debts` and in every `PATCH`, a field that is not in the contract returns `400 INVALID_REQUEST`. In `POST /v2/debts` the field comes in `details[].field`.
* In `POST /contacts`, `POST /products`, `POST /payment-links`, and `POST /subscriptions`, undeclared fields are ignored.

## Validation errors

Validation errors return `400 INVALID_REQUEST` with a `details[]` that indicates each field that failed and why. Use `details[].field` to show your user what to fix, and do not depend on the text of `message` or `issue`: they are informational and can change.

In the batch mode of `POST /v2/debts`, each entry of `details[]` also carries `index` (the item's position in `debts`) and `externalRequestId`. See [Error handling](/en/concepts/errors).
