Banking API webhooks

API 1.0.0

The Webhooks group lets you register the HTTPS destination where you receive events, see which features you can subscribe to per payment method and fetch each event's payload contract before writing the receiver.

POST /api/public/v1/webhook/destination
GET  /api/public/v1/webhook/features
GET  /api/public/v1/webhook/event-contracts/{event_type}

Events#

Our API notifies you of four event types:

EventWhen it arrives
payin.postedAn inbound movement was credited.
payment.succeededThe payment finished successfully.
payment.failedThe payment finished with a failure.
payment.reversedThe payment was reversed.

A reversed payment keeps the posted state and is told apart by result: reversed and has_reversal: true; see payment states.

Receiver best practices#

Delivery is attempted up to 3 times per event and any HTTP 2xx counts as success. Respond 2xx as soon as you receive the event and process it separately: a slow receiver causes redeliveries. Treat events as potentially repeated and deduplicate by the envelope's event.event_id (retries redeliver the same event with the same event_id). And do not trust the final state to the webhook alone: confirm it with the payment lookup.

Operations#

Create webhook destination

POST /api/public/v1/webhook/destination

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

Valid fields depend on destination_kind and scope_kind. Send only the fields that apply to your combination; other scope fields must be omitted (not null placeholders).

Event payloads Inspect outbound contracts with GET /api/public/v1/webhook/event-contracts/{event_type} (payin.posted, payment.succeeded, payment.failed, payment.reversed).

