# Banking API accounts

> Banking API account operations: validation, listing, balances and statements.

- kind: api-operation
- status: stable
- api_version: 1.0.0
- last_verified: 2026-10-22
- url: https://tilopay.com/en/developers/api-bancario/cuentas

The `Accounts` group covers the account as a resource: validate a destination account
before transferring, list accessible accounts, check balances and request statements.

```http
POST /api/public/v1/accounts/validate
GET  /api/public/v1/accounts
POST /api/public/v1/accounts/balances
GET  /api/public/v1/accounts/balance
POST /api/public/v1/accounts/statements
GET  /api/public/v1/accounts/statements/{request_id}
```

## Resource rules [#reglas]

**Scope.** The listing returns only the accounts the token authorizes; `tenant_id` and
`owner_id` are never sent as parameters.

**Validate first.** Validate the destination account before creating a payment: the
operation confirms the account exists and is eligible, and returns the holder data the
service exposes.

**Asynchronous statements.** `POST /api/public/v1/accounts/statements` accepts `Idempotency-Key` and always returns **HTTP 202 (`ACCEPTED`)** with `request_id` and `status: PENDING`; generation is asynchronous. Poll `GET /api/public/v1/accounts/statements/{request_id}`: it returns **HTTP 200** with `status` `PENDING`, `PROCESSING`, `DONE` or `ERROR`. `DONE` includes `download_url`, **valid for 48 hours**. See
[safe retries](/en/developers/api-bancario/convenciones#reintentos).

**Pagination.** `GET /api/public/v1/accounts` and `GET /api/public/v1/accounts/balances`
use `limit` and `offset`, and return `pagination` with `limit`, `offset` and `total`.

## Operations [#operaciones]

### Validate external account for a payment method [#post-accounts-validate]

```http
POST /api/public/v1/accounts/validate
```

Host: `https://api-baas-sandbox.tilopay.com` — requires `Authorization: Bearer <access_token>`.

Validates a counterparty account (IBAN or phone) via the SINPE provider for the given payment method.
Does not validate internal platform accounts.
When valid, `data.holder.id_type` uses the same identification catalog keys as
`identification.type` in `POST /payments` (e.g. `cedula`, `juridica`, `dimex`).

## SINPE_MOVIL field availability (expected, stable)

For `payment_method_code = SINPE_MOVIL`, the SINPE Móvil wallet
query does not return identification type or institution name.
A successful validation **may** therefore return:

- `data.holder.id_type` = `unknown`
- `data.financial_institution.entity_name` = `""` (empty string)

This is **expected, stable behavior**, not a mapping bug or a transient provider error.
PIN (account query in SINPE) does populate both fields.

Integrators must not report these values as defects, and must not assume that
`holder.id_type` or `financial_institution.entity_name` are always populated
when `is_valid` is true.

## SINPE rejection codes (`data.rejection`)

When `is_valid` is `false`, `data.rejection` explains why (see the `rejected`
example on the `200` response below). `rejection.code` is one of a fixed set of
normalized semantics (see the value table below). **Most rows in
the table below do NOT get a distinct `code`** — only a subset of `reason_code`
values has a dedicated semantic; every other `reason_code`, even one listed below
with a specific `message`, returns `code = SINPE_REJECTED` (e.g. `reason_code = 21`
"fondos insuficientes" still returns `SINPE_REJECTED`). This same object and
catalog is also used by the `payment.failed` webhook's `rejection` field.
A `reason_code` not listed here at all still produces a response — `message` falls
back to a generic text and `code` falls back to `SINPE_REJECTED`.

**Account / profile / limits**

| Code | Message |
|---|---|
| 21 | Cuenta Cliente con fondos insuficientes |
| 22 | Cuenta Cliente no admite créditos |
| 23 | Cuenta Cliente cerrada |
| 24 | Cuenta Cliente inactiva |
| 25 | Cuenta Cliente no admite débitos |
| 26 | Cuenta Cliente no es de fondos |
| 27 | Moneda de la Cuenta Cliente no corresponde |
| 28 | Cuenta cliente no existe |
| 29 | Cuenta Cliente no registrada en el SINPE |
| 30 | Cuenta Cliente no habilitada para el servicio |
| 31 | Cuenta Cliente bloqueada |
| 32 | Id cliente destino no coincide con registrado en la entidad |
| 33 | Nombre del cliente destino no coincide con el registrado en la entidad |
| 34 | Cuenta Cliente en proceso de cierre |
| 35 | Cuenta Cliente embargada |
| 36 | Cuenta Cliente con retención judicial |
| 37 | Cuenta de expediente simplificado no permite el monto indicado |
| 38 | Límite transaccional de la Cuenta Cliente excedido |
| 39 | Cuenta Cliente incorrecta |
| 40 | IBAN de la cuenta destino inválido |
| 41 | IBAN de la cuenta origen inválido |
| 42 | Tipo de cuenta no permite la transacción |
| 43 | Cuenta Cliente no pertenece a la entidad indicada |
| 44 | Producto de la cuenta no admite el servicio |
| 45 | Cuenta Cliente en estado de cancelación |
| 46 | Cuenta Cliente restringida por política de la entidad |
| 47 | Cuenta Cliente no permite pagos inmediatos |
| 48 | Cuenta Cliente no permite SINPE Móvil |
| 49 | Titular de la cuenta destino fallecido |
| 50 | Cuenta Cliente consolidada o migrada |
| 51 | Identificación del cliente origen no coincide |
| 52 | Identificación del cliente destino inválida |
| 53 | Cliente destino no autorizado para recibir el pago |
| 54 | Cliente origen no autorizado para enviar el pago |
| 55 | Perfil del cliente origen no permite la transacción |
| 56 | Monto inferior al mínimo permitido |
| 57 | Monto superior al máximo permitido |
| 58 | Cantidad de transacciones diarias excedida |
| 59 | Cantidad de transacciones mensuales excedida |
| 60 | Límite acumulado diario excedido |
| 61 | Límite acumulado mensual excedido |
| 62 | Comisión no pudo ser aplicada |
| 63 | Tipo de cambio no disponible |
| 64 | Transacción rechazada por control de lavado de dinero |
| 65 | Transacción rechazada por listas de control |
| 66 | Transacción en revisión de cumplimiento |
| 67 | Documento de respaldo requerido no presente |
| 68 | Firma o autenticación inválida |
| 69 | Token o segundo factor inválido |
| 70 | Sesión de usuario expirada |
| 71 | Usuario no autorizado para el canal |
| 72 | Dispositivo no registrado |
| 73 | Geolocalización no permitida |
| 74 | Operación no soportada en la moneda indicada |
| 75 | Operación no soportada para el tipo de cliente |
| 76 | Problemas de comunicación |
| 77 | Tiempo de espera agotado en la entidad origen |
| 78 | Tiempo de espera agotado en el SINPE |
| 79 | Error interno de la entidad origen |
| 80 | Error interno de la entidad destino |
| 81 | Entidad origen no disponible |
| 82 | Entidad destino no encontrada |
| 83 | Problemas en la respuesta del destino |
| 84 | Respuesta de la entidad origen incorrecta |
| 85 | Mensaje con formato electrónico inválido |
| 86 | Versión del estándar electrónico no soportada |
| 87 | Campo obligatorio no informado |
| 88 | Campo con valor fuera de catálogo |
| 89 | Checksum o integridad del mensaje inválida |
| 90 | Referencia SINPE duplicada |
| 91 | Moneda no corresponde |
| 92 | Transacción no autorizada por entidad destino |
| 93 | Transacción no autorizada por cliente destino |
| 94 | Transacción no autorizada por entidad origen |
| 95 | Transacción no autorizada por cliente origen |
| 96 | Reverso no permitido para el estado de la transacción |
| 97 | Reverso ya aplicado |
| 98 | Confirmación no permitida para el estado de la transacción |
| 99 | Liquidación no permitida para el estado de la transacción |
| 100 | Entidad Destino no disponible para procesar en tiempo real |
| 101 | Entidad origen no disponible para procesar en tiempo real |
| 102 | Servicio PIN no habilitado para la entidad destino |
| 103 | Servicio PIN no habilitado para la entidad origen |
| 104 | Código de entidad destino inválido |
| 105 | Código de entidad origen inválido |
| 106 | Código de país de la entidad destino inválido |
| 107 | Código de país de la entidad origen inválido |
| 108 | Número de referencia interna inválido |
| 109 | Número de referencia SINPE inválido |
| 110 | Transacción no se encuentra en un estado que permita la consulta |

**Settlement with destination institution**

| Code | Message |
|---|---|
| 201 | Tiempo respuesta excedido por la entidad destino |
| 202 | Respuesta de la entidad destino incorrecta según el estándar electrónico |
| 203 | Se recibió una excepción de la entidad destino |
| 204 | Error de comunicación con la entidad destino |
| 205 | Falló procesamiento en el SINPE |
| 206 | Transacción no autorizada por cliente destino |
| 207 | Perfil transaccional del cliente destino no permite recibir el pago |
| 208 | Operación no autorizada entre las entidades participantes |
| 209 | Pendientes de Cumplimiento |
| 210 | Conciliación de la transacción no fue posible |

**Identification**

| Code | Message |
|---|---|
| 801 | Identificación inválida |
| 802 | Identificación del cliente origen no encontrada |
| 803 | Identificación del cliente destino no encontrada |
| 804 | Identificación no vigente |
| 805 | Identificación vencida |
| 806 | Identificación no corresponde al tipo indicado |
| 807 | Tipo de identificación inválido |
| 808 | País de la identificación no soportado |
| 809 | Identificación de menor de edad no permitida |
| 810 | Identificación de persona jurídica no permitida para el servicio |

**Format validation / Core Banking**

| Code | Message |
|---|---|
| 1002 | El Id de cliente destino no cumple con el formato esperado por el SINPE |
| 1003 | El Id de cliente origen no fue informado |
| 1004 | El Id de cliente origen no cumple con el formato esperado por el SINPE |
| 1005 | Monto con formato inválido |
| 1006 | Moneda con formato inválido |
| 1007 | Problemas de comunicación con el Core Bancario |
| 1008 | Core Bancario no disponible |
| 1009 | El valor para el campo no puede ser nulo o infringir su longitud mínima o máxima |
| 1010 | El valor para el campo no corresponde al tipo de dato esperado |
| 1011 | El valor para el campo no corresponde al catálogo permitido |
| 1012 | Fecha con formato inválido |
| 1013 | Hora con formato inválido |
| 1014 | Número de referencia con formato inválido |
| 1015 | IBAN con formato inválido |

**Channel / identification type**

| Code | Message |
|---|---|
| 1040 | Canal no informado |
| 1041 | Canal inválido |
| 1042 | Canal no corresponde |
| 1043 | Canal no habilitado para la entidad |
| 1044 | Canal no habilitado para el servicio |
| 1045 | El formato de la identificación es inválido |
| 1046 | Tipo de identificación no informado |
| 1080 | Tipo de identificación no corresponde al cliente origen |
| 1081 | Tipo de identificación no corresponde al cliente destino |
| 1082 | Tipo de identificación no vigente |
| 1083 | Tipo de identificación no soportado por el servicio |
| 1084 | Tipo de identificación inválido |
| 1085 | Tipo de identificación no soportado por la entidad |

**SINPE Móvil (wallet)**

| Code | Message |
|---|---|
| 15300 | El número de teléfono origen indicado es inválido |
| 15301 | El número de teléfono origen no tiene activo el Servicio Monedero |
| 15302 | El número de teléfono destino indicado es inválido |
| 15303 | El número de teléfono destino no está registrado en el padrón móvil del BCCR |
| 15304 | No es posible inactivar el monedero indicado pues no existe |
| 15305 | El número de teléfono indicado ya se encuentra activo como monedero en el padrón local |

**`rejection.code` values**

| Value | Meaning |
|---|---|
| `ACCOUNT_CLOSED` | Account closed |
| `ACCOUNT_NOT_FOUND` | Account not found |
| `ACCOUNT_BLOCKED` | Account blocked |
| `INVALID_ACCOUNT` | Invalid account |
| `IDENT_MISMATCH` | Identification does not match |
| `IDENT_TYPE_INVALID` | Invalid identification type |
| `INVALID_ID_FORMAT` | Invalid identification format |
| `VALIDATION_ERROR` | Validation error |
| `CURRENCY_MISMATCH` | Currency mismatch |
| `NOT_AUTHORIZED_BY_RECIPIENT` | Not authorized by the recipient |
| `PROFILE_NOT_ALLOWED` | Profile not allowed |
| `CHANNEL_NOT_RECOGNIZED` | Channel not recognized |
| `COMMUNICATION_ERROR` | Communication error |
| `RETRY_LATER` | Retry later |
| `WALLET_PHONE_ORIGIN_INVALID` | Origin phone invalid (SINPE Móvil) |
| `WALLET_PHONE_ORIGIN_NOT_ENABLED` | Origin phone without wallet service |
| `WALLET_PHONE_DEST_INVALID` | Destination phone invalid (SINPE Móvil) |
| `WALLET_PHONE_NOT_REGISTERED_BCCR` | Phone not registered in the BCCR mobile registry |
| `WALLET_NOT_FOUND` | Wallet not found |
| `WALLET_PHONE_ALREADY_ACTIVE` | Phone already has an active wallet |
| `PHONE_NOT_REGISTERED` | Phone not registered |
| `SINPE_REJECTED` | Rejected by SINPE (generic fallback) |

Rate limits currently apply to authentication and account validation. Exceeding them returns HTTP 429 (TOO_MANY_REQUESTS). Retry with exponential backoff.

SINPE code 1001 means the account exists and is active; it is a valid result, not a rejection reason.

**Parameters**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Correlation-Id` | header | string | no | Optional client-supplied correlation id for end-to-end tracing. Echoed back as `correlation_id` in the response envelope. If omitted, the API generates one and still returns it. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `payment_method_code` | string | yes | Payment method used to route validation (PIN for external IBAN, SINPE_MOVIL for phone wallet). Values: PIN, SINPE_MOVIL |
| `account` | object | yes | — |
| `account.type` | string | yes | Values: IBAN, PHONE |
| `account.value` | string | yes | — |

**Request example** — pin

```json
{
  "payment_method_code": "PIN",
  "account": {
    "type": "IBAN",
    "value": "CR16010400000000000031"
  }
}
```

**Request example** — sinpe_movil

```json
{
  "payment_method_code": "SINPE_MOVIL",
  "account": {
    "type": "PHONE",
    "value": "88887777"
  }
}
```

**200** — The request was processed successfully.

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `is_valid` | boolean | no | — |
| `account` | object | no | — |
| `account.type` | string | no | — |
| `account.value` | string | no | — |
| `account.currency` | string | no | — |
| `account.product_name` | string | no | — |
| `holder` | object | no | Account holder when validation succeeds. `id_type` matches `PaymentIdentification.type` (same catalog as POST /payments). For SINPE_MOVIL, `id_type` may be `unknown` because the wallet operation does not return an identification type. That is expected and stable; do not treat it as a bug. Can be null. |
| `holder.name` | string | no | — |
| `holder.id_type` | string | no | Identification catalog key (lowercase), e.g. `cedula`, `juridica`, `dimex`. Same vocabulary as `identification.type` on payment create. PIN: mapped from the provider account query (confirmed by the provider). SINPE_MOVIL: the wallet query does not return an identification type, so this field **may be `unknown`**. That is expected, stable behavior due to available provider data — not a mapping bug or a transient error. Do not assume a confirmed type is always present. Values: cedula, dimex, gobierno, juridica, institucion_autonoma, didi, pasaporte, unknown |
| `holder.id_number` | string | no | — |
| `financial_institution` | object | no | Counterparty institution when validation succeeds. PIN fills `entity_name` from the SINPE account query. SINPE_MOVIL only receives an entity **code** from the wallet query; it does not return the institution name, so `entity_name` **may be `""`**. That is expected, stable behavior — not a transient error. Do not assume `entity_name` is always populated. Can be null. |
| `financial_institution.country_code` | string | no | — |
| `financial_institution.entity_code` | string | no | — |
| `financial_institution.entity_name` | string | no | Human-readable institution name. PIN: populated from the provider account-info operation. SINPE_MOVIL: may be `""` because the wallet query does not expose the name. Expected limitation, not an error. |
| `rejection` | object | no | Present when `is_valid` is `false` and the provider rejected the account. `null` when `is_valid` is `true`. Can be null. |
| `rejection.code` | string | yes | Normalized platform semantic derived from `reason_code`. Only a subset of `reason_code` values (see the catalog in the description of `POST /accounts/validate` and of the `payment.failed` webhook contract endpoint) has a distinct entry here — for any other numeric `reason_code`, `code` is `SINPE_REJECTED` even when `message` is specific (e.g. `reason_code = 21` "fondos insuficientes" still returns `code = SINPE_REJECTED`, it has no dedicated semantic). `PHONE_NOT_REGISTERED` is the fallback when `reason_code` is non-numeric (SINPE_MOVIL wallet flow). Values: ACCOUNT_CLOSED, ACCOUNT_NOT_FOUND, ACCOUNT_BLOCKED, INVALID_ACCOUNT, IDENT_MISMATCH, IDENT_TYPE_INVALID, INVALID_ID_FORMAT, VALIDATION_ERROR, CURRENCY_MISMATCH, NOT_AUTHORIZED_BY_RECIPIENT, PROFILE_NOT_ALLOWED, CHANNEL_NOT_RECOGNIZED, COMMUNICATION_ERROR, RETRY_LATER, WALLET_PHONE_ORIGIN_INVALID, WALLET_PHONE_ORIGIN_NOT_ENABLED, WALLET_PHONE_DEST_INVALID, WALLET_PHONE_NOT_REGISTERED_BCCR, WALLET_NOT_FOUND, WALLET_PHONE_ALREADY_ACTIVE, PHONE_NOT_REGISTERED, SINPE_REJECTED |
| `rejection.reason_code` | string | yes | Raw Motivo/code returned by SINPE. See the endpoint description of `POST /accounts/validate` for the full code catalog (shared by this object wherever it appears, including the `payment.failed` webhook). A code not listed there still produces a response — `message` falls back to a generic text and `code` falls back to `SINPE_REJECTED`. |
| `rejection.message` | string | yes | Descriptive Motivo text; prefers provider `Detalle` when present. |
| `provider` | object | no | — |
| `provider.operation` | string | no | — |
| `provider.correlation_id` | string | no | Can be null. |
| `provider.occurred_at` | string (date-time) | no | Can be null. |

**Response example** — valid

```json
{
  "success": true,
  "http_status_code": 200,
  "response_code": "OK",
  "message": "The request was processed successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "is_valid": true,
    "account": {
      "type": "IBAN",
      "value": "CR16010400000000000031",
      "currency": "CRC",
      "product_name": "Cuenta corriente"
    },
    "holder": {
      "name": "Juan Perez",
      "id_type": "cedula"
    },
    "financial_institution": {
      "entity_code": "104",
      "entity_name": "Banco Ejemplo"
    },
    "provider": {
      "reason_code": "0",
      "message": "Cuenta válida",
      "code": null
    },
    "rejection": null
  }
}
```

**Response example** — rejected

```json
{
  "success": true,
  "http_status_code": 200,
  "response_code": "OK",
  "message": "The request was processed successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "is_valid": false,
    "account": {
      "type": "IBAN",
      "value": "CR16010400000000000099",
      "currency": "CRC",
      "product_name": ""
    },
    "holder": null,
    "financial_institution": {
      "entity_code": "104",
      "entity_name": "Banco Ejemplo"
    },
    "provider": {
      "reason_code": "28",
      "message": "Cuenta cliente no existe",
      "code": "ACCOUNT_NOT_FOUND"
    },
    "rejection": {
      "code": "ACCOUNT_NOT_FOUND",
      "reason_code": "28",
      "message": "Cuenta cliente no existe"
    }
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 400 | `INVALID_REQUEST` | Invalid request. Check the required fields and try again. |
| 400 | `INVALID_ACCOUNT_TYPE` | Unsupported or invalid account type. |
| 400 | `INVALID_ACCOUNT_FORMAT` | Invalid account format. |
| 400 | `INVALID_PAYMENT_METHOD` | Invalid or unsupported payment method. |
| 401 | `UNAUTHORIZED` | Unauthorized. Verify your session or credentials. |
| 403 | `FORBIDDEN` | You do not have permission to perform this action. |
| 403 | `ACCOUNT_ACCESS_DENIED` | The actor does not have access to the requested account. |
| 422 | `UNPROCESSABLE_ENTITY` | The request is well-formed but cannot be processed due to business rules. |
| 429 | `TOO_MANY_REQUESTS` | Too many requests. Please retry after a short delay. |
| 500 | `INTERNAL_ERROR` | An unexpected error occurred. Please try again later. |
| 502 | `PROVIDER_ERROR` | Unable to complete the request with the payment provider. Please try again later. |

### List balances for accessible accounts [#get-accounts-balances]

```http
GET /api/public/v1/accounts/balances
```

Host: `https://api-baas-sandbox.tilopay.com` — requires `Authorization: Bearer <access_token>`.

Returns paginated account balances (`account`, `amounts`, `as_of`). This endpoint checks
the current balance in real time and may respond more slowly than `GET /accounts`.
For account attributes without amounts, use `GET /accounts`.

Optional filters use the public account identification standard: `type` + `value`
(identifier scheme + value). `type` is not the internal account classification.

**Parameters**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Correlation-Id` | header | string | no | Optional client-supplied correlation id for end-to-end tracing. Echoed back as `correlation_id` in the response envelope. If omitted, the API generates one and still returns it. |
| `limit` | query | integer | no | — |
| `offset` | query | integer | no | — |
| `type` | query | string | no | Optional filter by account **identifier scheme** (e.g. `IBAN`). Not the internal account classification. |
| `value` | query | string | no | Optional filter by account identifier value (partial match supported for list endpoints). |

**200** — The request was processed successfully.

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | array<object> | no | — |
| `items[].account` | object | yes | — |
| `items[].account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `items[].account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `items[].account.currency` | string | no | Optional account currency in response payloads. |
| `items[].amounts` | object | yes | Ledger balances as decimal strings with 2 fraction digits (same display scale as public payment amounts). Prefer string over JSON number for financial amounts. |
| `items[].amounts.available` | string | no | — |
| `items[].amounts.posted` | string | no | — |
| `items[].amounts.inbound_in_transit` | string | no | — |
| `items[].amounts.outbound_in_transit` | string | no | — |
| `items[].as_of` | string (date-time) | yes | Ledger timestamp when known (RFC 3339 UTC, second precision, `Z`); `null` when not available. Can be null. |
| `pagination` | object | no | — |
| `pagination.limit` | integer | no | — |
| `pagination.offset` | integer | no | — |
| `pagination.total` | integer | no | — |

**Response example**

```json
{
  "success": true,
  "http_status_code": 200,
  "response_code": "OK",
  "message": "The request was processed successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "items": [
      {
        "account": {
          "type": "IBAN",
          "value": "CR26036900101010588061",
          "currency": "CRC"
        },
        "amounts": {
          "available": "991900.00",
          "posted": "991900.00",
          "inbound_in_transit": "0.00",
          "outbound_in_transit": "0.00"
        },
        "as_of": "2026-01-15T12:00:00Z"
      }
    ],
    "pagination": {
      "limit": 20,
      "offset": 0,
      "total": 1
    }
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 400 | `INVALID_REQUEST` | Invalid request. Check the required fields and try again. |
| 401 | `UNAUTHORIZED` | Unauthorized. Verify your session or credentials. |
| 403 | `FORBIDDEN` | You do not have permission to perform this action. |
| 403 | `ACCOUNT_ACCESS_DENIED` | The actor does not have access to the requested account. |
| 404 | `NOT_FOUND` | The requested resource was not found. |
| 500 | `INTERNAL_ERROR` | An unexpected error occurred. Please try again later. |
| 502 | — | Dependency error |
| 503 | `SERVICE_UNAVAILABLE` | A required service is temporarily unavailable. Please try again later. |

### List accessible accounts without balances [#get-accounts]

```http
GET /api/public/v1/accounts
```

Host: `https://api-baas-sandbox.tilopay.com` — requires `Authorization: Bearer <access_token>`.

Returns paginated accessible accounts without querying current balances.
Each item is account attributes only (`account.type`, `account.value`, `account.currency`).
`amounts` and `as_of` are not included; use `GET /accounts/balances` or `POST /accounts/balance`.

Optional filters use the public account identification standard: `type` + `value`
(identifier scheme + value). `type` is not the internal account classification.

**Parameters**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Correlation-Id` | header | string | no | Optional client-supplied correlation id for end-to-end tracing. Echoed back as `correlation_id` in the response envelope. If omitted, the API generates one and still returns it. |
| `limit` | query | integer | no | — |
| `offset` | query | integer | no | — |
| `type` | query | string | no | Optional filter by account **identifier scheme** (e.g. `IBAN`). Not the internal account classification. |
| `value` | query | string | no | Optional filter by account identifier value (partial match supported for list endpoints). |

**200** — The request was processed successfully.

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `items` | array<object> | no | — |
| `items[].account` | object | yes | — |
| `items[].account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `items[].account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `items[].account.currency` | string | no | Optional account currency in response payloads. |
| `pagination` | object | no | — |
| `pagination.limit` | integer | no | — |
| `pagination.offset` | integer | no | — |
| `pagination.total` | integer | no | — |

**Response example**

```json
{
  "success": true,
  "http_status_code": 200,
  "response_code": "OK",
  "message": "The request was processed successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "items": [
      {
        "account": {
          "type": "IBAN",
          "value": "CR26036900101010588061",
          "currency": "CRC"
        }
      }
    ],
    "pagination": {
      "limit": 20,
      "offset": 0,
      "total": 1
    }
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 400 | `INVALID_REQUEST` | Invalid request. Check the required fields and try again. |
| 401 | `UNAUTHORIZED` | Unauthorized. Verify your session or credentials. |
| 403 | `FORBIDDEN` | You do not have permission to perform this action. |
| 404 | `NOT_FOUND` | The requested resource was not found. |
| 500 | `INTERNAL_ERROR` | An unexpected error occurred. Please try again later. |
| 502 | — | Dependency error |
| 503 | `SERVICE_UNAVAILABLE` | A required service is temporarily unavailable. Please try again later. |

### Get account balance [#post-accounts-balance]

```http
POST /api/public/v1/accounts/balance
```

Host: `https://api-baas-sandbox.tilopay.com` — requires `Authorization: Bearer <access_token>`.

Returns a single account snapshot with `amounts` and `as_of`.
For account attributes without balances, use `GET /accounts`.

**Parameters**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Correlation-Id` | header | string | no | Optional client-supplied correlation id for end-to-end tracing. Echoed back as `correlation_id` in the response envelope. If omitted, the API generates one and still returns it. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | object | yes | — |
| `account.type` | string | yes | Account **identifier scheme**. Only `IBAN` is supported here. This is not the internal account classification (e.g. operational). Values: IBAN |
| `account.value` | string | yes | IBAN (spaces allowed; normalized server-side) |

**200** — The request was processed successfully.

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | object | yes | — |
| `account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `account.currency` | string | no | Optional account currency in response payloads. |
| `amounts` | object | yes | Ledger balances as decimal strings with 2 fraction digits (same display scale as public payment amounts). Prefer string over JSON number for financial amounts. |
| `amounts.available` | string | no | — |
| `amounts.posted` | string | no | — |
| `amounts.inbound_in_transit` | string | no | — |
| `amounts.outbound_in_transit` | string | no | — |
| `as_of` | string (date-time) | yes | Ledger timestamp when known (RFC 3339 UTC, second precision, `Z`); `null` when not available. Can be null. |

**Response example**

```json
{
  "success": true,
  "http_status_code": 200,
  "response_code": "OK",
  "message": "The request was processed successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "account": {
      "type": "IBAN",
      "value": "CR26036900101010588061",
      "currency": "CRC"
    },
    "amounts": {
      "available": "991900.00",
      "posted": "991900.00",
      "inbound_in_transit": "0.00",
      "outbound_in_transit": "0.00"
    },
    "as_of": "2026-01-15T12:00:00Z"
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 400 | `INVALID_REQUEST` | Invalid request. Check the required fields and try again. |
| 401 | `UNAUTHORIZED` | Unauthorized. Verify your session or credentials. |
| 403 | `FORBIDDEN` | You do not have permission to perform this action. |
| 403 | `ACCOUNT_ACCESS_DENIED` | The actor does not have access to the requested account. |
| 404 | `ACCOUNT_NOT_FOUND` | Account not found. |
| 422 | `ACCOUNT_NOT_CONFIGURED` | This account is not mapped in the system. Please contact support to configure it correctly. |
| 422 | `UNPROCESSABLE_ENTITY` | The request is well-formed but cannot be processed due to business rules. |
| 500 | `INTERNAL_ERROR` | An unexpected error occurred. Please try again later. |
| 502 | `PROVIDER_ERROR` | Unable to complete the request with the payment provider. Please try again later. |
| 503 | `SERVICE_UNAVAILABLE` | A required service is temporarily unavailable. Please try again later. |

### Request account statement generation [#post-accounts-statements]

```http
POST /api/public/v1/accounts/statements
```

Host: `https://api-baas-sandbox.tilopay.com` — requires `Authorization: Bearer <access_token>`.

Creates an asynchronous account statement request for a date range (max 60 days).
Identify the account with `account.type` + `account.value` (identifier scheme + value,
e.g. IBAN). This follows the public account identification standard.
`account.type` is not the internal account classification.
Returns `{request_id, status: PENDING}` immediately.
Poll `GET /accounts/statements/{request_id}` for status; when `DONE`, the response includes a signed `download_url`.

Always returns HTTP 202 (ACCEPTED) with request_id and status PENDING. Generation is asynchronous. Poll GET /api/public/v1/accounts/statements/{request_id}: HTTP 200 with PENDING, PROCESSING, DONE or ERROR. DONE includes download_url, valid for 48 hours.

**Parameters**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Correlation-Id` | header | string | no | Optional client-supplied correlation id for end-to-end tracing. Echoed back as `correlation_id` in the response envelope. If omitted, the API generates one and still returns it. |
| `Idempotency-Key` | header | string | no | — |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `account` | object | yes | — |
| `account.type` | string | yes | Account **identifier scheme**. Only `IBAN` is supported here. This is not the internal account classification (e.g. operational). Values: IBAN |
| `account.value` | string | yes | IBAN (spaces allowed; normalized server-side) |
| `date_from` | string (date-time) | yes | RFC 3339 timestamp accepted on requests. UTC `Z` or a numeric offset is allowed. Optional fractional seconds are accepted and truncated to seconds. |
| `date_to` | string (date-time) | yes | RFC 3339 (max 60 days after date_from) |
| `notify_email` | boolean | no | — |

**202** — Statement request accepted for asynchronous generation.

`response_code`: `ACCEPTED`

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `request_id` | string (uuid) | yes | — |
| `status` | string | yes | Values: PENDING |

**Response example**

```json
{
  "success": true,
  "http_status_code": 202,
  "response_code": "ACCEPTED",
  "message": "Statement request accepted.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "request_id": "5f3b2c1d-9e8a-4b7c-8d6e-5f4a3b2c1d0e",
    "status": "PENDING"
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 400 | `INVALID_REQUEST` | Invalid request. Check the required fields and try again. |
| 401 | `UNAUTHORIZED` | Unauthorized. Verify your session or credentials. |
| 403 | `FORBIDDEN` | You do not have permission to perform this action. |
| 403 | `ACCOUNT_ACCESS_DENIED` | The actor does not have access to the requested account. |
| 404 | `NOT_FOUND` | The requested resource was not found. |
| 409 | `IDEMPOTENCY_CONFLICT` | Idempotency conflict: the same Idempotency-Key was reused with a different request body, or a previous request is still in progress. |
| 500 | `INTERNAL_ERROR` | An unexpected error occurred. Please try again later. |
| 502 | — | Dependency error |
| 503 | `SERVICE_UNAVAILABLE` | A required service is temporarily unavailable. Please try again later. |

### Get account statement request status [#get-accounts-statements-request-id]

```http
GET /api/public/v1/accounts/statements/{request_id}
```

Host: `https://api-baas-sandbox.tilopay.com` — requires `Authorization: Bearer <access_token>`.

Returns request status. When `DONE`, includes a signed, time-limited `download_url` (48h from generation) and `expires_at`.
When `ERROR`, includes a generic `message` without internal details.
Unknown or unauthorized `request_id` returns 404.

Returns HTTP 200 with status PENDING, PROCESSING, DONE or ERROR. DONE includes download_url, valid for 48 hours.

**Parameters**

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `X-Correlation-Id` | header | string | no | Optional client-supplied correlation id for end-to-end tracing. Echoed back as `correlation_id` in the response envelope. If omitted, the API generates one and still returns it. |
| `request_id` | path | string (uuid) | yes | — |

**200** — The request was processed successfully.

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `request_id` | string (uuid) | yes | — |
| `status` | string | yes | Values: PENDING, PROCESSING, DONE, ERROR |
| `download_url` | string (uri) | no | Signed download URL, present when status is DONE; valid for 48 hours from generation. |
| `expires_at` | string (date-time) | no | Expiration of download_url (48h from generation) |
| `message` | string | no | Generic error message when status is ERROR |

**Response example**

```json
{
  "success": true,
  "http_status_code": 200,
  "response_code": "OK",
  "message": "The request was processed successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "request_id": "5f3b2c1d-9e8a-4b7c-8d6e-5f4a3b2c1d0e",
    "status": "DONE",
    "download_url": "https://storage.example.com/statements/5f3b2c1d.pdf?signature=example",
    "expires_at": "2026-01-17T12:00:00Z",
    "message": null
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 401 | `UNAUTHORIZED` | Unauthorized. Verify your session or credentials. |
| 403 | `FORBIDDEN` | You do not have permission to perform this action. |
| 404 | `NOT_FOUND` | The requested resource was not found. |
| 500 | `INTERNAL_ERROR` | An unexpected error occurred. Please try again later. |
| 503 | `SERVICE_UNAVAILABLE` | A required service is temporarily unavailable. Please try again later. |

## Validation and SINPE codes [#validacion-sinpe]

Code `1001` means the account exists and is active: it is not a rejection. `208` means “Operation not authorized between the participating institutions” and `209`, “Pending compliance”.

Account validation is rate-limited: exceeding the limit returns `429` (`TOO_MANY_REQUESTS`). Retry with exponential backoff.
