Acepta Cleo en tu checkout
Deja que tus clientes compren ahora y paguen después. Cleo se encarga de la identidad, la evaluación de crédito, la conexión bancaria y el plan de cuotas — tú solo haces dos llamadas a la API.
Inicio rápido
Toda la integración es un redirect y una confirmación. Cleo aloja el checkout, así que la identidad del comprador, sus credenciales bancarias y la lógica de crédito nunca tocan tus servidores.
Crea una sesión
Servidor a servidor con tu clave secreta: envías el amount del pedido y tus URLs de retorno. Cleo devuelve un session_id y un checkout_url.
Redirige al comprador
Envía el navegador al checkout_url que te devolvimos. No lo construyas a mano.
El comprador completa Cleo
RUT → OTP por WhatsApp → conexión bancaria → decisión de crédito → plan de cuotas → mandato de descuento automático. Todo dentro de Cleo.
Confirma el resultado
Recibes un callback servidor a servidor, y siempre puedes consultar el estado de la sesión. Confirma antes de despachar.
149990 significa $149.990 CLP.Autenticación y entornos
Cada llamada lleva tu clave secreta como bearer token. Tu merchant_id se deriva de la clave — no lo envías en el body y no puedes actuar en nombre de otro comercio.
Authorization: Bearer sk_live_<tu-clave> Content-Type: application/json
Entornos
| Entorno | URL base | Clave |
|---|---|---|
| Producción | https://api-bnpl.cleo.cl | sk_live_… |
| Sandbox — integra aquí primero | https://sandbox-api-bnpl.cleo.cl | sk_test_… |
| Staging / Dev (uso interno de Cleo) | staging-api-bnpl.cleo.cl · dev-api-bnpl.cleo.cl | sk_test_… |
Cada entorno tiene su propia base de datos: nada de lo que hagas en sandbox toca producción. La API rechaza una clave del entorno equivocado en vez de hacer algo inesperado con ella:
{"error":"wrong_environment","detail":"This host serves test keys; that key is live. Use the production base URL."}
Lo único que cambia entre sandbox y producción es la URL base y la clave.
checkout_url).
Tu clave secreta nunca debe llegar al navegador.
La clave se emite cuando Cleo aprueba tu solicitud de comercio y se muestra una sola vez. La guardamos con hash: no podemos recuperarla, solo reemplazarla. Se verifica en cada llamada, así que una clave revocada deja de funcionar en la petición siguiente.
| Respuesta | Significado |
|---|---|
401 merchant_key_required | Falta el header Authorization. |
401 invalid_or_revoked_key | Clave incorrecta o revocada. |
401 wrong_environment | Clave de test contra producción, o al revés. |
409 merchant_not_linked | La clave es válida pero la cuenta no está terminada — contacta a Cleo. |
429 rate_limited | Más de 120 peticiones por minuto. |
Crear una sesión
Body
| Campo | Requerido | Notas |
|---|---|---|
amount | sí | Entero en CLP, entre 1.000 y 10.000.000. Es lo que se le cobra al comprador; él no puede modificarlo. |
currency | Solo CLP (valor por defecto). | |
merchant_order_id | recomendado | Tu referencia de pedido. Activa las protecciones de idempotencia y doble pago (más abajo) y viaja de vuelta en el callback. |
success_url / failure_url | recomendado | A dónde vuelve el navegador del comprador al terminar. |
callback_url / callback_failure_url | A dónde publicamos el resultado servidor a servidor. Si no las envías, usamos las configuradas por defecto en tu cuenta. | |
callback_method / callback_failure_method | POST (por defecto) o GET. | |
merchant_session_id · merchant_customer_id | Tus propias referencias; se devuelven tal cual en el callback. | |
expires_in_minutes | Entre 5 y 1440. Por defecto 60. | |
sign_callback | Si es true, los callbacks salientes de esta sesión llevan un header X-Cleo-Signature: sha256=… — ver Verificando la firma del callback. | |
payment_method | Fija el plan de cuotas y salta la selección. Uno de PAY_IN_14_DAYS, PAY_IN_30_DAYS, PAY_IN_3_PARTS, PAY_IN_6_PARTS, PAY_IN_9_PARTS, PAY_IN_12_PARTS, ETPAY. |
Todas las URLs deben ser https públicas. Rechazamos localhost, rangos privados, direcciones link-local y URLs con credenciales.
curl -X POST https://api-bnpl.cleo.cl/v1/sessions \ -H "Authorization: Bearer $CLEO_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 149990, "currency": "CLP", "merchant_order_id": "ORD-10432", "success_url": "https://tienda.example.com/checkout/gracias?order=10432", "failure_url": "https://tienda.example.com/checkout/error?order=10432", "callback_url": "https://tienda.example.com/api/cleo/webhook" }'
<?php $ch = curl_init("https://api-bnpl.cleo.cl/v1/sessions"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("CLEO_SECRET_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 149990, "merchant_order_id" => "ORD-10432", "success_url" => "https://tienda.example.com/checkout/gracias?order=10432", "failure_url" => "https://tienda.example.com/checkout/error?order=10432", "callback_url" => "https://tienda.example.com/api/cleo/webhook", ]), CURLOPT_RETURNTRANSFER => true, ]); $session = json_decode(curl_exec($ch), true); header("Location: " . $session["checkout_url"]);
import os, requests r = requests.post( "https://api-bnpl.cleo.cl/v1/sessions", headers={"Authorization": f"Bearer {os.environ['CLEO_SECRET_KEY']}"}, json={ "amount": 149990, "merchant_order_id": "ORD-10432", "success_url": "https://tienda.example.com/checkout/gracias?order=10432", "failure_url": "https://tienda.example.com/checkout/error?order=10432", "callback_url": "https://tienda.example.com/api/cleo/webhook", }, ) session = r.json() checkout_url = session["checkout_url"] # redirige al comprador aquí
const res = await fetch("https://api-bnpl.cleo.cl/v1/sessions", { method: "POST", headers: { authorization: `Bearer ${process.env.CLEO_SECRET_KEY}`, "content-type": "application/json", }, body: JSON.stringify({ amount: 149990, merchant_order_id: "ORD-10432", success_url: "https://tienda.example.com/checkout/gracias?order=10432", failure_url: "https://tienda.example.com/checkout/error?order=10432", callback_url: "https://tienda.example.com/api/cleo/webhook", }), }); const session = await res.json(); return Response.redirect(session.checkout_url, 303);
Respuesta · 201
{
"session_id": "b26d5129-146e-459b-9411-3a3800874305",
"checkout_url": "https://api-bnpl.cleo.cl/checkout/start/b26d5129-146e-459b-9411-3a3800874305",
"expires_at": "2026-07-23T15:26:06.900Z",
"amount": 149990,
"currency": "CLP",
"status": "RUT",
"reused": false
}
checkout_url tal como viene en la respuesta. En sandbox apunta al host de sandbox; construirlo a mano es la forma más rápida de mandar al comprador al entorno equivocado.Reintentos y doble pago
Si vuelves a crear una sesión con el mismo merchant_order_id:
- mientras la sesión siga abierta, devolvemos la existente (
200,"reused": true) — un reintento nunca crea un segundo checkout; - una vez que ese pedido está pagado, devolvemos
409 order_already_paid, así una pestaña vieja o una petición duplicada no puede cobrarle dos veces al comprador.
Los comercios que legítimamente reutilizan referencias pueden marcarse para permitirlo.
Redirigir al comprador
Envía el navegador al checkout_url que recibiste:
Esa página muestra el nombre de tu tienda, la referencia del pedido y el monto, y luego ejecuta el flujo de Cleo: verificación del RUT chileno, OTP por WhatsApp, conexión bancaria de solo lectura, decisión de crédito, elección del plan de cuotas y firma del mandato de descuento automático. El monto lo fija el comercio y el comprador no puede modificarlo.
Al terminar, el comprador ve un botón «Volver a {tu tienda}» que apunta a tu success_url.
Callback (resultado)
Cuando el checkout termina, publicamos el resultado en tu callback_url:
{
"createdAt": "2026-07-23T15:40:11.000Z",
"callbackId": 8123,
"version": "v1",
"event": "CHECKOUT_SUCCEEDED",
"payload": {
"sessionId": "b26d5129-…",
"merchantId": "tu-login",
"merchantSessionId": null,
"merchantCustomerId": null,
"merchantOrderId": "ORD-10432",
"status": "OK",
"currency": "CLP",
"amount": 149990,
"invoiceNumber": "CLO-4821-1",
"customer": {
"ssn": "12345678-9",
"email": "…",
"phone": "…",
"name": { "first": "…", "last": "…", "org": null },
"address": { "street": "…", "region": "…", "country": "CL" }
}
}
}
event es CHECKOUT_SUCCEEDED (status: "OK") o CHECKOUT_FAILED (status: "ERROR", por ejemplo crédito rechazado). Con callback_method: "GET" el mismo JSON llega en base64 en el parámetro de query SWEETPAY_DATA.
Entrega at-least-once
Intentamos dos veces de inmediato y después reintentamos con backoff — 1 min, 5 min, 30 min, 2 h, 6 h, es decir 6 intentos en unas 8,5 horas — hasta que tu endpoint responda 2xx. Cualquier 2xx cuenta como entregado; un timeout de 45 s cuenta como fallo.
Como el reintento reenvía el mismo payload byte a byte, el callbackId es estable entre intentos:
- Haz tu handler idempotente — deduplica por
callbackId. Si procesaste la petición pero el 2xx no nos llegó, te vamos a llamar otra vez. - Responde rápido (acusa recibo y luego procesa). Un endpoint lento lo tratamos como fallo.
Si todos los intentos fallan dejamos de insistir y lo registramos; el pedido sigue pagado y el endpoint de estado sigue siendo la fuente de verdad.
sign_callback: true al crear la sesión y los
reintentos y el callback de expiración llevan el header X-Cleo-Signature: sha256=…
(ver abajo). El primer intento — el que se envía apenas termina el checkout — todavía no firma,
porque sale de un servicio distinto. Hasta que eso se cierre, confirma cada callback contra el
endpoint de estado con tu clave secreta antes de despachar, esté firmado o no —
esa llamada sí está autenticada y es autoritativa, y además cubre el caso de un callback que nunca llega.
Verificando la firma del callback
Cuando X-Cleo-Signature está presente, es sha256=<hex> donde <hex>
es HMAC-SHA256(cuerpo crudo, tu webhook_signing_secret) — el mismo whsec_… que te
mostramos cuando se emitió tu clave (pídelo de nuevo a Cleo si lo perdiste). Recalcúlalo sobre los
bytes crudos exactos que recibiste (antes de cualquier re-serialización JSON, porque el orden de las
llaves o los espacios cambian el hash) y compáralo con una comparación de tiempo constante:
const crypto = require("node:crypto"); function verify(rawBody, header, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return header?.length === expected.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected)); }
Consultar el estado
curl https://api-bnpl.cleo.cl/v1/sessions/b26d5129-146e-459b-9411-3a3800874305 \ -H "Authorization: Bearer $CLEO_SECRET_KEY"
const s = await (await fetch( `https://api-bnpl.cleo.cl/v1/sessions/${sessionId}`, { headers: { authorization: `Bearer ${process.env.CLEO_SECRET_KEY}` } }, )).json(); if (s.paid) await despacharPedido(s.merchant_order_id);
Respuesta · 200
{
"session_id": "b26d5129-…",
"status": "CONFIRMED",
"paid": true,
"expired": false,
"expires_at": "2026-07-23T15:26:06.900Z",
"amount": 149990,
"currency": "CLP",
"merchant_order_id": "ORD-10432",
"cancelled": false,
"callback": { "sent_at": "2026-07-23T15:40:11.000Z", "delivered": true },
"installments": [
{ "invoice_number": "CLO-4821-1", "amount": 52497, "due_at": "…", "number": 1, "payment_status": "PENDING" }
]
}
paid es el campo del que te tienes que fiar. Una sesión que no creaste tú devuelve 404 — solo puedes leer las tuyas.
Cancelar una orden
// cancel-partial { "amount": 15000 }
Cancela (total o por un amount dado) la orden detrás de esa sesión y crea un reembolso por lo
ya pagado sobre el nuevo total — mismo motor y reglas que cuando Cleo la cancela por ti: una orden ya
cancelada devuelve 409 already_cancelled, una con una cuota en proceso de pago devuelve
409 invoices_in_payment_process, y un monto de cancel-partial que no sea
estrictamente menor al remanente cancelable de la orden devuelve 400 amount_too_high (o
400 bad_amount si no es un número positivo). Una sesión que no creaste tú devuelve
404 session_not_found — solo puedes cancelar las tuyas, igual que al leer el estado.
Respuesta · 200
{ "ok": true, "cancelledInvoiceNumbers": ["CLO-4821-1"], "refundCreated": true, "amountRefunded": 31497 }
También vas a recibir un callback ORDER_CANCELLED (ver Callback) en tu
callback_url — discrimínalo por event, ya que es la misma URL que recibe
CHECKOUT_SUCCEEDED/CHECKOUT_FAILED.
Estados de la sesión
El campo status recorre el viaje del comprador. Para decidir qué hacer con el pedido te basta paid; los estados intermedios sirven para diagnosticar.
| Estado | Significado | ¿Terminal? |
|---|---|---|
RUT · PHONE · PIN · CUSTOMER_DETAILS · EMAIL_PIN | Identidad y contacto — el comprador va avanzando. | no |
BANK_ONBOARDING · BANK_ONBOARDING_PENDING · CHECKING_CUSTOMER_DATA | Conexión bancaria y evaluación de crédito en curso. | no |
PAYMENT_METHOD · SUBSCRIPTION_INTENT_INIT | Eligiendo el plan de cuotas y firmando el mandato. | no |
CONFIRMED | Aprobado y mandato firmado. paid: true — despacha el pedido. | sí |
DENIED | La evaluación de crédito no aprobó. | sí |
ERROR · FAILED | El flujo no pudo completarse. | sí |
Una sesión que pasa su expires_at sin confirmarse se devuelve con "expired": true. El comprador puede volver a empezar creando una sesión nueva.
Errores
| Código | Error | Qué hacer |
|---|---|---|
| 400 | invalid_amount | Entero en CLP dentro del rango permitido. |
| 400 | unsupported_currency | Solo CLP. |
| 400 | *_must_be_https · *_must_be_public · *_invalid | Usa una URL https pública. |
| 401 | merchant_key_required · invalid_or_revoked_key · wrong_environment | Revisa la clave y la URL base del entorno. |
| 403 | merchant_inactive | Cuenta deshabilitada — contacta a Cleo. |
| 404 | session_not_found | Ese session_id no existe o no es tuyo. |
| 409 | order_already_paid | Ese merchant_order_id ya está pagado. |
| 409 | merchant_not_linked · merchant_not_found | Configuración de la cuenta incompleta — contacta a Cleo. |
| 400 | invalid_payment_method | Uno de los valores listados en Crear una sesión. |
| 400 | bad_amount | amount de cancel-partial debe ser un número positivo. |
| 400 | amount_too_high | amount de cancel-partial debe ser estrictamente menor al remanente cancelable de la orden. |
| 409 | already_cancelled | Esa orden ya fue cancelada. |
| 409 | invoices_in_payment_process | Una cuota está en proceso de pago; reintenta cuando se resuelva. |
| 503 | feature_disabled | La cancelación todavía no está disponible — ver Cancelar una orden. |
| 429 | rate_limited | Baja el ritmo; el límite es 120 req/min. |
| 503 | temporarily_unavailable | Reintenta con backoff. |
Probar en sandbox
Integra primero contra https://sandbox-api-bnpl.cleo.cl con una clave sk_test_…. Estos valores completan una compra de punta a punta sin un WhatsApp, un banco ni una consulta de crédito reales:
| Campo | Valor de prueba |
|---|---|
| RUT | 11.111.111-1 — RUT de simulación; devuelve una persona y un perfil bancario ficticios |
| Teléfono | +56911111111 |
| PIN de WhatsApp / Email | 1111 |
CONFIRMED, y súbelo para ver DENIED.EVALUATED_AMOUNT_TOO_HIGH cuando el monto del pedido más la deuda pendiente del comprador supera $1.000.000 CLP. Ojo: la API acepta al crear la sesión montos de hasta $10.000.000, pero el checkout aplica ese tope al evaluar — un pedido por encima del límite se crea bien y termina en DENIED.Salir a producción
Cuando tu integración en sandbox esté lista:
Pide tus credenciales productivas
Cleo aprueba tu comercio y emite una clave sk_live_…. Se muestra una sola vez — guárdala en tu gestor de secretos, nunca en el repositorio.
Cambia la URL base y la clave
sandbox-api-bnpl.cleo.cl → api-bnpl.cleo.cl. No hay ningún otro cambio de código.
Registra tus URLs productivas
Tu callback_url, success_url y failure_url de producción deben ser https públicas y estar accesibles desde internet.
Verifica antes de despachar
Confirma cada callback contra GET /v1/sessions/{id} con tu clave secreta.