Banking API payments
API 1.0.0An MCP tool wraps this operation: Banking tools.
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.
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/searchResource rules#
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.
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.
States#
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 event. Always check those two
fields before treating a payment as final.
Operations#
List customer payments
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. |
Response 200
The request was processed successfully.
| 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
{
"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 /api/public/v1/transactions/payments
Host: https://api-baas-sandbox.tilopay.com — requires Authorization: Bearer <access_token>. Accepts Idempotency-Key.
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
{
"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"
}Response 201
The resource was created successfully.
| 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
{
"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 /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
{
"account": {
"type": "IBAN",
"value": "CR08036900101010933608"
},
"payment_id": "11111111-1111-1111-1111-111111111111"
}byPublicId
{
"account": {
"type": "IBAN",
"value": "CR08036900101010933608"
},
"public_id": "134729"
}byClientReference
{
"account": {
"type": "IBAN",
"value": "CR08036900101010933608"
},
"client_reference": "INV-01082"
}Response 200
The request was processed successfully.
| 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
{
"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 /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. |
Response 200
The request was processed successfully.
| 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
{
"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#
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 transfer-confirmation flow is separate from this direct API creation.
Last verified: 2026-10-22