Webhooks del API Bancario
API 1.0.0El grupo Webhooks permite registrar el destino HTTPS donde recibís los eventos, ver qué
features podés suscribir por método de pago y consultar el contrato de payload de cada
evento antes de escribir el receptor.
POST /api/public/v1/webhook/destination
GET /api/public/v1/webhook/features
GET /api/public/v1/webhook/event-contracts/{event_type}Eventos#
Nuestro API te notifica cuatro tipos de evento:
| Evento | Cuándo llega |
|---|---|
payin.posted | Se acreditó un movimiento de entrada. |
payment.succeeded | El pago terminó de forma exitosa. |
payment.failed | El pago terminó con falla. |
payment.reversed | El pago fue revertido. |
Un pago revertido conserva el estado posted y se distingue por result: reversed y
has_reversal: true; ver estados del pago.
Buenas prácticas del receptor#
La entrega se intenta hasta 3 veces por evento y se considera exitosa con cualquier HTTP 2xx. Respondé 2xx en cuanto recibís el evento y procesá aparte: un receptor lento provoca
reenvíos. Tratá los eventos como potencialmente repetidos y deduplicá por event.event_id
del sobre (los reintentos reenvían el mismo evento con el mismo event_id). Y no confíes el estado final sólo al webhook: confirmalo con
la consulta del pago.
Operaciones#
Crear destino de webhook
POST /api/public/v1/webhook/destination
Host: https://api-baas-sandbox.tilopay.com — requiere Authorization: Bearer <access_token>.
Los campos válidos dependen de destination_kind y scope_kind. Enviá solo los campos que aplican a tu combinación; los demás campos de alcance deben omitirse (no como marcadores null).
Payloads de eventos Inspeccioná los contratos salientes con GET /api/public/v1/webhook/event-contracts/{event_type} (payin.posted, payment.succeeded, payment.failed, payment.reversed).
destination_kind
EXTERNAL_WEBHOOK(único tipo disponible en el API pública): requierewebhook_url(debe empezar conhttps://) ysecret_ref. Ajustes opcionales de entrega (timeout_ms,max_attempts). Los payloads siempre van cifrados conAES_GCMusandosecret_ref; el cuerpo HTTP es{"data":"<ciphertext>"}.INTERNALestá reservado para la administración de la plataforma y no puede crearse por este endpoint.
scope_kind
TENANT: omitíscope_account_id,scope_owner_typeyscope_owner_id.ACCOUNT: enviáscope_account_id; omití los campos de owner.OWNER: enviáscope_owner_typeyscope_owner_id; omitíscope_account_id.scope_owner_typees uno detenant,partner,user,platform,customer(en minúsculas en el payload).
Cifrado del webhook (EXTERNAL_WEBHOOK)
Las entregas siempre envían Content-Type: application/json y Accept: application/json.
El cuerpo HTTP siempre es {"data": "<base64>"}. Los bytes decodificados de base64 son:
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 se usan datos autenticados adicionales (AAD). plaintext es el mismo sobre JSON documentado por tipo de evento en GET /webhook/event-contracts/{event_type}: descifrá primero y luego aplicá ese contrato. La autenticidad la da la etiqueta de autenticación GCM (el descifrado falla si el cuerpo fue alterado).
Deduplicá los reintentos con event.event_id del sobre del payload: los reintentos de entrega pueden reenviar el mismo evento con el mismo event_id.
La entrega se intenta hasta 3 veces por evento. Cualquier respuesta HTTP 2xx se considera exitosa.
Parámetros
| Parámetro | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
X-Correlation-Id | header | string | no | Id de correlación opcional que enviás para trazar la solicitud de punta a punta.
Se devuelve como correlation_id en el sobre de respuesta. Si lo omitís,
la API genera uno y igual lo devuelve. |
Cuerpo del request
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
feature_id | string (uuid) | sí | Función a suscribir (de GET /api/public/v1/webhook/features). Debe existir o la creación falla. |
is_enabled | boolean | no | Por defecto true si se omite. |
destination_kind | string | sí | La API pública solo acepta EXTERNAL_WEBHOOK (callback HTTPS con secreto compartido). Los destinos INTERNAL se administran internamente.Valores: EXTERNAL_WEBHOOK |
destination_ref | string | no | No se usa en la API pública. Reservado para destinos internos.Puede ser null. |
webhook_url | string (uri) | no | Obligatorio cuando destination_kind = EXTERNAL_WEBHOOK (debe usar https://).Puede ser null. |
secret_ref | string | no | Obligatorio cuando destination_kind = EXTERNAL_WEBHOOK: secreto compartido para descifrar los payloads AES_GCM (key = SHA256(secret_ref)).
Ver "Cifrado del webhook" en la descripción de esta operación.Puede ser null. |
signature_algo | string | no | Fijo en AES_GCM para EXTERNAL_WEBHOOK. Se omite al crear; se guarda como AES_GCM.
HMAC_SHA256 ya no se admite.Puede ser null.Valores: AES_GCM |
replay_window_sec | integer | no | Reservado para destinos históricos. No se usa en entregas AES_GCM de EXTERNAL_WEBHOOK.Puede ser null. |
timeout_ms | integer | no | Timeout HTTP de los intentos de entrega (EXTERNAL_WEBHOOK).Puede ser null. |
max_attempts | integer | no | Intentos de entrega por evento, hasta 3.Puede ser null. |
scope_kind | string | sí | - TENANT: sin campos de id de alcance.
- ACCOUNT: enviá solo scope_account_id.
- OWNER: enviá solo scope_owner_type y scope_owner_id.Valores: TENANT, ACCOUNT, OWNER |
scope_account_id | string (uuid) | no | Obligatorio cuando scope_kind es ACCOUNT. Debe omitirse para TENANT y OWNER.Puede ser null. |
scope_owner_type | string | no | Obligatorio cuando scope_kind es OWNER. Valores permitidos (en minúsculas): tenant, partner, user, platform, customer. Se omite para TENANT y ACCOUNT.Puede ser null. |
scope_owner_id | string (uuid) | no | Obligatorio cuando scope_kind es OWNER. Se omite para TENANT y ACCOUNT.Puede ser null. |
Ejemplo de request
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"
}Respuesta 201
El recurso se creó correctamente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
id | string (uuid) | no | — |
feature_id | string (uuid) | no | — |
is_enabled | boolean | no | — |
destination_kind | string | no | — |
destination_ref | string | no | — |
webhook_url | string | no | — |
secret_ref | string | no | — |
signature_algo | string | no | — |
replay_window_sec | integer | no | — |
timeout_ms | integer | no | — |
max_attempts | integer | no | Intentos de entrega por evento, hasta 3. |
scope_kind | string | no | — |
scope_account_id | string (uuid) | no | — |
scope_owner_type | string | no | — |
scope_owner_id | string (uuid) | no | — |
created_at | string | no | — |
updated_at | string | no | — |
Ejemplo de respuesta
{
"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"
}
}Respuestas de error
| HTTP | response_code | Descripción |
|---|---|---|
| 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. |
| 500 | INTERNAL_ERROR | An unexpected error occurred. Please try again later. |
| 503 | SERVICE_UNAVAILABLE | A required service is temporarily unavailable. Please try again later. |
Listar funciones de webhook por método de pago
GET /api/public/v1/webhook/features
Host: https://api-baas-sandbox.tilopay.com — requiere Authorization: Bearer <access_token>.
Devuelve los IDs de función y los event_types que cada una puede emitir. El alcance del comercio se toma del token de acceso; no enviés tenant_id.
Nombres: los event_types usan payment.* para resultados a nivel de recurso (payment.succeeded, payment.failed, payment.reversed) y payin.posted solo para la dirección entrante (fondos recibidos). No existe payout.posted ni eventos transaction.*. El objeto JSON entregado siempre es payment.
Inspeccioná las formas de payload con GET /api/public/v1/webhook/event-contracts/{event_type} (payin.posted, payment.succeeded, payment.failed, payment.reversed).
Parámetros
| Parámetro | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
X-Correlation-Id | header | string | no | Id de correlación opcional que enviás para trazar la solicitud de punta a punta.
Se devuelve como correlation_id en el sobre de respuesta. Si lo omitís,
la API genera uno y igual lo devuelve. |
country_code | query | string | no | — |
payment_method_code | query | string | no | — |
webhook_enabled | query | boolean | no | — |
Respuesta 200
La solicitud se procesó correctamente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
items | array<object> | no | — |
items[].id | string (uuid) | sí | — |
items[].event_types | array<string> | sí | Tipos de evento públicos que esta función puede emitir. payment.* = resultados
a nivel de recurso (cualquier dirección). payin.posted = solo dirección entrante
(fondos recibidos). No existe payout.posted ni transaction.*.
Inspeccioná las formas de payload con GET /api/public/v1/webhook/event-contracts/{event_type}. |
Ejemplo de respuesta
{
"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"
]
}
]
}
}Respuestas de error
| HTTP | response_code | Descripción |
|---|---|---|
| 401 | UNAUTHORIZED | Unauthorized. Verify your session or credentials. |
| 403 | FORBIDDEN | You do not have permission to perform this action. |
| 500 | INTERNAL_ERROR | An unexpected error occurred. Please try again later. |
| 503 | SERVICE_UNAVAILABLE | A required service is temporarily unavailable. Please try again later. |
Contrato de evento de webhook
GET /api/public/v1/webhook/event-contracts/{event_type}
Host: https://api-baas-sandbox.tilopay.com — requiere Authorization: Bearer <access_token>.
Devuelve el contrato del payload saliente de webhook para un tipo de evento público (schema_version 1.0.0). El objeto del sobre siempre es payment (nunca payin, payout o transaction).
Cuándo usar cada nombre
- payment — recurso y sobre. Eventos de resultado:
payment.succeeded,payment.failed,payment.reversed(tanto PAYIN como PAYOUT). Se correlaciona con/transactions/paymentsde REST. - payin / payout — dirección (
PAYINentrante,PAYOUTsaliente).payin.postedes solo entrante (registrado en el ledger / fondos recibidos). No existe unpayout.postedpúblico. - transaction — solo prefijo de ruta REST. No se usa en
event_typeni en los objetos del payload.
Valores de event_type admitidos:
payin.posted— un pago entrante (PAYIN) llegó al estado públicoposted. El campo del sobre espayment.payment.local_paymentes un booleano JSON (true/false), no una cadena.payment.succeeded— el proveedor confirmó el éxito de un pago (PAYIN o PAYOUT; PIN o SINPE Móvil). Resultado a nivel de recurso, no específico de dirección.payment.failed— el procesamiento terminó en un fallo definitivo (status=failedpúblico), conpayment.errorestructurado (domain/platform). Cuando el proveedor rechazó el pago, el sobre también incluyerejection. No se usa para reversos posteriores.payment.reversed— reverso posterior de un pago que ya había avanzado (típicamentestatus=postedoconfirmed). RESTresult=reversedyhas_reversal=true. Elstatusde procesamiento no cambia. No es unpayment.failed.
Los valores de event_type desconocidos (incluido el interno payment.status_changed) devuelven 404.
Códigos de rechazo SINPE (rejection, solo en payment.failed)
Cuando event_type = payment.failed y el proveedor rechazó el pago, el sobre incluye rejection (mismo objeto y catálogo que data.rejection en POST /accounts/validate). rejection.code es uno de un conjunto fijo de semánticas normalizadas (ver la tabla de valores más abajo). La mayoría de filas de la tabla NO tienen un `code` propio: solo un subconjunto de valores de reason_code tiene una semántica dedicada; cualquier otro reason_code, incluso uno listado abajo con un message específico, devuelve code = SINPE_REJECTED (ej. reason_code = 21 "fondos insuficientes" igual devuelve SINPE_REJECTED). Un reason_code que no aparece aquí igual produce respuesta: message cae a un texto genérico y code a SINPE_REJECTED.
Cuenta / perfil / límites
| Code | Message |
|---|---|
| 21 | Cuenta Cliente con fondos insuficientes |
| 22 | Cuenta Cliente no admite créditos |
| 23 | Cuenta Cliente cerrada |
| 24 | Cuenta Cliente inactiva |
| 25 | Cuenta Cliente no admite débitos |
| 26 | Cuenta Cliente no es de fondos |
| 27 | Moneda de la Cuenta Cliente no corresponde |
| 28 | Cuenta cliente no existe |
| 29 | Cuenta Cliente no registrada en el SINPE |
| 30 | Cuenta Cliente no habilitada para el servicio |
| 31 | Cuenta Cliente bloqueada |
| 32 | Id cliente destino no coincide con registrado en la entidad |
| 33 | Nombre del cliente destino no coincide con el registrado en la entidad |
| 34 | Cuenta Cliente en proceso de cierre |
| 35 | Cuenta Cliente embargada |
| 36 | Cuenta Cliente con retención judicial |
| 37 | Cuenta de expediente simplificado no permite el monto indicado |
| 38 | Límite transaccional de la Cuenta Cliente excedido |
| 39 | Cuenta Cliente incorrecta |
| 40 | IBAN de la cuenta destino inválido |
| 41 | IBAN de la cuenta origen inválido |
| 42 | Tipo de cuenta no permite la transacción |
| 43 | Cuenta Cliente no pertenece a la entidad indicada |
| 44 | Producto de la cuenta no admite el servicio |
| 45 | Cuenta Cliente en estado de cancelación |
| 46 | Cuenta Cliente restringida por política de la entidad |
| 47 | Cuenta Cliente no permite pagos inmediatos |
| 48 | Cuenta Cliente no permite SINPE Móvil |
| 49 | Titular de la cuenta destino fallecido |
| 50 | Cuenta Cliente consolidada o migrada |
| 51 | Identificación del cliente origen no coincide |
| 52 | Identificación del cliente destino inválida |
| 53 | Cliente destino no autorizado para recibir el pago |
| 54 | Cliente origen no autorizado para enviar el pago |
| 55 | Perfil del cliente origen no permite la transacción |
| 56 | Monto inferior al mínimo permitido |
| 57 | Monto superior al máximo permitido |
| 58 | Cantidad de transacciones diarias excedida |
| 59 | Cantidad de transacciones mensuales excedida |
| 60 | Límite acumulado diario excedido |
| 61 | Límite acumulado mensual excedido |
| 62 | Comisión no pudo ser aplicada |
| 63 | Tipo de cambio no disponible |
| 64 | Transacción rechazada por control de lavado de dinero |
| 65 | Transacción rechazada por listas de control |
| 66 | Transacción en revisión de cumplimiento |
| 67 | Documento de respaldo requerido no presente |
| 68 | Firma o autenticación inválida |
| 69 | Token o segundo factor inválido |
| 70 | Sesión de usuario expirada |
| 71 | Usuario no autorizado para el canal |
| 72 | Dispositivo no registrado |
| 73 | Geolocalización no permitida |
| 74 | Operación no soportada en la moneda indicada |
| 75 | Operación no soportada para el tipo de cliente |
| 76 | Problemas de comunicación |
| 77 | Tiempo de espera agotado en la entidad origen |
| 78 | Tiempo de espera agotado en el SINPE |
| 79 | Error interno de la entidad origen |
| 80 | Error interno de la entidad destino |
| 81 | Entidad origen no disponible |
| 82 | Entidad destino no encontrada |
| 83 | Problemas en la respuesta del destino |
| 84 | Respuesta de la entidad origen incorrecta |
| 85 | Mensaje con formato electrónico inválido |
| 86 | Versión del estándar electrónico no soportada |
| 87 | Campo obligatorio no informado |
| 88 | Campo con valor fuera de catálogo |
| 89 | Checksum o integridad del mensaje inválida |
| 90 | Referencia SINPE duplicada |
| 91 | Moneda no corresponde |
| 92 | Transacción no autorizada por entidad destino |
| 93 | Transacción no autorizada por cliente destino |
| 94 | Transacción no autorizada por entidad origen |
| 95 | Transacción no autorizada por cliente origen |
| 96 | Reverso no permitido para el estado de la transacción |
| 97 | Reverso ya aplicado |
| 98 | Confirmación no permitida para el estado de la transacción |
| 99 | Liquidación no permitida para el estado de la transacción |
| 100 | Entidad Destino no disponible para procesar en tiempo real |
| 101 | Entidad origen no disponible para procesar en tiempo real |
| 102 | Servicio PIN no habilitado para la entidad destino |
| 103 | Servicio PIN no habilitado para la entidad origen |
| 104 | Código de entidad destino inválido |
| 105 | Código de entidad origen inválido |
| 106 | Código de país de la entidad destino inválido |
| 107 | Código de país de la entidad origen inválido |
| 108 | Número de referencia interna inválido |
| 109 | Número de referencia SINPE inválido |
| 110 | Transacción no se encuentra en un estado que permita la consulta |
Compensación con entidad destino
| Code | Message |
|---|---|
| 201 | Tiempo respuesta excedido por la entidad destino |
| 202 | Respuesta de la entidad destino incorrecta según el estándar electrónico |
| 203 | Se recibió una excepción de la entidad destino |
| 204 | Error de comunicación con la entidad destino |
| 205 | Falló procesamiento en el SINPE |
| 206 | Transacción no autorizada por cliente destino |
| 207 | Perfil transaccional del cliente destino no permite recibir el pago |
| 208 | Operación no autorizada entre las entidades participantes |
| 209 | Pendientes de Cumplimiento |
| 210 | Conciliación de la transacción no fue posible |
Identificación
| Code | Message |
|---|---|
| 801 | Identificación inválida |
| 802 | Identificación del cliente origen no encontrada |
| 803 | Identificación del cliente destino no encontrada |
| 804 | Identificación no vigente |
| 805 | Identificación vencida |
| 806 | Identificación no corresponde al tipo indicado |
| 807 | Tipo de identificación inválido |
| 808 | País de la identificación no soportado |
| 809 | Identificación de menor de edad no permitida |
| 810 | Identificación de persona jurídica no permitida para el servicio |
Validación de formato / Core Bancario
| Code | Message |
|---|---|
| 1002 | El Id de cliente destino no cumple con el formato esperado por el SINPE |
| 1003 | El Id de cliente origen no fue informado |
| 1004 | El Id de cliente origen no cumple con el formato esperado por el SINPE |
| 1005 | Monto con formato inválido |
| 1006 | Moneda con formato inválido |
| 1007 | Problemas de comunicación con el Core Bancario |
| 1008 | Core Bancario no disponible |
| 1009 | El valor para el campo no puede ser nulo o infringir su longitud mínima o máxima |
| 1010 | El valor para el campo no corresponde al tipo de dato esperado |
| 1011 | El valor para el campo no corresponde al catálogo permitido |
| 1012 | Fecha con formato inválido |
| 1013 | Hora con formato inválido |
| 1014 | Número de referencia con formato inválido |
| 1015 | IBAN con formato inválido |
Canal / tipo de identificación
| Code | Message |
|---|---|
| 1040 | Canal no informado |
| 1041 | Canal inválido |
| 1042 | Canal no corresponde |
| 1043 | Canal no habilitado para la entidad |
| 1044 | Canal no habilitado para el servicio |
| 1045 | El formato de la identificación es inválido |
| 1046 | Tipo de identificación no informado |
| 1080 | Tipo de identificación no corresponde al cliente origen |
| 1081 | Tipo de identificación no corresponde al cliente destino |
| 1082 | Tipo de identificación no vigente |
| 1083 | Tipo de identificación no soportado por el servicio |
| 1084 | Tipo de identificación inválido |
| 1085 | Tipo de identificación no soportado por la entidad |
SINPE Móvil (monedero)
| Code | Message |
|---|---|
| 15300 | El número de teléfono origen indicado es inválido |
| 15301 | El número de teléfono origen no tiene activo el Servicio Monedero |
| 15302 | El número de teléfono destino indicado es inválido |
| 15303 | El número de teléfono destino no está registrado en el padrón móvil del BCCR |
| 15304 | No es posible inactivar el monedero indicado pues no existe |
| 15305 | El número de teléfono indicado ya se encuentra activo como monedero en el padrón local |
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.
Valores de `rejection.code`
| Valor | Significado |
|---|---|
ACCOUNT_CLOSED | Cuenta cerrada |
ACCOUNT_NOT_FOUND | Cuenta no encontrada |
ACCOUNT_BLOCKED | Cuenta bloqueada |
INVALID_ACCOUNT | Cuenta inválida |
IDENT_MISMATCH | La identificación no coincide |
IDENT_TYPE_INVALID | Tipo de identificación inválido |
INVALID_ID_FORMAT | Formato de identificación inválido |
VALIDATION_ERROR | Error de validación |
CURRENCY_MISMATCH | La moneda no corresponde |
NOT_AUTHORIZED_BY_RECIPIENT | No autorizado por el destinatario |
PROFILE_NOT_ALLOWED | Perfil no permitido |
CHANNEL_NOT_RECOGNIZED | Canal no reconocido |
COMMUNICATION_ERROR | Error de comunicación |
RETRY_LATER | Reintentar más tarde |
WALLET_PHONE_ORIGIN_INVALID | Teléfono origen inválido (SINPE Móvil) |
WALLET_PHONE_ORIGIN_NOT_ENABLED | Teléfono origen sin servicio de monedero |
WALLET_PHONE_DEST_INVALID | Teléfono destino inválido (SINPE Móvil) |
WALLET_PHONE_NOT_REGISTERED_BCCR | Teléfono no registrado en el padrón móvil del BCCR |
WALLET_NOT_FOUND | Monedero no encontrado |
WALLET_PHONE_ALREADY_ACTIVE | El teléfono ya tiene un monedero activo |
PHONE_NOT_REGISTERED | Teléfono no registrado |
SINPE_REJECTED | Rechazado por SINPE (valor genérico de respaldo) |
La entrega se intenta hasta 3 veces por evento. Cualquier respuesta HTTP 2xx se considera exitosa.
Parámetros
| Parámetro | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
X-Correlation-Id | header | string | no | Id de correlación opcional que enviás para trazar la solicitud de punta a punta.
Se devuelve como correlation_id en el sobre de respuesta. Si lo omitís,
la API genera uno y igual lo devuelve. |
event_type | path | string | sí | —Valores: payin.posted, payment.succeeded, payment.failed, payment.reversed |
Respuesta 200
La solicitud se procesó correctamente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
event_type | string | sí | —Valores: payin.posted, payment.succeeded, payment.failed, payment.reversed |
schema_version | string | sí | — |
description | string | sí | — |
example | object | sí | — |
Ejemplo de respuesta
{
"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)."
}
}Respuestas de error
| HTTP | response_code | Descripción |
|---|---|---|
| 401 | UNAUTHORIZED | Unauthorized. Verify your session or credentials. |
| 403 | FORBIDDEN | You do not have permission to perform this action. |
| 404 | NOT_FOUND | The requested resource was not found. |
| 500 | INTERNAL_ERROR | An unexpected error occurred. Please try again later. |
| 503 | SERVICE_UNAVAILABLE | A required service is temporarily unavailable. Please try again later. |
Contratos de payload#
Cada evento tiene su propia estructura de datos. Esto es lo que te enviamos en cada uno:
payin.posted
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
schema_version | string | sí | — |
event | object | sí | — |
event.event_id | string (uuid) | sí | — |
event.event_type | string | sí | —Valores: payin.posted |
event.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
event.correlation_id | string | sí | — |
context | object | sí | — |
context.country_code | string | sí | — |
context.payment_method_code | string | sí | Catálogo público de métodos de pago. Mismos valores que en REST (PIN, SINPE_MOVIL). Nunca se exponen códigos internos.Valores: PIN, SINPE_MOVIL |
payment | object | sí | — |
payment.payment_id | string (uuid) | sí | — |
payment.public_id | integer (int64) | sí | Es el mismo identificador: número en webhooks y texto en REST. Tratalo siempre como texto. |
payment.status | string | sí | Catálogo público de estados del pago. Siempre en minúsculas. Mismos valores en REST
(crear, listar, obtener) y en webhooks (payment.status).
Los estados internos del ciclo de vida se consolidan:
- pending: iniciado, validado, aceptado
- processing: procesando, pendiente de procesamiento, registrando
- confirmed: confirmado por el riel, aún no registrado en el ledger
- posted: registrado en el ledger (se mantiene posted tras un reverso posterior)
- failed: fallido, rechazado, posting_failed, cancelado, expirado
Un reverso no es un estado. Usá result=reversed / has_reversal=true en REST
y el webhook payment.reversed.
El detalle operativo queda en status_detail (nombre interno en mayúsculas)
en crear/listar/obtener de REST. El webhook payment.failed usa error.platform.code
para distinguir fallos de registro de rechazos del proveedor.Valores: pending, processing, confirmed, posted, failed |
payment.amount | object | sí | — |
payment.amount.amount | string | sí | Monto como texto decimal con exactamente 2 decimales (ej. "1000.00"). |
payment.amount.currency | string | sí | Moneda ISO 4217 del pago.Valores: CRC, USD |
payment.posted_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
payment.account | object | sí | — |
payment.account.type | string | sí | — |
payment.account.value | string | sí | — |
payment.reference | object | sí | — |
payment.reference.client_reference | string | sí | Referencia del comercio enviada al crear el pago (client_reference), no detail_reference. |
payment.reference.external_reference | string | sí | — |
payment.destination_phone_number | string | no | — |
payment.local_payment | boolean | no | Booleano JSON (true/false), no las cadenas "true"/"false". |
payment.origin_client_name | string | no | — |
provider | object | sí | Foto compartida del proveedor en las lecturas REST de pagos y en los sobres de webhook.
La referencia del riel solo está en payment.external_reference, no se duplica aquí.
Los provider_status_* coinciden con rejection.reason_code / message / code del webhook
cuando el proveedor rechazó la operación. null cuando no hay rechazo. |
provider.correlation_id | string | sí | 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.Puede ser null. |
provider.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
provider.provider_status_code | string | sí | Motivo/código crudo del proveedor (ej. 31). Mismo significado que
rejection.reason_code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_desc | string | sí | Texto Motivo del proveedor (prefiere Detalle). Mismo significado que
rejection.message del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_semantic | string | sí | Semántica normalizada de la plataforma (ej. ACCOUNT_BLOCKED). Mismo significado que
rejection.code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
payment.succeeded
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
schema_version | string | sí | — |
event | object | sí | — |
event.event_id | string (uuid) | sí | — |
event.event_type | string | sí | —Valores: payment.succeeded |
event.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
event.correlation_id | string | sí | — |
context | object | sí | — |
context.country_code | string | sí | — |
context.payment_method_code | string | sí | Catálogo público de métodos de pago. Mismos valores que en REST (PIN, SINPE_MOVIL). Nunca se exponen códigos internos.Valores: PIN, SINPE_MOVIL |
payment | object | sí | — |
payment.payment_id | string (uuid) | sí | — |
payment.public_id | integer (int64) | sí | Es el mismo identificador: número en webhooks y texto en REST. Tratalo siempre como texto. |
payment.status | string | sí | Catálogo público de estados del pago. Siempre en minúsculas. Mismos valores en REST
(crear, listar, obtener) y en webhooks (payment.status).
Los estados internos del ciclo de vida se consolidan:
- pending: iniciado, validado, aceptado
- processing: procesando, pendiente de procesamiento, registrando
- confirmed: confirmado por el riel, aún no registrado en el ledger
- posted: registrado en el ledger (se mantiene posted tras un reverso posterior)
- failed: fallido, rechazado, posting_failed, cancelado, expirado
Un reverso no es un estado. Usá result=reversed / has_reversal=true en REST
y el webhook payment.reversed.
El detalle operativo queda en status_detail (nombre interno en mayúsculas)
en crear/listar/obtener de REST. El webhook payment.failed usa error.platform.code
para distinguir fallos de registro de rechazos del proveedor.Valores: pending, processing, confirmed, posted, failed |
payment.amount | object | sí | — |
payment.amount.amount | string | sí | Monto como texto decimal con exactamente 2 decimales (ej. "1000.00"). |
payment.amount.currency | string | sí | Moneda ISO 4217 del pago.Valores: CRC, USD |
payment.client_reference | string | sí | Referencia del comercio enviada al crear el pago (client_reference), no detail_reference. |
payment.external_reference | string | sí | — |
payment.fee_total | string | sí | Total de comisiones como texto decimal (misma moneda que amount.currency) |
payment.tax_total | string | sí | Total de impuestos como texto decimal (misma moneda que amount.currency) |
payment.net_amount | string | sí | Impacto neto en efectivo en la cuenta del comercio. PAYIN: amount - fee_total - tax_total (lo que recibe el comercio). PAYOUT: amount + fee_total + tax_total (débito total; la comisión se cobra aparte y no se descuenta del monto transferido). |
payment.succeeded_at | string (date-time) | sí | Marca de tiempo en que el proveedor confirmó el éxito del pago. |
provider | object | sí | Foto compartida del proveedor en las lecturas REST de pagos y en los sobres de webhook.
La referencia del riel solo está en payment.external_reference, no se duplica aquí.
Los provider_status_* coinciden con rejection.reason_code / message / code del webhook
cuando el proveedor rechazó la operación. null cuando no hay rechazo. |
provider.correlation_id | string | sí | 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.Puede ser null. |
provider.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
provider.provider_status_code | string | sí | Motivo/código crudo del proveedor (ej. 31). Mismo significado que
rejection.reason_code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_desc | string | sí | Texto Motivo del proveedor (prefiere Detalle). Mismo significado que
rejection.message del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_semantic | string | sí | Semántica normalizada de la plataforma (ej. ACCOUNT_BLOCKED). Mismo significado que
rejection.code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
payment.failed
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
schema_version | string | sí | — |
event | object | sí | — |
event.event_id | string (uuid) | sí | — |
event.event_type | string | sí | —Valores: payment.failed |
event.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
event.correlation_id | string | sí | — |
context | object | sí | — |
context.country_code | string | sí | — |
context.payment_method_code | string | sí | Catálogo público de métodos de pago. Mismos valores que en REST (PIN, SINPE_MOVIL). Nunca se exponen códigos internos.Valores: PIN, SINPE_MOVIL |
payment | object | sí | — |
payment.payment_id | string (uuid) | sí | — |
payment.public_id | integer (int64) | sí | Es el mismo identificador: número en webhooks y texto en REST. Tratalo siempre como texto. |
payment.status | string | sí | Catálogo público de estados del pago. Siempre en minúsculas. Mismos valores en REST
(crear, listar, obtener) y en webhooks (payment.status).
Los estados internos del ciclo de vida se consolidan:
- pending: iniciado, validado, aceptado
- processing: procesando, pendiente de procesamiento, registrando
- confirmed: confirmado por el riel, aún no registrado en el ledger
- posted: registrado en el ledger (se mantiene posted tras un reverso posterior)
- failed: fallido, rechazado, posting_failed, cancelado, expirado
Un reverso no es un estado. Usá result=reversed / has_reversal=true en REST
y el webhook payment.reversed.
El detalle operativo queda en status_detail (nombre interno en mayúsculas)
en crear/listar/obtener de REST. El webhook payment.failed usa error.platform.code
para distinguir fallos de registro de rechazos del proveedor.Valores: pending, processing, confirmed, posted, failed |
payment.amount | object | sí | — |
payment.amount.amount | string | sí | Monto como texto decimal con exactamente 2 decimales (ej. "1000.00"). |
payment.amount.currency | string | sí | Moneda ISO 4217 del pago.Valores: CRC, USD |
payment.client_reference | string | no | Referencia del comercio enviada al crear el pago (client_reference), no detail_reference. |
payment.external_reference | string | no | — |
payment.error | object | sí | — |
payment.error.domain | string | sí | —Valores: provider_rejection, platform_validation, platform_posting, platform_infra, platform_reversal, sinpe_rejection |
payment.error.platform | object | sí | — |
provider | object | sí | Foto compartida del proveedor en las lecturas REST de pagos y en los sobres de webhook.
La referencia del riel solo está en payment.external_reference, no se duplica aquí.
Los provider_status_* coinciden con rejection.reason_code / message / code del webhook
cuando el proveedor rechazó la operación. null cuando no hay rechazo. |
provider.correlation_id | string | sí | 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.Puede ser null. |
provider.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
provider.provider_status_code | string | sí | Motivo/código crudo del proveedor (ej. 31). Mismo significado que
rejection.reason_code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_desc | string | sí | Texto Motivo del proveedor (prefiere Detalle). Mismo significado que
rejection.message del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_semantic | string | sí | Semántica normalizada de la plataforma (ej. ACCOUNT_BLOCKED). Mismo significado que
rejection.code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
rejection | object | no | Rechazo del proveedor independiente del riel. Compartido por POST /accounts/validate
(data.rejection) y el webhook payment.failed (rejection).
Objeto solo cuando el proveedor rechazó la operación; omitido/null en caso contrario. |
rejection.code | string | sí | Semántica normalizada de la plataforma derivada de reason_code. Solo un subconjunto
de valores de reason_code (ver el catálogo en la descripción de POST /accounts/validate
y del endpoint de contrato del webhook payment.failed) tiene entrada propia aquí;
para cualquier otro reason_code numérico, code es SINPE_REJECTED aunque message
sea específico (ej. reason_code = 21 "fondos insuficientes" igual devuelve
code = SINPE_REJECTED, no tiene semántica propia). PHONE_NOT_REGISTERED es
el valor de respaldo cuando reason_code no es numérico (flujo del monedero SINPE_MOVIL).Valores: ACCOUNT_CLOSED, ACCOUNT_NOT_FOUND, ACCOUNT_BLOCKED, INVALID_ACCOUNT, IDENT_MISMATCH, IDENT_TYPE_INVALID, INVALID_ID_FORMAT, VALIDATION_ERROR, CURRENCY_MISMATCH, NOT_AUTHORIZED_BY_RECIPIENT, PROFILE_NOT_ALLOWED, CHANNEL_NOT_RECOGNIZED, COMMUNICATION_ERROR, RETRY_LATER, WALLET_PHONE_ORIGIN_INVALID, WALLET_PHONE_ORIGIN_NOT_ENABLED, WALLET_PHONE_DEST_INVALID, WALLET_PHONE_NOT_REGISTERED_BCCR, WALLET_NOT_FOUND, WALLET_PHONE_ALREADY_ACTIVE, PHONE_NOT_REGISTERED, SINPE_REJECTED |
rejection.reason_code | string | sí | Motivo/código crudo devuelto por SINPE. El catálogo completo está en la descripción
de POST /accounts/validate (lo comparte este objeto dondequiera que aparezca,
incluido el webhook payment.failed). Un código no listado igual produce respuesta:
message cae a un texto genérico y code a SINPE_REJECTED. |
rejection.message | string | sí | Texto descriptivo del Motivo; prefiere el Detalle del proveedor cuando existe. |
payment.reversed
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
schema_version | string | sí | — |
event | object | sí | — |
event.event_id | string (uuid) | sí | — |
event.event_type | string | sí | —Valores: payment.reversed |
event.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
event.correlation_id | string | sí | — |
context | object | sí | — |
context.country_code | string | sí | — |
context.payment_method_code | string | sí | Catálogo público de métodos de pago. Mismos valores que en REST (PIN, SINPE_MOVIL). Nunca se exponen códigos internos.Valores: PIN, SINPE_MOVIL |
payment | object | sí | — |
payment.payment_id | string (uuid) | sí | — |
payment.public_id | integer (int64) | sí | Es el mismo identificador: número en webhooks y texto en REST. Tratalo siempre como texto. |
payment.status | string | sí | Catálogo público de estados del pago. Siempre en minúsculas. Mismos valores en REST
(crear, listar, obtener) y en webhooks (payment.status).
Los estados internos del ciclo de vida se consolidan:
- pending: iniciado, validado, aceptado
- processing: procesando, pendiente de procesamiento, registrando
- confirmed: confirmado por el riel, aún no registrado en el ledger
- posted: registrado en el ledger (se mantiene posted tras un reverso posterior)
- failed: fallido, rechazado, posting_failed, cancelado, expirado
Un reverso no es un estado. Usá result=reversed / has_reversal=true en REST
y el webhook payment.reversed.
El detalle operativo queda en status_detail (nombre interno en mayúsculas)
en crear/listar/obtener de REST. El webhook payment.failed usa error.platform.code
para distinguir fallos de registro de rechazos del proveedor.Valores: pending, processing, confirmed, posted, failed |
payment.result | string | sí | —Valores: reversed |
payment.has_reversal | boolean | sí | —Valores: true |
payment.amount | object | sí | — |
payment.amount.amount | string | sí | Monto como texto decimal con exactamente 2 decimales (ej. "1000.00"). |
payment.amount.currency | string | sí | Moneda ISO 4217 del pago.Valores: CRC, USD |
payment.client_reference | string | no | — |
payment.external_reference | string | no | — |
payment.reversed_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
payment.reason | object | no | — |
payment.reason.code | string | no | — |
payment.reason.message | string | no | — |
provider | object | sí | Foto compartida del proveedor en las lecturas REST de pagos y en los sobres de webhook.
La referencia del riel solo está en payment.external_reference, no se duplica aquí.
Los provider_status_* coinciden con rejection.reason_code / message / code del webhook
cuando el proveedor rechazó la operación. null cuando no hay rechazo. |
provider.correlation_id | string | sí | 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.Puede ser null. |
provider.occurred_at | string (date-time) | sí | Marca de tiempo RFC 3339 en UTC con precisión de segundos y sufijo Z.
Nunca se emiten fracciones de segundo. Ejemplo: 2026-01-15T12:00:00Z. |
provider.provider_status_code | string | sí | Motivo/código crudo del proveedor (ej. 31). Mismo significado que
rejection.reason_code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_desc | string | sí | Texto Motivo del proveedor (prefiere Detalle). Mismo significado que
rejection.message del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
provider.provider_status_semantic | string | sí | Semántica normalizada de la plataforma (ej. ACCOUNT_BLOCKED). Mismo significado que
rejection.code del webhook. null cuando no hay rechazo del proveedor.Puede ser null. |
Identificador público#
payment.public_id llega como número en el webhook y como texto en REST; es el mismo identificador. Tratalo siempre como texto.
Última verificación: 2026-10-22