# Banking API payments

> Banking API payment operations: create, fetch, list and search PIN and SINPE Móvil payments.

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

The `Payments` group covers a payment's full cycle: create it, fetch it by identifier,
list it with filters and search it by any of its references.

```http
POST /api/public/v1/transactions/payments
GET  /api/public/v1/transactions/payments
GET  /api/public/v1/transactions/payments/{payment_id}
POST /api/public/v1/transactions/payments/search
```

## Resource rules [#reglas]

**Methods.** `payment_method_code` accepts `PIN` and `SINPE_MOVIL`. **Direction.**
At creation, `direction` only accepts `OUT` (outbound); `IN` (inbound) payments remain available in listings and webhooks. **Destination account.** it is
identified by `IBAN` or by `PHONE`, depending on the method. Validate it before
transferring with [account validation](/en/developers/api-bancario/cuentas#operaciones).

**Your own reference.** `client_reference` is your payment identifier and is unique per
tenant: two payments of the same customer cannot share it. Use it to look a payment up
without storing the `payment_id`.

**Safe retries.** Creation accepts `Idempotency-Key`; resending the same key with the same
body returns the original payment instead of creating another one. See
[safe retries](/en/developers/api-bancario/convenciones#reintentos).

## States [#estados]

A payment moves through five states: `pending`, `processing`, `confirmed`, `posted` and
`failed`.

A reversal is **not a state**: a reversed payment stays in `posted` and is recognized by
`result: reversed` and `has_reversal: true`, plus the
[`payment.reversed`](/en/developers/api-bancario/webhooks) event. Always check those two
fields before treating a payment as final.

## Operations [#operaciones]

### List customer payments [#get-transactions-payments]

```http
GET /api/public/v1/transactions/payments
```

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

Simple REST list of payments for an owned account (IBAN).

Filters are query parameters. Ownership (`tenant_id`, `owner_type`, `owner_id`)
is derived from the access token; clients must not send ownership filters.

**Account identification (required):** `type` + `value` (identifier
scheme + value, e.g. `IBAN` + IBAN string). This is not the internal
account classification.

Pagination is cursor-based (`limit`, `cursor`). Default limit is 20 (max 100).

`status` in the response and as a filter uses the public lowercase catalog
(`pending`, `processing`, `confirmed`, `posted`, `failed`). Same values as webhooks.
REST also returns `status_detail` (uppercase operational name).

Each item includes a consolidated `result` (`succeeded`, `failed`, `reversed`, `pending`)
for reconciliation when a webhook was missed. `status` is only the processing/lifecycle
catalog and does not change to `failed` when a posted payment is later reversed —
use `result=reversed` and `has_reversal=true` (webhook `payment.reversed`).
`error` is `null` when there is no failure information; it is an object only when
`domain` / `platform` is present (`result` is `failed` or `reversed`), same shape
as webhook `payment.failed` `payment.error`.
`source` / `destination` use `null` for unknown `account`, `name`, and `identification`;
account values are IBAN or PHONE only (never internal account UUIDs).
For PIN payments, `identification.number` is the partner-submitted public number
(no SINPE hyphens); older payments may still show the hyphenated SINPE form.

**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. |
| `type` | query | string | yes | Account **identifier scheme** (e.g. `IBAN`). This is **not** the internal account classification (e.g. operational). Values: IBAN |
| `value` | query | string | yes | Account identifier value (IBAN). Spaces are ignored. |
| `status` | query | string | no | Optional payment status filter. Public catalog is lowercase: `pending`, `processing`, `confirmed`, `posted`, `failed`. Uppercase aliases of the same values are accepted. Legacy values such as `INITIATED` map to `pending`. Values: pending, processing, confirmed, posted, failed |
| `date_from` | query | string (date-time) | no | Inclusive lower bound for `created_at`. RFC 3339 (UTC `Z` or offset; optional fractional seconds). |
| `date_to` | query | string (date-time) | no | Inclusive upper bound for `created_at`. RFC 3339 (UTC `Z` or offset; optional fractional seconds). |
| `client_reference` | query | string | no | Filter by partner client reference (exact match against the value sent at payment creation). |
| `currency` | query | string | no | Filter by payment currency (ISO 4217, e.g. CRC). |
| `payment_method_code` | query | string | no | Filter by public payment method code. Values: PIN, SINPE_MOVIL |
| `direction` | query | string | no | Filter by payment direction. Accepts public values `OUT`/`IN` or `PAYOUT`/`PAYIN` (internally normalized to `payout`/`payin`). Values: OUT, IN, PAYOUT, PAYIN |
| `limit` | query | integer | no | Page size (default 20, max 100). |
| `cursor` | query | string | no | Opaque cursor from a previous response `pagination.next_cursor`. |

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

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `schema_version` | string | yes | Same value as webhook envelope `schema_version` (`1.0.0`). |
| `items` | array<object> | yes | — |
| `items[].schema_version` | string | yes | Payment-read contract version. Same value as webhook envelope `schema_version` (`1.0.0`) for `payment.succeeded` / `payment.failed`. |
| `items[].payment_id` | string (uuid) | yes | Payment UUID returned by create. Partner-facing payment key for `GET /transactions/payments/{payment_id}` and search. Not an internal account id. |
| `items[].public_id` | string | no | Numeric public payment identifier returned by create. `null` if not yet assigned. Not an internal account UUID. The same identifier is a number in webhooks and text in REST. Always treat it as text. Can be null. |
| `items[].type` | string | yes | Payment type derived from direction (`PAYIN` / `PAYOUT`). |
| `items[].payment_method_code` | string | yes | Public payment method catalog (`PIN`, `SINPE_MOVIL`). Same values as webhooks. Values: PIN, SINPE_MOVIL |
| `items[].status` | string | yes | Public payment status catalog. Always lowercase. Same values on REST (create, list, get) and webhooks (`payment.status`). Internal lifecycle states are collapsed: - `pending`: initiated, validated, accepted - `processing`: processing, pending_processing, posting - `confirmed`: confirmed by the rail, not yet ledger-posted - `posted`: ledger posted (stays `posted` after a later reversal) - `failed`: failed, rejected, posting_failed, cancelled, expired A reversal is **not** a status. Use REST `result=reversed` / `has_reversal=true` and webhook `payment.reversed`. Operational detail remains in `status_detail` (uppercase internal name) on REST create/list/get. Webhook `payment.failed` uses `error.platform.code` to distinguish posting failures from provider rejections. Values: pending, processing, confirmed, posted, failed |
| `items[].status_detail` | string | no | Operational (internal) status in uppercase, e.g. `VALIDATED`, `POSTED`, `POSTING_FAILED`. Companion to the public `status` catalog. Independent of `result` (`posted` is the debit/posting milestone, not provider success). |
| `items[].result` | string | yes | Consolidated business outcome for reconciliation (backup to webhooks). Independent of processing `status`. Derived as: `reversed` when `has_reversal` is true (highest priority); `succeeded` when `succeeded_at` is set and there is no reversal; `failed` when processing status is `failed` or `posting_failed`; otherwise `pending`. A posted payment that was later reversed has `status=posted` and `result=reversed`. Values: succeeded, failed, reversed, pending |
| `items[].succeeded_at` | string (date-time) | no | Provider success confirmation timestamp (RFC 3339 UTC, second precision, `Z`). Null while the payment may already be `posted` internally but the provider has not confirmed success. Can be null. |
| `items[].has_reversal` | boolean | yes | True when a reversal was applied. Independent of `status`. Correlates with webhook `payment.reversed`. |
| `items[].reversed_at` | string (date-time) | no | When the reversal was applied. Null when `has_reversal` is false. Can be null. |
| `items[].created_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `items[].updated_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `items[].amount` | object | no | Same monetary shape as payment create (`PaymentAmount`). |
| `items[].amount.amount` | string | yes | — |
| `items[].amount.currency` | string | yes | — |
| `items[].source` | object | no | Payment party on read APIs. Nested fields are always present: `null` when the value is unknown. `account` is a public identifier (`IBAN` or `PHONE`); internal account UUIDs are never returned. |
| `items[].source.account` | object | yes | Public account identifier (`type` + `value`). `null` when the IBAN or phone cannot be resolved. Never an internal account UUID. Can be null. |
| `items[].source.name` | string | yes | Party name from processing metadata when available; otherwise `null`. Can be null. |
| `items[].source.identification` | object | yes | Party identification (`type` catalog key + `number`). Object only when at least one field is known; otherwise `null`. Does not expose internal SINPE type codes (`0`–`9`); unmapped types are omitted (`type: null`). `number` is the partner-submitted public value (digits, no SINPE hyphens). Historical payments may still return the hyphenated SINPE display form. Can be null. |
| `items[].destination` | object | no | Payment party on read APIs. Nested fields are always present: `null` when the value is unknown. `account` is a public identifier (`IBAN` or `PHONE`); internal account UUIDs are never returned. |
| `items[].destination.account` | object | yes | Public account identifier (`type` + `value`). `null` when the IBAN or phone cannot be resolved. Never an internal account UUID. Can be null. |
| `items[].destination.name` | string | yes | Party name from processing metadata when available; otherwise `null`. Can be null. |
| `items[].destination.identification` | object | yes | Party identification (`type` catalog key + `number`). Object only when at least one field is known; otherwise `null`. Does not expose internal SINPE type codes (`0`–`9`); unmapped types are omitted (`type: null`). `number` is the partner-submitted public value (digits, no SINPE hyphens). Historical payments may still return the hyphenated SINPE display form. Can be null. |
| `items[].client_reference` | string | no | Partner reference sent at payment creation (stable after processing). Same field as webhook `payment.client_reference`. |
| `items[].external_reference` | string | no | Rail/SINPE transaction id. Same field, location, and meaning as webhook `payment.external_reference` on `payment.succeeded` / `payment.failed` Sibling of `client_reference`. `null` if none. Can be null. |
| `items[].provider` | object | no | Shared provider snapshot on REST payment reads and webhook envelopes. Rail reference is only on `payment.external_reference`, not duplicated here. `provider_status_*` match webhook `rejection.reason_code` / `message` / `code` when the provider rejected the operation. `null` when there is no rejection. |
| `items[].provider.correlation_id` | string | yes | Provider transaction correlation identifier. Sent to SINPE as the correlation id. Not `channel_reference` (e.g. `MOBILE_APP`). Distinct from webhook `event.correlation_id` (HTTP request id for this delivery). `null` when unknown. Can be null. |
| `items[].provider.occurred_at` | string (date-time) | yes | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `items[].provider.provider_status_code` | string | yes | Raw provider Motivo/code (e.g. `31`). Same meaning as webhook `rejection.reason_code`. `null` when there is no provider rejection. Can be null. |
| `items[].provider.provider_status_desc` | string | yes | Provider Motivo text (prefers Detalle). Same meaning as webhook `rejection.message`. `null` when there is no provider rejection. Can be null. |
| `items[].provider.provider_status_semantic` | string | yes | Normalized platform semantic (e.g. `ACCOUNT_BLOCKED`). Same meaning as webhook `rejection.code`. `null` when there is no provider rejection. Can be null. |
| `items[].error` | object | no | Always present. `null` when there is no failure information (`succeeded`, `pending`, or failed/reversed without taxonomy). Object uses the same `domain` / `platform` shape as webhook `payment.failed`. Can be null. |
| `items[].error.domain` | string | yes | Same catalog as webhook `payment.error.domain`. Values: provider_rejection, platform_validation, platform_posting, platform_infra, platform_reversal |
| `items[].error.platform` | object | yes | — |
| `pagination` | object | no | Cursor pagination. Pass next_cursor as cursor on the next request. |
| `pagination.next_cursor` | string | no | Opaque cursor for the next page; omitted or empty when there is no next page |
| `pagination.limit` | integer | no | Page size applied to this response |

**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": {
    "schema_version": "1.0.0",
    "items": [
      {
        "schema_version": "1.0.0",
        "payment_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "public_id": "10234",
        "type": "PAYOUT",
        "payment_method_code": "PIN",
        "status": "posted",
        "status_detail": "POSTED",
        "result": "succeeded",
        "succeeded_at": "2026-01-15T12:00:00Z",
        "has_reversal": false,
        "reversed_at": null,
        "created_at": "2026-01-15T12:00:00Z",
        "updated_at": "2026-01-15T12:00:00Z",
        "amount": {
          "amount": "1000.00",
          "currency": "CRC",
          "fees": "0.00",
          "taxes": "0.00"
        },
        "source": {
          "account": {
            "type": "IBAN",
            "value": "CR26036900101010588061",
            "currency": "CRC"
          },
          "name": "María González",
          "identification": {
            "type": "cedula",
            "number": "0205620043"
          }
        },
        "destination": {
          "account": {
            "type": "IBAN",
            "value": "CR16010400000000000031",
            "currency": "CRC"
          },
          "name": "Juan Perez",
          "identification": {
            "type": "cedula",
            "number": "0101010101"
          }
        },
        "client_reference": "INV-1001",
        "external_reference": "SINPE-20260115-000123",
        "provider": {
          "correlation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
          "occurred_at": "2026-01-15T12:00:00Z",
          "provider_status_code": "00",
          "provider_status_desc": "Aprobada",
          "provider_status_semantic": "SUCCESS"
        },
        "error": null
      }
    ],
    "pagination": {
      "next_cursor": "",
      "limit": 20
    }
  }
}
```

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

### Create payment [#post-transactions-payments]

```http
POST /api/public/v1/transactions/payments
```

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

Creates a payment using an authorized internal source account. Processing may be asynchronous; HTTP 201 does not imply the payment is finalized.
`source.identification.number` and `destination.identification.number` are stored in public form (the submitted digits, without SINPE hyphens) and returned on later payment reads. The hyphenated SINPE form is used only internally and is not the public contract.
Idempotency-Key is optional; if omitted, each request is treated as a new attempt. A repeated `client_reference` returns 409 PAYMENT_DUPLICATE when an in-flight or completed payment already uses it; a prior `failed` (or rejected/cancelled/expired) payment does not block a retry with the same `client_reference`. If Idempotency-Key is provided, the same key with the same payload replays the prior response; a different payload with the same key returns 409 IDEMPOTENCY_CONFLICT.

There is no dual control in API payment creation: a payment is processed directly when the token, permissions and data are valid. Only direction OUT is accepted. Limits apply per transaction, daily and monthly, in amount and count. If any limit is exceeded, processing fails and the payment.failed event notifies the result.

**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 | Optional opaque client key for safe retries. When omitted, each call is a new attempt (duplicates are detected by client_reference on in-flight or completed payments). Reusing a key with a different body returns 409. |

**Request body**

| Field | Type | Required | Description |
|---|---|---|---|
| `payment_method_code` | string | yes | Public payment method catalog (`PIN`, `SINPE_MOVIL`). Values: PIN, SINPE_MOVIL |
| `direction` | string | yes | Creation only accepts OUT (outbound). IN payments remain available in listings and webhooks. Values: OUT |
| `country_code` | string | no | Operational country when multi-country applies |
| `amount` | object | yes | — |
| `amount.amount` | string | yes | Amount as a decimal string with exactly 2 fraction digits (e.g. "1000.00"). |
| `amount.currency` | string | yes | ISO 4217 currency of the payment. Values: CRC, USD |
| `source` | object | yes | — |
| `source.account` | object | yes | — |
| `source.account.type` | string | yes | Account **identifier scheme**. Only `IBAN` is supported here. This is not the internal account classification (e.g. operational). Values: IBAN |
| `source.account.value` | string | yes | IBAN (spaces allowed; normalized server-side) |
| `source.name` | string | yes | Account holder name |
| `source.identification` | object | yes | Counterparty identification for payment create. `type` uses platform identification-catalog keys (e.g. `cedula`, `juridica`, `dimex`). Same vocabulary as `holder.id_type` from `POST /accounts/validate`. `number` is the public identification (digits, no SINPE hyphens). The platform may reformat it internally for SINPE; that internal form is not echoed on public payment reads. For SINPE_MOVIL, account validate may return `unknown` because the wallet operation does not expose identification type; do not assume a confirmed type. |
| `source.identification.type` | string | no | Identification catalog key (lowercase), e.g. `cedula`, `juridica`. May be copied from `holder.id_type` after account validate. For SINPE_MOVIL that value may be `unknown` (expected limitation, not an error). Values: cedula, juridica, dimex, gobierno, institucion_autonoma, didi, pasaporte |
| `source.identification.number` | string | no | Public identification number as submitted by the partner. Digits only; do not send SINPE hyphenation (e.g. `01-1234-3005`) or treat this field as the internal SINPE display form. Supported types include `cedula`, `juridica`, `dimex`, `gobierno`, `institucion_autonoma`, `didi`, and `pasaporte`. |
| `destination` | object | yes | — |
| `destination.account` | object | yes | — |
| `destination.account.type` | string | yes | Counterparty account **identifier scheme** (`IBAN` for PIN, `PHONE` for SINPE_MOVIL). Not the internal account classification. Values: IBAN, PHONE |
| `destination.account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). Pattern for `PHONE`: `^\d{8}$`. |
| `destination.name` | string | yes | Counterparty name |
| `destination.identification` | object | yes | Counterparty identification for payment create. `type` uses platform identification-catalog keys (e.g. `cedula`, `juridica`, `dimex`). Same vocabulary as `holder.id_type` from `POST /accounts/validate`. `number` is the public identification (digits, no SINPE hyphens). The platform may reformat it internally for SINPE; that internal form is not echoed on public payment reads. For SINPE_MOVIL, account validate may return `unknown` because the wallet operation does not expose identification type; do not assume a confirmed type. |
| `destination.identification.type` | string | no | Identification catalog key (lowercase), e.g. `cedula`, `juridica`. May be copied from `holder.id_type` after account validate. For SINPE_MOVIL that value may be `unknown` (expected limitation, not an error). Values: cedula, juridica, dimex, gobierno, institucion_autonoma, didi, pasaporte |
| `destination.identification.number` | string | no | Public identification number as submitted by the partner. Digits only; do not send SINPE hyphenation (e.g. `01-1234-3005`) or treat this field as the internal SINPE display form. Supported types include `cedula`, `juridica`, `dimex`, `gobierno`, `institucion_autonoma`, `didi`, and `pasaporte`. |
| `detail_reference` | string | yes | Operational / transactional description stored on the payment |
| `client_reference` | string | no | Partner functional reference. Unique per tenant among in-flight and completed payments; a failed attempt can be retried with the same value. |
| `channel_reference` | string | no | — |
| `originator` | object | no | Optional Ultimate Debtor / Travel Rule originator information. Informational only in this release; no validation is applied. Stored under payment metadata when provided. Can be null. |
| `originator.company` | object | no | Can be null. |
| `originator.company.legal_name` | string | no | — |
| `originator.company.id_type` | string | no | — |
| `originator.company.id_number` | string | no | — |
| `originator.customer` | object | no | Can be null. |
| `originator.customer.name` | string | no | — |
| `originator.customer.id_type` | string | no | — |
| `originator.customer.id_number` | string | no | — |