destination_kind

  • EXTERNAL_WEBHOOK (only kind available on the public API): requires webhook_url (must start with https://) and secret_ref. Optional delivery tuning (timeout_ms, max_attempts). Payloads are always encrypted with AES_GCM using secret_ref; the HTTP body is {"data":"<ciphertext>"}.
  • INTERNAL is reserved for platform administration and cannot be created through this endpoint.

scope_kind

  • TENANT: omit scope_account_id, scope_owner_type, and scope_owner_id.
  • ACCOUNT: require scope_account_id; omit owner fields.
  • OWNER: require scope_owner_type and scope_owner_id; omit scope_account_id. scope_owner_type is one of tenant, partner, user, platform, customer (lowercase in payload).

Webhook encryption (EXTERNAL_WEBHOOK)

Deliveries always send Content-Type: application/json and Accept: application/json.

The HTTP body is always {"data": "<base64>"}. The base64-decoded bytes are:

nonce (12 bytes, random) || AES-256-GCM ciphertext || 16-byte auth tag
key                   = SHA256(secret_ref)              # 32 bytes → AES-256
wire                  = base64_decode(data)
nonce, sealed         = wire[:12], wire[12:]             # sealed = ciphertext || tag
plaintext             = AES-GCM-Open(key, nonce, sealed, additional_data = none)

No additional authenticated data (AAD) is used. plaintext is the same JSON envelope documented per event type in GET /webhook/event-contracts/{event_type} — decrypt first, then apply that contract. Authenticity is provided by the GCM auth tag (decryption fails if the body was tampered with).

Deduplicate retries using event.event_id in the payload envelope — delivery retries can redeliver the same event with the same event_id.

Delivery is attempted up to 3 times per event. Any HTTP 2xx response counts as successful delivery.

Parameters

ParameterInTypeRequiredDescription
X-Correlation-IdheaderstringnoOptional 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

FieldTypeRequiredDescription
feature_idstring (uuid)yesFeature to subscribe (from GET /api/public/v1/webhook/features). Must exist or create fails.
is_enabledbooleannoDefaults to true if omitted.
destination_kindstringyesPublic API accepts only EXTERNAL_WEBHOOK (HTTPS callback with shared secret). INTERNAL destinations are managed administratively.Values: EXTERNAL_WEBHOOK
destination_refstringnoNot used on the public API. Reserved for internal destinations.Can be null.
webhook_urlstring (uri)noRequired when destination_kind = EXTERNAL_WEBHOOK (must use https://).Can be null.
secret_refstringnoRequired when destination_kind = EXTERNAL_WEBHOOK: shared secret used to decrypt AES_GCM payloads (key = SHA256(secret_ref)). See "Webhook encryption" in this operation's description.Can be null.
signature_algostringnoFixed to AES_GCM for EXTERNAL_WEBHOOK. Omitted on create; stored as AES_GCM. HMAC_SHA256 is no longer supported.Can be null.Values: AES_GCM
replay_window_secintegernoReserved for legacy destinations. Not used for EXTERNAL_WEBHOOK AES_GCM deliveries.Can be null.
timeout_msintegernoHTTP timeout for delivery attempts (EXTERNAL_WEBHOOK).Can be null.
max_attemptsintegernoDelivery attempts per event, up to 3.Can be null.
scope_kindstringyes- TENANT: no scope id fields. - ACCOUNT: set scope_account_id only. - OWNER: set scope_owner_type and scope_owner_id only.Values: TENANT, ACCOUNT, OWNER
scope_account_idstring (uuid)noRequired when scope_kind is ACCOUNT. Must be omitted for TENANT and OWNER.Can be null.
scope_owner_typestringnoRequired when scope_kind is OWNER. Allowed values (lowercase) tenant, partner, user, platform, customer. Omit for TENANT and ACCOUNT.Can be null.
scope_owner_idstring (uuid)noRequired when scope_kind is OWNER. Omit for TENANT and ACCOUNT.Can be null.

Request example

external_webhook_tenant

{
  "feature_id": "11111111-1111-1111-1111-111111111111",
  "destination_kind": "EXTERNAL_WEBHOOK",
  "scope_kind": "TENANT",
  "webhook_url": "https://example.com/hooks/tilopay",
  "secret_ref": "whsec_example_shared_secret"
}

Response 201

The resource was created successfully.

FieldTypeRequiredDescription
idstring (uuid)no—
feature_idstring (uuid)no—
is_enabledbooleanno—
destination_kindstringno—
destination_refstringno—
webhook_urlstringno—
secret_refstringno—
signature_algostringno—
replay_window_secintegerno—
timeout_msintegerno—
max_attemptsintegernoDelivery attempts per event, up to 3.
scope_kindstringno—
scope_account_idstring (uuid)no—
scope_owner_typestringno—
scope_owner_idstring (uuid)no—
created_atstringno—
updated_atstringno—

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": {
    "id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "feature_id": "11111111-1111-1111-1111-111111111111",
    "is_enabled": true,
    "destination_kind": "EXTERNAL_WEBHOOK",
    "destination_ref": null,
    "webhook_url": "https://example.com/hooks/tilopay",
    "secret_ref": "whsec_example_shared_secret",
    "signature_algo": "AES_GCM",
    "replay_window_sec": null,
    "timeout_ms": 10000,
    "max_attempts": 8,
    "scope_kind": "TENANT",
    "scope_account_id": null,
    "scope_owner_type": null,
    "scope_owner_id": null,
    "created_at": "2026-01-15T12:00:00Z",
    "updated_at": "2026-01-15T12:00:00Z"
  }
}

Error responses

HTTPresponse_codeDescription
400INVALID_REQUESTInvalid request. Check the required fields and try again.
401UNAUTHORIZEDUnauthorized. Verify your session or credentials.
403FORBIDDENYou do not have permission to perform this action.
500INTERNAL_ERRORAn unexpected error occurred. Please try again later.
503SERVICE_UNAVAILABLEA required service is temporarily unavailable. Please try again later.

List webhook features by payment method

GET /api/public/v1/webhook/features

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

Returns feature IDs and the event_types each feature can emit. Tenant scope is taken from the access token; clients must not send tenant_id.

Naming: event_types use payment.* for resource-level outcomes (payment.succeeded, payment.failed, payment.reversed) and payin.posted for the inbound direction only (funds received). There is no payout.posted and no transaction.* events. The delivered JSON object is always payment.

Inspect payload shapes with GET /api/public/v1/webhook/event-contracts/{event_type} (payin.posted, payment.succeeded, payment.failed, payment.reversed).

Parameters

ParameterInTypeRequiredDescription
X-Correlation-IdheaderstringnoOptional 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.
country_codequerystringno—
payment_method_codequerystringno—
webhook_enabledquerybooleanno—

Response 200

The request was processed successfully.

