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

# Convenciones de la API

> Comportamientos comunes a todos los endpoints: IDs, companyCode, paginación, fechas, tipos estrictos, additionalData y errores de validación.

Estas reglas aplican a todos los recursos de la API. Las páginas de cada recurso solo mencionan las excepciones.

## IDs en el path

Los endpoints que reciben un `{id}` en el path aceptan el **ID público con prefijo** o el **valor sin prefijo**:

| Recurso      | Prefijo | Ejemplo con prefijo                        | Ejemplo sin prefijo                       |
| ------------ | ------- | ------------------------------------------ | ----------------------------------------- |
| Contacto     | `con_`  | `con_cmruu1lx2000002l4ctm7b0et`            | `cmruu1lx2000002l4ctm7b0et`               |
| Deuda        | `debt_` | `debt_COLE-OCT-00042`                      | `COLE-OCT-00042` (el `externalRequestId`) |
| Liga de pago | `plk_`  | `plk_61a837f5-3c67-423a-ade7-94b78e0007cb` | `61a837f5-3c67-423a-ade7-94b78e0007cb`    |
| Suscripción  | `sub_`  | `sub_839d28e1-a175-40c7-a54b-55f6c17b7964` | `839d28e1-a175-40c7-a54b-55f6c17b7964`    |
| Producto     | `prd_`  | `prd_12985`                                | `12985`                                   |

Un ID que no corresponde a ningún recurso devuelve `404 RESOURCE_NOT_FOUND`. En productos, un prefijo distinto de `prd_` o un valor no numérico también devuelve `404`.

<Tip>
  Guarda y usa siempre el ID con prefijo que te devuelve la API: es el que aparece en todas las respuestas.
</Tip>

## `companyCode`

Si tu cliente tiene **una sola company**, no necesitas enviar `companyCode`: se resuelve desde tu token.

Si tiene **varias companies**, indica en cuál operas con `companyCode`, como query param (`?companyCode=MX-S-04937`) o en el cuerpo de un `POST` o `PATCH`:

* Si no lo envías, la API responde `400 BAD_REQUEST` pidiéndolo.
* Si envías un código que no es una company de tu cliente, la API responde `422 RESOLUTION_ERROR`.
* En los `PATCH` (contactos, productos, ligas de pago, suscripciones y deudas), `companyCode` en el cuerpo se toma como contexto: no se considera un campo a modificar ni se rechaza como campo no editable.

## JSON malformado

Un cuerpo que no es JSON válido devuelve `400 INVALID_REQUEST`:

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

## Paginación

Los listados reciben `page` y `limit` como query params y devuelven los resultados en `data` junto con un objeto `meta`:

| Parámetro | Default | Reglas                                                       |
| --------- | ------- | ------------------------------------------------------------ |
| `page`    | `1`     | Entero mayor o igual a 1.                                    |
| `limit`   | `50`    | Entero entre 1 y 500. Un valor mayor a 500 se recorta a 500. |

Los valores inválidos de `page` o `limit` **no** devuelven error: toman el default. `meta.limit` refleja el valor que se aplicó.

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

<Note>
  Solo `GET /payment-links` agrega `meta.capped`: vale `true` cuando `meta.total` es aproximado porque se alcanzó el máximo de ítems que el servicio recorre. Los demás listados no traen ese campo.
</Note>

## Fechas

* Los campos `date-time` de las respuestas (`createdAt`, `updatedAt`, `expiresAt`, etc.) vienen siempre en **UTC con milisegundos y `Z`**: `2026-09-24T17:38:21.107Z`.
* Los campos de fecha (`dueDate`, `startDate`, `endDate`) usan `YYYY-MM-DD` y deben ser fechas de calendario válidas (`2026-02-30` devuelve `400`).
* Los date-time que envías (por ejemplo `expiresAt` de una liga) deben ser RFC 3339, con `Z` u offset (`2026-12-31T23:59:59-06:00`). Se convierten y guardan en UTC, y se devuelven en UTC aunque los hayas enviado con offset.
* Los filtros `createdAtFrom` y `createdAtTo` de los listados aceptan fecha (`YYYY-MM-DD`) o date-time RFC 3339. Una fecha sola cubre el día UTC completo, incluido en los dos extremos. El detalle de cada listado está en [Deudas](/es/recursos/deudas) y [Ligas de pago](/es/recursos/ligas-de-pago).

## Tipos estrictos

La API no convierte tipos: cada campo debe llegar con el tipo que declara el contrato.

* **Booleanos**: solo aceptan `true` o `false` literal. `"true"`, `1`, `"yes"`, etc. devuelven `400 INVALID_REQUEST` con una entrada en `details[]` por cada campo. Aplica a `allowOverduePayment` y `allowPartialPayments` en `POST /v2/debts`, a `autopay`, `allowOverduePayment`, `allowPartialPayments` y `enforcePaymentOrder` en `POST /subscriptions`, y a los booleanos editables de `PATCH /subscriptions/{id}`. El query param `active` de `GET /products` también acepta solo `true` o `false`.
* **`description`**: debe ser un string. Otro tipo devuelve `400`.
* **Montos**: números en pesos con hasta 2 decimales.

```json 400 Booleano no literal 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` guarda datos propios de tu sistema junto al cobro. Se acepta en `POST /v2/debts`, `POST /payment-links` y `POST`/`PATCH /subscriptions`, y se persiste en la deuda (en una liga, en la deuda asociada a la liga; en una suscripción, en las deudas que genera).

* Debe ser un **objeto plano**. Un escalar (string, número o booleano) devuelve `400 INVALID_REQUEST`.
* No puede usar **claves reservadas**, que TapiPay escribe internamente: `description`, `paymentMethods`, `productName`, `externalProductId`, `debtExpirationDate`, `overduePayment`, `allowPartialPayments`, `recurringDebt` y `debtReference`. Si aparece alguna, la API responde `400 INVALID_REQUEST` con `details[].field = "additionalData"`.

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

## Campos no declarados

* En `POST /v2/debts` y en todos los `PATCH`, un campo que no está en el contrato devuelve `400 INVALID_REQUEST`. En `POST /v2/debts` el campo viene en `details[].field`.
* En `POST /contacts`, `POST /products`, `POST /payment-links` y `POST /subscriptions`, los campos no declarados se ignoran.

## Errores de validación

Los errores de validación devuelven `400 INVALID_REQUEST` con un `details[]` que indica cada campo que falló y por qué. Usa `details[].field` para mostrar a tu usuario qué corregir, y no dependas del texto de `message` ni de `issue`: son informativos y pueden cambiar.

En el modo lote de `POST /v2/debts`, cada entrada de `details[]` trae también `index` (posición del item en `debts`) y `externalRequestId`. Ver [Manejo de errores](/es/conceptos/errores).