**Request example**

```json
{
  "payment_method_code": "PIN",
  "direction": "OUT",
  "country_code": "CR",
  "amount": {
    "amount": "1000.00",
    "currency": "CRC"
  },
  "source": {
    "account": {
      "type": "IBAN",
      "value": "CR26036900101010588061"
    },
    "name": "María González",
    "identification": {
      "type": "cedula",
      "number": "0205620043"
    }
  },
  "destination": {
    "account": {
      "type": "IBAN",
      "value": "CR16010400000000000031"
    },
    "name": "Juan Perez",
    "identification": {
      "type": "cedula",
      "number": "0101010101"
    }
  },
  "detail_reference": "Pago factura 1001",
  "client_reference": "INV-1001",
  "channel_reference": "MOBILE_APP"
}
```

**201** — The resource was created successfully.

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `payment_id` | string (uuid) | no | Internal payment UUID |
| `public_id` | string | no | Public numeric payment identifier The same identifier is a number in webhooks and text in REST. Always treat it as text. |
| `status` | string | no | Public payment status catalog. Always lowercase. Same values on REST (create, list, get) and webhooks (`payment.status`). Internal lifecycle states are collapsed: - `pending`: initiated, validated, accepted - `processing`: processing, pending_processing, posting - `confirmed`: confirmed by the rail, not yet ledger-posted - `posted`: ledger posted (stays `posted` after a later reversal) - `failed`: failed, rejected, posting_failed, cancelled, expired A reversal is **not** a status. Use REST `result=reversed` / `has_reversal=true` and webhook `payment.reversed`. Operational detail remains in `status_detail` (uppercase internal name) on REST create/list/get. Webhook `payment.failed` uses `error.platform.code` to distinguish posting failures from provider rejections. Values: pending, processing, confirmed, posted, failed |
| `status_detail` | string | no | Operational status detail (e.g. PROCESSING) |
| `payment_method_code` | string | no | Public payment method catalog (`PIN`, `SINPE_MOVIL`). Values: PIN, SINPE_MOVIL |
| `created_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |

**Response example**

```json
{
  "success": true,
  "http_status_code": 201,
  "response_code": "CREATED",
  "message": "The resource was created successfully.",
  "correlation_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "data": {
    "payment_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "public_id": "10234",
    "status": "pending",
    "status_detail": "PROCESSING",
    "payment_method_code": "PIN",
    "created_at": "2026-01-15T12:00:00Z"
  }
}
```

**Error responses**

| HTTP | response_code | Description |
|---|---|---|
| 400 | `PAYMENT_INVALID_PAYLOAD` | Invalid payment request payload. |
| 400 | `INVALID_PAYMENT_METHOD` | Invalid or unsupported payment method. |
| 400 | `INVALID_ACCOUNT_TYPE` | Unsupported or invalid account type. |
| 400 | `ACCOUNT_INVALID` | The account is invalid or not enabled for the service. |
| 400 | `ACCOUNT_HOLDER_MISMATCH` | Account holder does not match the provided identification or name. |
| 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. |
| 409 | `IDEMPOTENCY_CONFLICT` | Idempotency conflict: the same Idempotency-Key was reused with a different request body, or a previous request is still in progress. |
| 409 | `PAYMENT_DUPLICATE` | An in-flight or completed payment with this client_reference already exists. |
| 409 | `CONFLICT` | The request conflicts with the current state. |
| 422 | `INSUFFICIENT_FUNDS` | Insufficient funds to complete the payment. |
| 422 | `LIMIT_EXCEEDED` | A transaction or accumulation limit was exceeded. |
| 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. |

### Search payment by identifier [#post-transactions-payments-search]

```http
POST /api/public/v1/transactions/payments/search
```

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

Looks up a single payment within the supplied customer account by exactly one of:
- `payment_id` (internal UUID)
- `public_id` (numeric public identifier)
- `client_reference` (partner reference sent at creation)

Sending more than one identifier returns `400`. Response shape matches get-by-id
(`schema_version`, payment-level `external_reference`, shared `provider` object,
`provider.provider_status_*`, webhook-aligned `error`). Same public contract as
`GET /transactions/payments`, including the public `identification.number` of PIN payments.
If several payments share a `client_reference` (failed attempt plus retry), the in-flight or completed payment is returned when one exists.

**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** (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. |
| `payment_id` | string (uuid) | no | Internal payment UUID. |
| `public_id` | string | no | Numeric public payment identifier. The same identifier is a number in webhooks and text in REST. Always treat it as text. |
| `client_reference` | string | no | Partner reference sent at payment creation. |

**Request example** — byPaymentId

```json
{
  "account": {
    "type": "IBAN",
    "value": "CR08036900101010933608"
  },
  "payment_id": "11111111-1111-1111-1111-111111111111"
}
```

**Request example** — byPublicId

```json
{
  "account": {
    "type": "IBAN",
    "value": "CR08036900101010933608"
  },
  "public_id": "134729"
}
```

**Request example** — byClientReference

```json
{
  "account": {
    "type": "IBAN",
    "value": "CR08036900101010933608"
  },
  "client_reference": "INV-01082"
}
```

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

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `schema_version` | string | yes | Payment-read contract version. Same value as webhook envelope `schema_version` (`1.0.0`) for `payment.succeeded` / `payment.failed`. |
| `payment_id` | string (uuid) | yes | Payment UUID returned by create. Partner-facing payment key for `GET /transactions/payments/{payment_id}` and search. Not an internal account id. |
| `public_id` | string | no | Numeric public payment identifier returned by create. `null` if not yet assigned. Not an internal account UUID. The same identifier is a number in webhooks and text in REST. Always treat it as text. Can be null. |
| `type` | string | yes | Payment type derived from direction (`PAYIN` / `PAYOUT`). |
| `payment_method_code` | string | yes | Public payment method catalog (`PIN`, `SINPE_MOVIL`). Same values as webhooks. Values: PIN, SINPE_MOVIL |
| `status` | string | yes | Public payment status catalog. Always lowercase. Same values on REST (create, list, get) and webhooks (`payment.status`). Internal lifecycle states are collapsed: - `pending`: initiated, validated, accepted - `processing`: processing, pending_processing, posting - `confirmed`: confirmed by the rail, not yet ledger-posted - `posted`: ledger posted (stays `posted` after a later reversal) - `failed`: failed, rejected, posting_failed, cancelled, expired A reversal is **not** a status. Use REST `result=reversed` / `has_reversal=true` and webhook `payment.reversed`. Operational detail remains in `status_detail` (uppercase internal name) on REST create/list/get. Webhook `payment.failed` uses `error.platform.code` to distinguish posting failures from provider rejections. Values: pending, processing, confirmed, posted, failed |
| `status_detail` | string | no | Operational (internal) status in uppercase, e.g. `VALIDATED`, `POSTED`, `POSTING_FAILED`. Companion to the public `status` catalog. Independent of `result` (`posted` is the debit/posting milestone, not provider success). |
| `result` | string | yes | Consolidated business outcome for reconciliation (backup to webhooks). Independent of processing `status`. Derived as: `reversed` when `has_reversal` is true (highest priority); `succeeded` when `succeeded_at` is set and there is no reversal; `failed` when processing status is `failed` or `posting_failed`; otherwise `pending`. A posted payment that was later reversed has `status=posted` and `result=reversed`. Values: succeeded, failed, reversed, pending |
| `succeeded_at` | string (date-time) | no | Provider success confirmation timestamp (RFC 3339 UTC, second precision, `Z`). Null while the payment may already be `posted` internally but the provider has not confirmed success. Can be null. |
| `has_reversal` | boolean | yes | True when a reversal was applied. Independent of `status`. Correlates with webhook `payment.reversed`. |
| `reversed_at` | string (date-time) | no | When the reversal was applied. Null when `has_reversal` is false. Can be null. |
| `created_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `updated_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `amount` | object | no | Same monetary shape as payment create (`PaymentAmount`). |
| `amount.amount` | string | yes | — |
| `amount.currency` | string | yes | — |
| `source` | object | no | Payment party on read APIs. Nested fields are always present: `null` when the value is unknown. `account` is a public identifier (`IBAN` or `PHONE`); internal account UUIDs are never returned. |
| `source.account` | object | yes | Public account identifier (`type` + `value`). `null` when the IBAN or phone cannot be resolved. Never an internal account UUID. Can be null. |
| `source.account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `source.account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `source.account.currency` | string | no | Optional account currency in response payloads. |
| `source.name` | string | yes | Party name from processing metadata when available; otherwise `null`. Can be null. |
| `source.identification` | object | yes | Party identification (`type` catalog key + `number`). Object only when at least one field is known; otherwise `null`. Does not expose internal SINPE type codes (`0`–`9`); unmapped types are omitted (`type: null`). `number` is the partner-submitted public value (digits, no SINPE hyphens). Historical payments may still return the hyphenated SINPE display form. Can be null. |
| `source.identification.type` | string | no | Identification catalog key (e.g. `cedula`, `juridica`). `null` when unknown. Can be null. |
| `source.identification.number` | string | no | Public identification number. New PIN payments return the partner-submitted value (digits, no SINPE hyphens). Historical payments may still return the SINPE display form (hyphenated, possibly padded). Can be null. |
| `destination` | object | no | Payment party on read APIs. Nested fields are always present: `null` when the value is unknown. `account` is a public identifier (`IBAN` or `PHONE`); internal account UUIDs are never returned. |
| `destination.account` | object | yes | Public account identifier (`type` + `value`). `null` when the IBAN or phone cannot be resolved. Never an internal account UUID. Can be null. |
| `destination.account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `destination.account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `destination.account.currency` | string | no | Optional account currency in response payloads. |
| `destination.name` | string | yes | Party name from processing metadata when available; otherwise `null`. Can be null. |
| `destination.identification` | object | yes | Party identification (`type` catalog key + `number`). Object only when at least one field is known; otherwise `null`. Does not expose internal SINPE type codes (`0`–`9`); unmapped types are omitted (`type: null`). `number` is the partner-submitted public value (digits, no SINPE hyphens). Historical payments may still return the hyphenated SINPE display form. Can be null. |
| `destination.identification.type` | string | no | Identification catalog key (e.g. `cedula`, `juridica`). `null` when unknown. Can be null. |
| `destination.identification.number` | string | no | Public identification number. New PIN payments return the partner-submitted value (digits, no SINPE hyphens). Historical payments may still return the SINPE display form (hyphenated, possibly padded). Can be null. |
| `client_reference` | string | no | Partner reference sent at payment creation (stable after processing). Same field as webhook `payment.client_reference`. |
| `external_reference` | string | no | Rail/SINPE transaction id. Same field, location, and meaning as webhook `payment.external_reference` on `payment.succeeded` / `payment.failed` Sibling of `client_reference`. `null` if none. Can be null. |
| `provider` | object | no | Shared provider snapshot on REST payment reads and webhook envelopes. Rail reference is only on `payment.external_reference`, not duplicated here. `provider_status_*` match webhook `rejection.reason_code` / `message` / `code` when the provider rejected the operation. `null` when there is no rejection. |
| `provider.correlation_id` | string | yes | Provider transaction correlation identifier. Sent to SINPE as the correlation id. Not `channel_reference` (e.g. `MOBILE_APP`). Distinct from webhook `event.correlation_id` (HTTP request id for this delivery). `null` when unknown. Can be null. |
| `provider.occurred_at` | string (date-time) | yes | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `provider.provider_status_code` | string | yes | Raw provider Motivo/code (e.g. `31`). Same meaning as webhook `rejection.reason_code`. `null` when there is no provider rejection. Can be null. |
| `provider.provider_status_desc` | string | yes | Provider Motivo text (prefers Detalle). Same meaning as webhook `rejection.message`. `null` when there is no provider rejection. Can be null. |
| `provider.provider_status_semantic` | string | yes | Normalized platform semantic (e.g. `ACCOUNT_BLOCKED`). Same meaning as webhook `rejection.code`. `null` when there is no provider rejection. Can be null. |
| `error` | object | no | Always present. `null` when there is no failure information (`succeeded`, `pending`, or failed/reversed without taxonomy). Object uses the same `domain` / `platform` shape as webhook `payment.failed`. Can be null. |
| `error.domain` | string | yes | Same catalog as webhook `payment.error.domain`. Values: provider_rejection, platform_validation, platform_posting, platform_infra, platform_reversal |
| `error.platform` | object | yes | — |
| `error.platform.code` | string | yes | Stable platform public code (same as webhook `payment.error.platform.code`). |
| `error.platform.message` | string | yes | Actionable message (prefers provider Detalle). Same as webhook `payment.error.platform.message`. |

**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": {
    "schema_version": "1.0.0",
    "payment_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "public_id": "10234",
    "type": "PAYOUT",
    "payment_method_code": "PIN",
    "status": "posted",
    "status_detail": "POSTED",
    "result": "succeeded",
    "succeeded_at": "2026-01-15T12:00:00Z",
    "has_reversal": false,
    "reversed_at": null,
    "created_at": "2026-01-15T12:00:00Z",
    "updated_at": "2026-01-15T12:00:00Z",
    "amount": {
      "amount": "1000.00",
      "currency": "CRC",
      "fees": "0.00",
      "taxes": "0.00"
    },
    "source": {
      "account": {
        "type": "IBAN",
        "value": "CR26036900101010588061",
        "currency": "CRC"
      },
      "name": "María González",
      "identification": {
        "type": "cedula",
        "number": "0205620043"
      }
    },
    "destination": {
      "account": {
        "type": "IBAN",
        "value": "CR16010400000000000031",
        "currency": "CRC"
      },
      "name": "Juan Perez",
      "identification": {
        "type": "cedula",
        "number": "0101010101"
      }
    },
    "client_reference": "INV-1001",
    "external_reference": "SINPE-20260115-000123",
    "provider": {
      "correlation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "occurred_at": "2026-01-15T12:00:00Z",
      "provider_status_code": "00",
      "provider_status_desc": "Aprobada",
      "provider_status_semantic": "SUCCESS"
    },
    "error": null
  }
}
```