FieldTypeRequiredDescription
itemsarray<object>no—
items[].idstring (uuid)yes—
items[].event_typesarray<string>yesPublic event types this feature can emit. payment.* = resource-level outcomes (any direction). payin.posted = inbound direction only (funds received). There is no payout.posted and no transaction.*. Inspect payload shapes with GET /api/public/v1/webhook/event-contracts/{event_type}.

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": {
    "items": [
      {
        "id": "11111111-1111-1111-1111-111111111111",
        "event_types": [
          "payin.posted",
          "payment.succeeded",
          "payment.failed",
          "payment.reversed"
        ]
      }
    ]
  }
}

Error responses

HTTPresponse_codeDescription
401UNAUTHORIZEDUnauthorized. Verify your session or credentials.
403FORBIDDENYou do not have permission to perform this action.
500INTERNAL_ERRORAn unexpected error occurred. Please try again later.
503SERVICE_UNAVAILABLEA required service is temporarily unavailable. Please try again later.

Webhook event contract

GET /api/public/v1/webhook/event-contracts/{event_type}

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

Returns the outbound webhook payload contract for one public event type (schema_version 1.0.0). The envelope object is always payment (never payin, payout, or transaction).

When to use each name

  • payment — resource and envelope. Outcome events: payment.succeeded, payment.failed, payment.reversed (both PAYIN and PAYOUT). Correlate with REST /transactions/payments.
  • payin / payout — direction (PAYIN inbound, PAYOUT outbound). payin.posted is inbound-only (ledger posted / funds received). There is no public payout.posted.
  • transaction — REST path prefix only. Not used in webhook event_type or payload objects.

Supported event_type values:

  • payin.posted — inbound (PAYIN) payment reached public status posted. Envelope field is payment. payment.local_payment is a JSON boolean (true/false), not a string.
  • payment.succeeded — the provider confirmed success for a payment (PAYIN or PAYOUT; PIN or SINPE Móvil). Resource-level outcome, not direction-specific.
  • payment.failed — processing ended in a definitive failure (public status=failed), with structured payment.error (domain / platform). When the provider rejected the payment, the envelope also includes rejection. Not used for later reversals.
  • payment.reversed — subsequent reversal of a payment that already progressed (typically status=posted or confirmed). REST result=reversed and has_reversal=true. Processing status is unchanged. Not a payment.failed.

Unknown event_type values (including internal-only payment.status_changed) return 404.

SINPE rejection codes (rejection, only on payment.failed)

When event_type = payment.failed and the provider rejected the payment, the envelope includes rejection (same object and catalog as data.rejection on POST /accounts/validate). rejection.code is one of a fixed set of normalized semantics (see the value table below). Most rows in the table below do NOT get a distinct `code` — only a subset of reason_code values has a dedicated semantic; every other reason_code, even one listed below with a specific message, returns code = SINPE_REJECTED (e.g. reason_code = 21 "fondos insuficientes" still returns SINPE_REJECTED). A reason_code not listed here at all still produces a response — message falls back to a generic text and code falls back to SINPE_REJECTED.

Account / profile / limits

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

Settlement with destination institution

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

Identification

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

Format validation / Core Banking

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

Channel / identification type

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

SINPE Móvil (wallet)

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

Delivery body is JSON. EXTERNAL_WEBHOOK HTTP body is always {"data":"<ciphertext>"} (AES-256-GCM, key = SHA256(secret_ref)). Decrypt to obtain the event envelope documented per GET /webhook/event-contracts/{event_type}; the decrypted plaintext matches that contract.

`rejection.code` values

ValueMeaning
ACCOUNT_CLOSEDAccount closed
ACCOUNT_NOT_FOUNDAccount not found
ACCOUNT_BLOCKEDAccount blocked
INVALID_ACCOUNTInvalid account
IDENT_MISMATCHIdentification does not match
IDENT_TYPE_INVALIDInvalid identification type
INVALID_ID_FORMATInvalid identification format
VALIDATION_ERRORValidation error
CURRENCY_MISMATCHCurrency mismatch
NOT_AUTHORIZED_BY_RECIPIENTNot authorized by the recipient
PROFILE_NOT_ALLOWEDProfile not allowed
CHANNEL_NOT_RECOGNIZEDChannel not recognized
COMMUNICATION_ERRORCommunication error
RETRY_LATERRetry later
WALLET_PHONE_ORIGIN_INVALIDOrigin phone invalid (SINPE Móvil)
WALLET_PHONE_ORIGIN_NOT_ENABLEDOrigin phone without wallet service
WALLET_PHONE_DEST_INVALIDDestination phone invalid (SINPE Móvil)
WALLET_PHONE_NOT_REGISTERED_BCCRPhone not registered in the BCCR mobile registry
WALLET_NOT_FOUNDWallet not found
WALLET_PHONE_ALREADY_ACTIVEPhone already has an active wallet
PHONE_NOT_REGISTEREDPhone not registered
SINPE_REJECTEDRejected by SINPE (generic fallback)