**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 | `PAYMENT_NOT_FOUND` | Payment not found. |
| 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. |

### Get payment by id [#get-transactions-payments-payment-id]

```http
GET /api/public/v1/transactions/payments/{payment_id}
```

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

Returns a single payment using the same public contract as the list endpoint,
including consolidated `result`, `succeeded_at`, `has_reversal`, and `error`.
`error` is `null` when there is no failure information; it is an object only when
`domain` / `platform` is present (`result` is `failed` or `reversed`), same shape
as webhook `payment.failed` `payment.error`.
`source` / `destination` use `null` for unknown nested fields and never expose internal account UUIDs.
`identification.number` on PIN payments is the partner-submitted public number
(digits, no SINPE hyphens). Older payments may still return the hyphenated SINPE form.
Use this as a webhook backup.

Looks up a payment by either identifier:
- `payment_id` (internal UUID)
- `public_id` (numeric public identifier)

For `client_reference`, use `POST /api/public/v1/transactions/payments/search`.

**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. |
| `payment_id` | path | string | yes | Internal payment UUID or numeric public_id. |

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

Fields in `data`:

| Field | Type | Required | Description |
|---|---|---|---|
| `schema_version` | string | yes | Payment-read contract version. Same value as webhook envelope `schema_version` (`1.0.0`) for `payment.succeeded` / `payment.failed`. |
| `payment_id` | string (uuid) | yes | Payment UUID returned by create. Partner-facing payment key for `GET /transactions/payments/{payment_id}` and search. Not an internal account id. |
| `public_id` | string | no | Numeric public payment identifier returned by create. `null` if not yet assigned. Not an internal account UUID. The same identifier is a number in webhooks and text in REST. Always treat it as text. Can be null. |
| `type` | string | yes | Payment type derived from direction (`PAYIN` / `PAYOUT`). |
| `payment_method_code` | string | yes | Public payment method catalog (`PIN`, `SINPE_MOVIL`). Same values as webhooks. Values: PIN, SINPE_MOVIL |
| `status` | string | yes | Public payment status catalog. Always lowercase. Same values on REST (create, list, get) and webhooks (`payment.status`). Internal lifecycle states are collapsed: - `pending`: initiated, validated, accepted - `processing`: processing, pending_processing, posting - `confirmed`: confirmed by the rail, not yet ledger-posted - `posted`: ledger posted (stays `posted` after a later reversal) - `failed`: failed, rejected, posting_failed, cancelled, expired A reversal is **not** a status. Use REST `result=reversed` / `has_reversal=true` and webhook `payment.reversed`. Operational detail remains in `status_detail` (uppercase internal name) on REST create/list/get. Webhook `payment.failed` uses `error.platform.code` to distinguish posting failures from provider rejections. Values: pending, processing, confirmed, posted, failed |
| `status_detail` | string | no | Operational (internal) status in uppercase, e.g. `VALIDATED`, `POSTED`, `POSTING_FAILED`. Companion to the public `status` catalog. Independent of `result` (`posted` is the debit/posting milestone, not provider success). |
| `result` | string | yes | Consolidated business outcome for reconciliation (backup to webhooks). Independent of processing `status`. Derived as: `reversed` when `has_reversal` is true (highest priority); `succeeded` when `succeeded_at` is set and there is no reversal; `failed` when processing status is `failed` or `posting_failed`; otherwise `pending`. A posted payment that was later reversed has `status=posted` and `result=reversed`. Values: succeeded, failed, reversed, pending |
| `succeeded_at` | string (date-time) | no | Provider success confirmation timestamp (RFC 3339 UTC, second precision, `Z`). Null while the payment may already be `posted` internally but the provider has not confirmed success. Can be null. |
| `has_reversal` | boolean | yes | True when a reversal was applied. Independent of `status`. Correlates with webhook `payment.reversed`. |
| `reversed_at` | string (date-time) | no | When the reversal was applied. Null when `has_reversal` is false. Can be null. |
| `created_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `updated_at` | string (date-time) | no | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `amount` | object | no | Same monetary shape as payment create (`PaymentAmount`). |
| `amount.amount` | string | yes | — |
| `amount.currency` | string | yes | — |
| `source` | object | no | Payment party on read APIs. Nested fields are always present: `null` when the value is unknown. `account` is a public identifier (`IBAN` or `PHONE`); internal account UUIDs are never returned. |
| `source.account` | object | yes | Public account identifier (`type` + `value`). `null` when the IBAN or phone cannot be resolved. Never an internal account UUID. Can be null. |
| `source.account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `source.account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `source.account.currency` | string | no | Optional account currency in response payloads. |
| `source.name` | string | yes | Party name from processing metadata when available; otherwise `null`. Can be null. |
| `source.identification` | object | yes | Party identification (`type` catalog key + `number`). Object only when at least one field is known; otherwise `null`. Does not expose internal SINPE type codes (`0`–`9`); unmapped types are omitted (`type: null`). `number` is the partner-submitted public value (digits, no SINPE hyphens). Historical payments may still return the hyphenated SINPE display form. Can be null. |
| `source.identification.type` | string | no | Identification catalog key (e.g. `cedula`, `juridica`). `null` when unknown. Can be null. |
| `source.identification.number` | string | no | Public identification number. New PIN payments return the partner-submitted value (digits, no SINPE hyphens). Historical payments may still return the SINPE display form (hyphenated, possibly padded). Can be null. |
| `destination` | object | no | Payment party on read APIs. Nested fields are always present: `null` when the value is unknown. `account` is a public identifier (`IBAN` or `PHONE`); internal account UUIDs are never returned. |
| `destination.account` | object | yes | Public account identifier (`type` + `value`). `null` when the IBAN or phone cannot be resolved. Never an internal account UUID. Can be null. |
| `destination.account.type` | string | yes | Account **identifier scheme** (e.g. `IBAN`, `PHONE`). Not the internal account classification (e.g. operational). Values: IBAN, PHONE |
| `destination.account.value` | string | yes | When `type` is `PHONE` (SINPE Móvil): 8 local digits, no country prefix (e.g. "88887777"). |
| `destination.account.currency` | string | no | Optional account currency in response payloads. |
| `destination.name` | string | yes | Party name from processing metadata when available; otherwise `null`. Can be null. |
| `destination.identification` | object | yes | Party identification (`type` catalog key + `number`). Object only when at least one field is known; otherwise `null`. Does not expose internal SINPE type codes (`0`–`9`); unmapped types are omitted (`type: null`). `number` is the partner-submitted public value (digits, no SINPE hyphens). Historical payments may still return the hyphenated SINPE display form. Can be null. |
| `destination.identification.type` | string | no | Identification catalog key (e.g. `cedula`, `juridica`). `null` when unknown. Can be null. |
| `destination.identification.number` | string | no | Public identification number. New PIN payments return the partner-submitted value (digits, no SINPE hyphens). Historical payments may still return the SINPE display form (hyphenated, possibly padded). Can be null. |
| `client_reference` | string | no | Partner reference sent at payment creation (stable after processing). Same field as webhook `payment.client_reference`. |
| `external_reference` | string | no | Rail/SINPE transaction id. Same field, location, and meaning as webhook `payment.external_reference` on `payment.succeeded` / `payment.failed` Sibling of `client_reference`. `null` if none. Can be null. |
| `provider` | object | no | Shared provider snapshot on REST payment reads and webhook envelopes. Rail reference is only on `payment.external_reference`, not duplicated here. `provider_status_*` match webhook `rejection.reason_code` / `message` / `code` when the provider rejected the operation. `null` when there is no rejection. |
| `provider.correlation_id` | string | yes | Provider transaction correlation identifier. Sent to SINPE as the correlation id. Not `channel_reference` (e.g. `MOBILE_APP`). Distinct from webhook `event.correlation_id` (HTTP request id for this delivery). `null` when unknown. Can be null. |
| `provider.occurred_at` | string (date-time) | yes | RFC 3339 timestamp in UTC with second precision and a `Z` suffix. Fractional seconds are never emitted. Example: `2026-01-15T12:00:00Z`. |
| `provider.provider_status_code` | string | yes | Raw provider Motivo/code (e.g. `31`). Same meaning as webhook `rejection.reason_code`. `null` when there is no provider rejection. Can be null. |
| `provider.provider_status_desc` | string | yes | Provider Motivo text (prefers Detalle). Same meaning as webhook `rejection.message`. `null` when there is no provider rejection. Can be null. |
| `provider.provider_status_semantic` | string | yes | Normalized platform semantic (e.g. `ACCOUNT_BLOCKED`). Same meaning as webhook `rejection.code`. `null` when there is no provider rejection. Can be null. |
| `error` | object | no | Always present. `null` when there is no failure information (`succeeded`, `pending`, or failed/reversed without taxonomy). Object uses the same `domain` / `platform` shape as webhook `payment.failed`. Can be null. |
| `error.domain` | string | yes | Same catalog as webhook `payment.error.domain`. Values: provider_rejection, platform_validation, platform_posting, platform_infra, platform_reversal |
| `error.platform` | object | yes | — |
| `error.platform.code` | string | yes | Stable platform public code (same as webhook `payment.error.platform.code`). |
| `error.platform.message` | string | yes | Actionable message (prefers provider Detalle). Same as webhook `payment.error.platform.message`. |

**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": {
    "schema_version": "1.0.0",
    "payment_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
    "public_id": "10234",
    "type": "PAYOUT",
    "payment_method_code": "PIN",
    "status": "posted",
    "status_detail": "POSTED",
    "result": "succeeded",
    "succeeded_at": "2026-01-15T12:00:00Z",
    "has_reversal": false,
    "reversed_at": null,
    "created_at": "2026-01-15T12:00:00Z",
    "updated_at": "2026-01-15T12:00:00Z",
    "amount": {
      "amount": "1000.00",
      "currency": "CRC",
      "fees": "0.00",
      "taxes": "0.00"
    },
    "source": {
      "account": {
        "type": "IBAN",
        "value": "CR26036900101010588061",
        "currency": "CRC"
      },
      "name": "María González",
      "identification": {
        "type": "cedula",
        "number": "0205620043"
      }
    },
    "destination": {
      "account": {
        "type": "IBAN",
        "value": "CR16010400000000000031",
        "currency": "CRC"
      },
      "name": "Juan Perez",
      "identification": {
        "type": "cedula",
        "number": "0101010101"
      }
    },
    "client_reference": "INV-1001",
    "external_reference": "SINPE-20260115-000123",
    "provider": {
      "correlation_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "occurred_at": "2026-01-15T12:00:00Z",
      "provider_status_code": "00",
      "provider_status_desc": "Aprobada",
      "provider_status_semantic": "SUCCESS"
    },
    "error": null
  }
}
```

**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 | `PAYMENT_NOT_FOUND` | Payment not found. |
| 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. |

## Processing and limits [#procesamiento]

API payment creation has no dual control: the payment is processed directly when the token, permissions and data are valid. Limits apply per transaction, daily and monthly, in amount and count. If any limit is exceeded, processing fails and the `payment.failed` event notifies the result.

The [MCP server](/en/developers/agentes/mcp/api-bancario) transfer-confirmation flow is separate from this direct API creation.