Delivery is attempted up to 3 times per event. Any HTTP 2xx response counts as successful delivery.

Parameters

ParameterInTypeRequiredDescription
X-Correlation-IdheaderstringnoOptional 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.
event_typepathstringyes—Values: payin.posted, payment.succeeded, payment.failed, payment.reversed

Response 200

The request was processed successfully.

FieldTypeRequiredDescription
event_typestringyes—Values: payin.posted, payment.succeeded, payment.failed, payment.reversed
schema_versionstringyes—
descriptionstringyes—
exampleobjectyes—

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": {
    "event_type": "payment.succeeded",
    "schema_version": "1.0.0",
    "description": "Envelope delivered for `payment.succeeded` (decrypt AES_GCM `data` field first).",
    "example": {
      "event_id": "evt_01JEXAMPLE",
      "event_type": "payment.succeeded",
      "schema_version": "1.0.0"
    },
    "x-description-es": "Sobre entregado para `payment.succeeded` (descifrá primero el campo `data` AES_GCM)."
  }
}

Error responses

HTTPresponse_codeDescription
401UNAUTHORIZEDUnauthorized. Verify your session or credentials.
403FORBIDDENYou do not have permission to perform this action.
404NOT_FOUNDThe requested resource was not found.
500INTERNAL_ERRORAn unexpected error occurred. Please try again later.
503SERVICE_UNAVAILABLEA required service is temporarily unavailable. Please try again later.

Payload contracts#

Each event has its own data structure. This is what we send you in each one:

payin.posted

FieldTypeRequiredDescription
schema_versionstringyes—
eventobjectyes—
event.event_idstring (uuid)yes—
event.event_typestringyes—Values: payin.posted
event.occurred_atstring (date-time)yesRFC 3339 timestamp in UTC with second precision and a Z suffix. Fractional seconds are never emitted. Example: 2026-01-15T12:00:00Z.
event.correlation_idstringyes—
contextobjectyes—
context.country_codestringyes—
context.payment_method_codestringyesPublic payment method catalog. Same values as REST (PIN, SINPE_MOVIL). Internal codes are never exposed.Values: PIN, SINPE_MOVIL
paymentobjectyes—
payment.payment_idstring (uuid)yes—
payment.public_idinteger (int64)yesThe same identifier is a number in webhooks and text in REST. Always treat it as text.
payment.statusstringyesPublic 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
payment.amountobjectyes—
payment.amount.amountstringyesAmount as a decimal string with exactly 2 fraction digits (e.g. "1000.00").
payment.amount.currencystringyesISO 4217 currency of the payment.Values: CRC, USD
payment.posted_atstring (date-time)yesRFC 3339 timestamp in UTC with second precision and a Z suffix. Fractional seconds are never emitted. Example: 2026-01-15T12:00:00Z.
payment.accountobjectyes—
payment.account.typestringyes—
payment.account.valuestringyes—
payment.referenceobjectyes—
payment.reference.client_referencestringyesPartner reference sent at payment creation (client_reference), not detail_reference.
payment.reference.external_referencestringyes—
payment.destination_phone_numberstringno—
payment.local_paymentbooleannoJSON boolean (true/false), not the strings "true"/"false".
payment.origin_client_namestringno—
providerobjectyesShared 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_idstringyesProvider 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_atstring (date-time)yesRFC 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_codestringyesRaw 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_descstringyesProvider Motivo text (prefers Detalle). Same meaning as webhook rejection.message. null when there is no provider rejection.Can be null.
provider.provider_status_semanticstringyesNormalized platform semantic (e.g. ACCOUNT_BLOCKED). Same meaning as webhook rejection.code. null when there is no provider rejection.Can be null.

payment.succeeded

FieldTypeRequiredDescription
schema_versionstringyes—
eventobjectyes—
event.event_idstring (uuid)yes—
event.event_typestringyes—Values: payment.succeeded
event.occurred_atstring (date-time)yesRFC 3339 timestamp in UTC with second precision and a Z suffix. Fractional seconds are never emitted. Example: 2026-01-15T12:00:00Z.
event.correlation_idstringyes—
contextobjectyes—
context.country_codestringyes—
context.payment_method_codestringyesPublic payment method catalog. Same values as REST (PIN, SINPE_MOVIL). Internal codes are never exposed.Values: PIN, SINPE_MOVIL
paymentobjectyes—
payment.payment_idstring (uuid)yes—
payment.public_idinteger (int64)yesThe same identifier is a number in webhooks and text in REST. Always treat it as text.
payment.statusstringyesPublic 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
payment.amountobjectyes—
payment.amount.amountstringyesAmount as a decimal string with exactly 2 fraction digits (e.g. "1000.00").
payment.amount.currencystringyesISO 4217 currency of the payment.Values: CRC, USD
payment.client_referencestringyesPartner reference sent at payment creation (client_reference), not detail_reference.
payment.external_referencestringyes—
payment.fee_totalstringyesFee total as decimal string (same currency as amount.currency)
payment.tax_totalstringyesTax total as decimal string (same currency as amount.currency)
payment.net_amountstringyesNet cash impact on the merchant account. PAYIN: amount - fee_total - tax_total (what the merchant receives). PAYOUT: amount + fee_total + tax_total (total debit; fee is charged separately and is not deducted from the transferred amount).
payment.succeeded_atstring (date-time)yesTimestamp when the provider confirmed the payment succeeded.
providerobjectyesShared 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_idstringyesProvider 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_atstring (date-time)yesRFC 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_codestringyesRaw 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_descstringyesProvider Motivo text (prefers Detalle). Same meaning as webhook rejection.message. null when there is no provider rejection.Can be null.
provider.provider_status_semanticstringyesNormalized platform semantic (e.g. ACCOUNT_BLOCKED). Same meaning as webhook rejection.code. null when there is no provider rejection.Can be null.

payment.failed

FieldTypeRequiredDescription
schema_versionstringyes—
eventobjectyes—
event.event_idstring (uuid)yes—
event.event_typestringyes—Values: payment.failed
event.occurred_atstring (date-time)yesRFC 3339 timestamp in UTC with second precision and a Z suffix. Fractional seconds are never emitted. Example: 2026-01-15T12:00:00Z.
event.correlation_idstringyes—
contextobjectyes—
context.country_codestringyes—
context.payment_method_codestringyesPublic payment method catalog. Same values as REST (PIN, SINPE_MOVIL). Internal codes are never exposed.Values: PIN, SINPE_MOVIL
paymentobjectyes—
payment.payment_idstring (uuid)yes—
payment.public_idinteger (int64)yesThe same identifier is a number in webhooks and text in REST. Always treat it as text.
payment.statusstringyesPublic 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
payment.amountobjectyes—
payment.amount.amountstringyesAmount as a decimal string with exactly 2 fraction digits (e.g. "1000.00").
payment.amount.currencystringyesISO 4217 currency of the payment.Values: CRC, USD
payment.client_referencestringnoPartner reference sent at payment creation (client_reference), not detail_reference.
payment.external_referencestringno—
payment.errorobjectyes—
payment.error.domainstringyes—Values: provider_rejection, platform_validation, platform_posting, platform_infra, platform_reversal, sinpe_rejection
payment.error.platformobjectyes—
providerobjectyesShared 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_idstringyesProvider 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_atstring (date-time)yesRFC 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_codestringyesRaw 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_descstringyesProvider Motivo text (prefers Detalle). Same meaning as webhook rejection.message. null when there is no provider rejection.Can be null.
provider.provider_status_semanticstringyesNormalized platform semantic (e.g. ACCOUNT_BLOCKED). Same meaning as webhook rejection.code. null when there is no provider rejection.Can be null.
rejectionobjectnoRail-agnostic provider rejection. Shared by POST /accounts/validate (data.rejection) and the payment.failed webhook (rejection). Object only when the provider rejected the operation; omitted/null otherwise.
rejection.codestringyesNormalized platform semantic derived from reason_code. Only a subset of reason_code values (see the catalog in the description of POST /accounts/validate and of the payment.failed webhook contract endpoint) has a distinct entry here — for any other numeric reason_code, code is SINPE_REJECTED even when message is specific (e.g. reason_code = 21 "fondos insuficientes" still returns code = SINPE_REJECTED, it has no dedicated semantic). PHONE_NOT_REGISTERED is the fallback when reason_code is non-numeric (SINPE_MOVIL wallet flow).Values: ACCOUNT_CLOSED, ACCOUNT_NOT_FOUND, ACCOUNT_BLOCKED, INVALID_ACCOUNT, IDENT_MISMATCH, IDENT_TYPE_INVALID, INVALID_ID_FORMAT, VALIDATION_ERROR, CURRENCY_MISMATCH, NOT_AUTHORIZED_BY_RECIPIENT, PROFILE_NOT_ALLOWED, CHANNEL_NOT_RECOGNIZED, COMMUNICATION_ERROR, RETRY_LATER, WALLET_PHONE_ORIGIN_INVALID, WALLET_PHONE_ORIGIN_NOT_ENABLED, WALLET_PHONE_DEST_INVALID, WALLET_PHONE_NOT_REGISTERED_BCCR, WALLET_NOT_FOUND, WALLET_PHONE_ALREADY_ACTIVE, PHONE_NOT_REGISTERED, SINPE_REJECTED
rejection.reason_codestringyesRaw Motivo/code returned by SINPE. See the endpoint description of POST /accounts/validate for the full code catalog (shared by this object wherever it appears, including the payment.failed webhook). A code not listed there still produces a response — message falls back to a generic text and code falls back to SINPE_REJECTED.
rejection.messagestringyesDescriptive Motivo text; prefers provider Detalle when present.

payment.reversed

FieldTypeRequiredDescription
schema_versionstringyes—
eventobjectyes—
event.event_idstring (uuid)yes—
event.event_typestringyes—Values: payment.reversed
event.occurred_atstring (date-time)yesRFC 3339 timestamp in UTC with second precision and a Z suffix. Fractional seconds are never emitted. Example: 2026-01-15T12:00:00Z.
event.correlation_idstringyes—
contextobjectyes—
context.country_codestringyes—
context.payment_method_codestringyesPublic payment method catalog. Same values as REST (PIN, SINPE_MOVIL). Internal codes are never exposed.Values: PIN, SINPE_MOVIL
paymentobjectyes—
payment.payment_idstring (uuid)yes—
payment.public_idinteger (int64)yesThe same identifier is a number in webhooks and text in REST. Always treat it as text.
payment.statusstringyesPublic 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
payment.resultstringyes—Values: reversed
payment.has_reversalbooleanyes—Values: true
payment.amountobjectyes—
payment.amount.amountstringyesAmount as a decimal string with exactly 2 fraction digits (e.g. "1000.00").
payment.amount.currencystringyesISO 4217 currency of the payment.Values: CRC, USD
payment.client_referencestringno—
payment.external_referencestringno—
payment.reversed_atstring (date-time)yesRFC 3339 timestamp in UTC with second precision and a Z suffix. Fractional seconds are never emitted. Example: 2026-01-15T12:00:00Z.
payment.reasonobjectno—
payment.reason.codestringno—
payment.reason.messagestringno—
providerobjectyesShared 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_idstringyesProvider 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_atstring (date-time)yesRFC 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_codestringyesRaw 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_descstringyesProvider Motivo text (prefers Detalle). Same meaning as webhook rejection.message. null when there is no provider rejection.Can be null.
provider.provider_status_semanticstringyesNormalized platform semantic (e.g. ACCOUNT_BLOCKED). Same meaning as webhook rejection.code. null when there is no provider rejection.Can be null.

Public identifier#

payment.public_id is a number in the webhook and text in REST; it is the same identifier. Always treat it as text.

Last verified: 2026-10-22

View as raw Markdown