# KUTI API — Documentación > API de pagos en Perú: QR interoperable (Yape/Plin), pago de servicios (institución KUTI), checkout, saldo, liquidaciones y webhooks. Base URL https://api.kuti.pe/v1. Sitio: https://www.kuti.pe Docs: https://docs.kuti.pe API base: https://api.kuti.pe/v1 Auth: header `Authorization` (ej. Bearer kuti_live_…) País / moneda: Perú · PEN Última actualización: 2026-09-22 Versión extendida: https://docs.kuti.pe/llms-full.txt Sitemap: https://docs.kuti.pe/sitemap.xml ## Licencia Los sistemas de IA pueden indexar y citar esta documentación con fines informativos. No uses el contenido para suplantar la marca ni para exfiltrar secretos de ejemplo como si fueran reales. ## Índice ### Introducción - Primeros pasos: https://docs.kuti.pe - Autenticación: https://docs.kuti.pe/autenticacion - SDKs: https://docs.kuti.pe/sdks - Métodos de pago: https://docs.kuti.pe/metodos-de-pago - Prueba vs producción: https://docs.kuti.pe/prueba-vs-produccion - Errores: https://docs.kuti.pe/errores ### Checkout - Checkout.js: https://docs.kuti.pe/checkout-js - Personalización: https://docs.kuti.pe/checkout-personalizacion - POST Crear sesión: https://docs.kuti.pe/post-checkout-sessions - GET Estado de sesión: https://docs.kuti.pe/get-checkout-session ### Negocios - POST Crear: https://docs.kuti.pe/post-merchants - GET Listar: https://docs.kuti.pe/get-merchants - GET Detalle: https://docs.kuti.pe/get-merchant - PATCH Editar: https://docs.kuti.pe/patch-merchant ### Clientes - POST Crear: https://docs.kuti.pe/post-customers - GET Listar: https://docs.kuti.pe/get-customers - GET Detalle: https://docs.kuti.pe/get-customer - PATCH Editar: https://docs.kuti.pe/patch-customer - DELETE Borrar: https://docs.kuti.pe/delete-customer ### Cobros - POST Crear: https://docs.kuti.pe/post-payment-intents - GET Listar: https://docs.kuti.pe/get-payment-intents - GET Detalle: https://docs.kuti.pe/get-payment-intent - POST Anular: https://docs.kuti.pe/post-payment-intent-cancel - POST Enviar por WhatsApp: https://docs.kuti.pe/post-payment-intent-send-whatsapp ### Saldo - GET Consultar: https://docs.kuti.pe/get-balance - GET Movimientos: https://docs.kuti.pe/get-balance-transactions - GET Cotizar comisión: https://docs.kuti.pe/get-fee-preview ### Pagos a tu cuenta - GET Listar: https://docs.kuti.pe/get-payouts - GET Detalle: https://docs.kuti.pe/get-payout - GET Movimientos: https://docs.kuti.pe/get-payout-transactions - GET Listar cuentas: https://docs.kuti.pe/get-payout-accounts - POST Registrar cuenta: https://docs.kuti.pe/post-payout-accounts - GET Preferencias: https://docs.kuti.pe/get-payout-settings - PUT Actualizar preferencias: https://docs.kuti.pe/put-payout-settings ### Webhooks - Cómo funcionan: https://docs.kuti.pe/webhooks - POST Crear endpoint: https://docs.kuti.pe/post-webhook-endpoints - GET Listar endpoints: https://docs.kuti.pe/get-webhook-endpoints - DELETE Eliminar endpoint: https://docs.kuti.pe/delete-webhook-endpoint - GET Listar eventos: https://docs.kuti.pe/get-events - GET Detalle evento: https://docs.kuti.pe/get-event - GET Entregas: https://docs.kuti.pe/get-event-deliveries - POST Reintentar entrega: https://docs.kuti.pe/post-webhook-delivery-retry ## Enlaces útiles - Marketing: https://www.kuti.pe - Dashboard: https://app.kuti.pe - Soporte: soporte@kuti.pe - Empresa: Consultia Digital S.A.C. --- # Contenido completo de la documentación Generado desde https://docs.kuti.pe. Última actualización: 2026-09-22. Cada sección es una página pública SSR. ## API de KUTI URL: https://docs.kuti.pe KUTI es la API de pagos para negocios en Perú. Tu backend crea un cobro; el cliente paga con Yape, Plin, billeteras bancarias o desde su banca buscando la institución KUTI. Tú confirmas el pago con webhooks. Base URL única: https://api.kuti.pe/v1. Usa kuti_test_… para integrar sin dinero real; kuti_live_… para producción (requiere sello KUTI habilitado en el dashboard). Arranque en 5 minutos - Crea tu cuenta en app.kuti.pe y un negocio. - Emite una secret key de prueba (kuti_test_…) en Desarrolladores → API keys. - Crea un cobro con POST /payment-intents (QR + pago de servicios). - Abre el checkout_url o embebe Checkout.js. - Escucha payment.succeeded en un webhook para marcar el pedido como pagado. Primer cobro (cURL) ```bash curl -s https://api.kuti.pe/v1/payment-intents \ -H 'Authorization: Bearer kuti_test_…' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440001' \ -d '{ "amount": { "amount": "50.00", "currency": "PEN" }, "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "description": "Pedido #1042", "external_reference": "order-1042" }' ``` La respuesta trae payment_method.qr (payload EMVCo para dibujar el QR), payment_method.payment_code (código para banca) y checkout_url (pay.kuti.pe/c/…). Para asociar un cliente usa el objeto customer: con id (cus_…) si ya existe, o con type + datos si es nuevo. Cobro con cliente existente ```bash curl -s https://api.kuti.pe/v1/payment-intents \ -H 'Authorization: Bearer kuti_test_…' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440002' \ -d '{ "amount": { "amount": "50.00", "currency": "PEN" }, "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3" }, "description": "Pedido #1042", "external_reference": "order-1042" }' ``` Base URL Entorno | URL | API key Prueba | https://api.kuti.pe/v1 | kuti_test_… / kuti_pub_test_… Producción | https://api.kuti.pe/v1 | kuti_live_… / kuti_pub_live_… Qué incluye v1 - Cobros con QR interoperable y/o pago de servicios (institución KUTI) - Checkout hospedado (pay.kuti.pe) y Checkout.js embebido - Clientes, webhooks firmados, saldo y pagos a tu CCI - Modo prueba aislado del modo producción Fechas Todos los date-time van en UTC ISO-8601 con sufijo Z (created_at, expires_at, paid_at, …). Convierte a America/Lima en tu UI si lo necesitas. Siguiente - [Autenticación](/autenticacion) — secret vs publishable keys - [SDKs](/sdks) — Node (@kuti-pe/node), PHP y Python - [Métodos de pago](/metodos-de-pago) — QR y banca - [Prueba vs producción](/prueba-vs-produccion) — cuándo usar cada key - [Checkout.js](/checkout-js) — modal o embed en tu sitio - [Webhooks](/webhooks) — confirmar el pago en tu servidor --- ## Autenticación URL: https://docs.kuti.pe/autenticacion Cabecera ```http Authorization: Bearer kuti_live_… ``` - Obligatoria en los endpoints de API (cobros, clientes, webhooks, saldo, …): Authorization: Bearer kuti_… - Checkout público (GET /checkout-sessions/{id}): no uses la secret key; pasa el client_secret de la sesión como ?client_secret=… o header X-Kuti-Token. - Sin key o inválida → 401 UNAUTHORIZED. - kuti_test_… = prueba; kuti_live_… = producción. No mezcles datos entre entornos. Cómo obtener la key Dashboard KUTI → Desarrolladores → API keys. El plaintext solo se muestra al crearla. Guárdala en un secreto de servidor. Claves live: el negocio debe tener el sello KUTI habilitado (verificación en Ajustes). Si no, la API responde 403 KYB_REQUIRED. Las de prueba se pueden crear desde el inicio. Publishable keys kuti_pub_test_… / kuti_pub_live_… son para el cliente (Checkout.js). Solo pueden crear sesiones de checkout. Nunca uses una secret key en el navegador. Ejemplo cURL ```bash curl -s https://api.kuti.pe/v1/payment-intents \ -H 'Authorization: Bearer kuti_test_…' \ -H 'Content-Type: application/json' ``` Siguiente - [SDKs](/sdks) — Node, PHP y Python - [Prueba vs producción](/prueba-vs-produccion) — test vs live - [Crear cobro](/post-payment-intents) — POST /payment-intents --- ## Checkout.js URL: https://docs.kuti.pe/checkout-js Checkout.js es un script chico que abre pay.kuti.pe en un iframe. Tu backend crea el cobro con la secret key; el browser solo recibe un checkoutUrl y llama a window.Kuti.open(...). Dos modos: modal e inline Kuti.open acepta dos formas de mostrar el checkout. Sin containerId abre un modal; con containerId lo incrusta en un div de tu página. Modo | Cómo se activa | Cuándo usarlo Modal | Kuti.open({ checkoutUrl }) — default | Botón "Pagar" que abre overlay encima de tu sitio. Inline | Kuti.open({ checkoutUrl, containerId: "…" }) | Checkout embebido en un paso de tu propio flujo (sin overlay). onClose solo aplica al modal (X, click fuera o Esc). En inline no hay overlay: usa Kuti.close() para vaciar el contenedor. Instalación Pon esto antes de : HTML ```html ``` Sírvelo desde js.kuti.pe, no lo copies a tu CDN. Así te llegan fixes sin redeployar. Cómo funciona Tu servidor crea el cobro → tu página muestra el QR → el cliente paga → tú confirmas en backend. Cliente → Tu sitio: 1. Clic en Pagar Tu sitio → Tu backend: 2. Pedir cobro Tu backend → KUTI: 3. POST /checkout-sessions KUTI → Tu backend: 4. checkout_url Tu backend → Tu sitio: 5. checkoutUrl Tu sitio → Cliente: 6. Modal o embed con QR Cliente → KUTI: 7. Paga (QR / banca KUTI) KUTI → Tu sitio: 8. onSuccess KUTI → Tu backend: 9. webhook payment.succeeded La secret key no sale del servidor. Antes de marcar un pedido como pagado, confírmalo con la API o el webhook. 1. Crear la sesión (backend) No crees el cobro desde el browser ni uses un monto que mande el cliente. En el servidor resuelves el precio (por productId, por ejemplo) y llamas a KUTI con tu secret key. cURL ```bash curl -s https://api.kuti.pe/v1/checkout-sessions \ -H 'Authorization: Bearer kuti_live_…' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 550e8400-e29b-41d4-a716-446655440030' \ -d '{ "amount": { "amount": "249.90", "currency": "PEN" }, "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "description": "Zapatillas Running Aero Talla 42", "external_reference": "order-1042" }' ``` Te devuelve checkout_url (ya trae el client_secret). Pásalo al frontend como checkoutUrl. Si quieres, también payment_intent_id para la página de gracias. Node (Express) ```js app.post("/api/checkout", async (req, res) => { const product = catalog[req.body.productId]; // precio real en tu catálogo if (!product) return res.status(404).json({ error: "unknown_product" }); const r = await fetch("https://api.kuti.pe/v1/checkout-sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.KUTI_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID(), }, body: JSON.stringify({ amount: { amount: product.amount, currency: "PEN" }, payment_method_types: ["INTEROPERABLE_QR", "BANK_TRANSFER"], description: product.name, external_reference: `order-${Date.now()}`, customer: req.body.customer, // opcional }), }); const json = await r.json(); res.status(201).json({ checkoutUrl: json.data.checkout_url, paymentIntentId: json.data.payment_intent_id, }); }); ``` 2. Modo modal (default) Pides el checkoutUrl a tu API y abres el overlay. No pases containerId: Modal ```html ``` 3. Modo inline Pásale containerId con el id de un div de tu página. El iframe se renderiza ahí (sin modal ni overlay): Inline ```html
``` Si el div no existe, onError recibe CONTAINER_NOT_FOUND. Dale altura mínima al contenedor (p. ej. min-height: 640px) para que el QR se vea bien. Ejemplos Si prefieres partir de código que ya corre, clona este repo (iremos sumando más stacks): - Next.js: Tienda de muestra: modal, inline y una /gracias que vuelve a chequear el pago en el servidor. Kuti.open(options) Opción | Tipo | Req. | Qué hace checkoutUrl | string | Sí* | La URL de la sesión (data.checkout_url). Lo normal. clientSecret | string | Sí* | Si preferís armar la URL a mano. containerId | string | No | Id del div → modo inline. Sin esto, modal. appearance | object | No | Color y logo. Ver Personalización. onSuccess | fn | No | El pago salió bien en el iframe. onFailure | fn | No | Cobro en FAILED o CANCELLED. onExpired | fn | No | Se venció sin pagar. onClose | fn | No | Cerró el modal (X, fuera o Esc). No aplica en inline. onError | fn | No | Algo falló al cargar (no es el estado del cobro). *Necesitas checkoutUrl o clientSecret (uno de los dos). Kuti.close() ```js // Cierra el modal o vacía el div inline. // No dispara onClose (eso es solo si el usuario lo cierra). window.Kuti.close(); ``` Callbacks onSuccess / onFailure / onExpired ```js onSuccess: ({ paymentIntentId }) => { /* pi_… */ }, onFailure: ({ paymentIntentId, status }) => { // status: "FAILED" | "CANCELLED" }, onExpired: ({ paymentIntentId }) => { // normalmente creas otra sesión y vuelves a open() }, onError: ({ code, message }) => { // MISSING_CHECKOUT_URL | CONTAINER_NOT_FOUND | CHECKOUT_LOAD_FAILED } ``` onSuccess no es la verdad absoluta: alguien podría dispararlo a mano. Antes de entregar, pregunta a KUTI desde tu backend (GET /payment-intents/{id}) o espera payment.succeeded. Confirmar el pago - Lo mínimo: en /gracias tu API pregunta si ese payment_intent_id está SUCCEEDED. - En producción suma el webhook payment.succeeded (por si el comprador cierra la pestaña). - El id en la URL no es secreto. Lo que importa es que solo tu backend, con la secret key, pueda decir "sí, pagó". Preguntas frecuentes ¿React / Vue / Angular? Sí. Cargas el script y llamas a Kuti.open en el click. En Next App Router, el componente del botón va con "use client". ¿Y si cierran el modal? El cobro sigue PENDING hasta que expire o lo anules. Pueden volver a abrir con el mismo checkoutUrl mientras sirva. ¿Hace falta publishable key en el browser? No. El checkoutUrl ya alcanza para esa sesión. La secret key nunca va al frontend. Siguiente - [Personalización](/checkout-personalizacion) — appearance y branding - [Crear sesión](/post-checkout-sessions) — POST /checkout-sessions - [Webhooks](/webhooks) — confirmar en el servidor --- ## Personalización URL: https://docs.kuti.pe/checkout-personalizacion Pásale appearance a Kuti.open. KUTI los manda al iframe de pay.kuti.pe como query params. Vale para los dos modos (modal e inline). Campo | Tipo | Qué cambia primaryColor | string (hex) | Botones y links. Ej. "#2563eb". Reemplaza el teal de KUTI. logo | string (URL) | Imagen del header. Tiene que ser URL absoluta (https). El logo debe ser una URL https completa. El checkout vive en pay.kuti.pe; una ruta relativa de tu tienda no carga. Modal Modal + appearance ```js window.Kuti.open({ checkoutUrl, appearance: { primaryColor: "#2563eb", logo: "https://cdn.mitienda.pe/brand/logo-120x32.png", }, onSuccess: ({ paymentIntentId }) => { location.href = "/gracias?kuti_payment_id=" + encodeURIComponent(paymentIntentId); }, }); ``` Inline Inline + appearance ```js window.Kuti.open({ checkoutUrl, containerId: "kuti-checkout", appearance: { primaryColor: "#2563eb", logo: "https://cdn.mitienda.pe/brand/logo-120x32.png", }, onSuccess: ({ paymentIntentId }) => { location.href = "/gracias?kuti_payment_id=" + encodeURIComponent(paymentIntentId); }, }); ``` En el ejemplo de Next.js ya viene un appearance de muestra en modal e inline: - Next.js: Modal e inline con color y logo configurados. Siguiente - [Crear sesión](/post-checkout-sessions) — obtener checkout_url - [Checkout.js](/checkout-js) — montar el modal/embed --- ## Borrar cliente URL: https://docs.kuti.pe/delete-customer Endpoint: DELETE https://api.kuti.pe/v1/customers/{id} Borra o archiva un customer. Si no tiene cobros se elimina; si tiene, se archiva y desaparece de listados. Nota: Si no tiene cobros: deleted=true, archived=false. Si tiene cobros: deleted=false, archived=true y payment_intents_count > 0 (desaparece de listados; los cobros siguen resolviendo al cliente). DELETE /customers/{id} --- ## Eliminar endpoint URL: https://docs.kuti.pe/delete-webhook-endpoint Endpoint: DELETE https://api.kuti.pe/v1/webhook-endpoints/{id} Elimina un webhook endpoint. Deja de recibir entregas. Nota: Responde 204 sin body si se eliminó. DELETE /webhook-endpoints/{id} --- ## Errores URL: https://docs.kuti.pe/errores Campo | Qué es success | Siempre false en errores message | Mensaje legible (igual que error.message) error.code | Código de dominio (p. ej. MERCHANT_NOT_FOUND) error.message | Qué falló error.request_id | Id de la petición para soporte error.doc_url | Enlace a la documentación del error error.details | Lista opcional de { field, code, message } Códigos HTTP HTTP | Cuándo 400 | Petición malformada 401 | Falta Authorization o la key no es válida 403 | Sin permiso, o KYB_REQUIRED (falta sello KUTI habilitado para live) 404 | El negocio u otro recurso no existe 409 | Conflicto (duplicado o Idempotency-Key reutilizada con otro payload) 422 | Validación de dominio o body inválido 403 — producción sin verificación ```json { "success": false, "message": "Completa la verificación KUTI para operar en modo producción.", "error": { "code": "KYB_REQUIRED", "message": "Completa la verificación KUTI para operar en modo producción.", "request_id": "req_01J8Z3K4M5N6P7Q8R9S0T1U2V3" } } ``` 404 ```json { "success": false, "message": "The requested merchant does not exist.", "error": { "code": "MERCHANT_NOT_FOUND", "message": "The requested merchant does not exist.", "request_id": "req_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "doc_url": "https://docs.kuti.pe/errors/MERCHANT_NOT_FOUND" } } ``` 401 ```json { "success": false, "message": "Missing or invalid credentials.", "error": { "code": "UNAUTHORIZED", "message": "Missing or invalid credentials.", "request_id": "req_01J8Z3K4M5N6P7Q8R9S0T1U2V3" } } ``` 422 ```json { "success": false, "message": "Validation failed.", "error": { "code": "VALIDATION_ERROR", "message": "Validation failed.", "request_id": "req_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "details": [ { "field": "tax_id.value", "code": "INVALID_FORMAT", "message": "RUC must match the expected pattern." } ] } } ``` Siguiente - [Crear cobro](/post-payment-intents) — primer endpoint - [Webhooks](/webhooks) — eventos firmados - [SDKs](/sdks) — cliente oficial --- ## Consultar saldo URL: https://docs.kuti.pe/get-balance Endpoint: GET https://api.kuti.pe/v1/balance Saldo actual del negocio: lo pendiente de depositar y lo ya depositado en tu cuenta bancaria. Nota: pending_deposit = cobrado que aún no llegó a tu banco. deposited = histórico de lo ya depositado. El modo (test/live) lo define tu API key. GET /balance --- ## Movimientos del saldo URL: https://docs.kuti.pe/get-balance-transactions Endpoint: GET https://api.kuti.pe/v1/balance-transactions Ledger del negocio: cada cobro, reembolso, payout o contracargo que afecta el saldo, con desglose de comisión. Nota: type: PAYMENT, REFUND, PAYOUT, ADJUSTMENT, CHARGEBACK. status: PENDING, AVAILABLE, IN_PAYOUT, PAID. Filtra por payout_id para ver lo incluido en un depósito. GET /balance-transactions --- ## Detalle de sesión de checkout URL: https://docs.kuti.pe/get-checkout-session Endpoint: GET https://api.kuti.pe/v1/checkout-sessions/{id} Consulta el estado de una sesión. Público: autentica con client_secret (query o X-Kuti-Token). Nota: Público: no uses Authorization Bearer. Autoriza con el client_secret de la sesión — query ?client_secret=… o header X-Kuti-Token (mismo valor; el header tiene prioridad). GET /checkout-sessions/{id} --- ## Detalle de cliente URL: https://docs.kuti.pe/get-customer Endpoint: GET https://api.kuti.pe/v1/customers/{id} Obtiene un customer por id (cus_…). Incluye payment_intents_count. Nota: Solo puedes leer clientes de tu merchant (el de la API key). GET /customers/{id} --- ## Listar clientes URL: https://docs.kuti.pe/get-customers Endpoint: GET https://api.kuti.pe/v1/customers Lista customers del merchant autenticado, con búsqueda y paginación. Nota: per_page acepta un entero 1–100 o el literal all. q busca en nombre, razón social, correo, documento o external_id. GET /customers --- ## Detalle del evento URL: https://docs.kuti.pe/get-event Endpoint: GET https://api.kuti.pe/v1/events/{id} Obtiene un evento por id (evt_…). Misma forma que el body del webhook. Payloads de cada tipo: guía Webhooks → Cómo funcionan. GET /events/{id} --- ## Entregas de un evento URL: https://docs.kuti.pe/get-event-deliveries Endpoint: GET https://api.kuti.pe/v1/events/{id}/deliveries Lista los intentos de entrega de un evento a tus webhooks (éxito, fallo, reintentos). Nota: status de entrega: PENDING, SUCCEEDED, FAILED, DEAD. attempt_history va del más reciente al más antiguo. GET /events/{id}/deliveries --- ## Listar eventos URL: https://docs.kuti.pe/get-events Endpoint: GET https://api.kuti.pe/v1/events Lista los eventos de tu negocio. Del más reciente al más antiguo. Mismo modo (test/live) que tu API key. Nota: Filtra por type (p. ej. payment.succeeded). Útil para reconciliar o depurar entregas. GET /events --- ## Cotizar comisión URL: https://docs.kuti.pe/get-fee-preview Endpoint: GET https://api.kuti.pe/v1/fee-preview Estima la comisión de un cobro sin crearlo: tu cliente paga X, tú recibes Y. Nota: No persiste nada. Usa el plan de comisiones vigente del negocio. fee es negativa (IGV incluido). GET /fee-preview --- ## Detalle de negocio URL: https://docs.kuti.pe/get-merchant Endpoint: GET https://api.kuti.pe/v1/merchants/{id} Obtiene un merchant por id. Solo puedes leer el negocio asociado a tu API key. Nota: El id tiene el formato mer_… (p. ej. mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3). GET /merchants/{id} --- ## Listar negocios URL: https://docs.kuti.pe/get-merchants Endpoint: GET https://api.kuti.pe/v1/merchants Lista merchants con paginación por offset. Del más reciente al más antiguo según el orden del servidor. Nota: per_page acepta un entero 1–100 o el literal all para traer todos en una sola página (total_pages = 1). GET /merchants --- ## Detalle del cobro URL: https://docs.kuti.pe/get-payment-intent Endpoint: GET https://api.kuti.pe/v1/payment-intents/{id} Obtiene un payment intent por id. Incluye datos para pagar, fee_preview y, si ya fue pagado, paid_with + settlement. Nota: client_secret permite operar el cobro desde el frontend (Checkout.js) sin exponer tu secret key. Si status=SUCCEEDED, usa paid_with (no payment_method) para mostrar cómo se pagó. GET /payment-intents/{id} --- ## Listar cobros URL: https://docs.kuti.pe/get-payment-intents Endpoint: GET https://api.kuti.pe/v1/payment-intents Lista los payment intents (cobros) del merchant autenticado. Del más reciente al más antiguo. Nota: status: REQUIRES_PAYMENT_METHOD, PENDING, PROCESSING, SUCCEEDED, FAILED, CANCELLED, EXPIRED. El modo (test/live) lo define tu API key. GET /payment-intents --- ## Detalle del pago a tu cuenta URL: https://docs.kuti.pe/get-payout Endpoint: GET https://api.kuti.pe/v1/payouts/{id} Obtiene un payout por id (pyt_…): monto, estado y cuenta destino. GET /payouts/{id} --- ## Listar cuentas bancarias URL: https://docs.kuti.pe/get-payout-accounts Endpoint: GET https://api.kuti.pe/v1/payout-accounts Lista las cuentas de liquidación (destino de los pagos a tu cuenta) del modo actual. GET /payout-accounts --- ## Preferencias de liquidación URL: https://docs.kuti.pe/get-payout-settings Endpoint: GET https://api.kuti.pe/v1/payout-settings Consulta el calendario y umbrales de los pagos a tu cuenta. Nota: schedule: PROVIDER (calendario del proveedor), DAILY, WEEKLY o MANUAL. GET /payout-settings --- ## Movimientos de un payout URL: https://docs.kuti.pe/get-payout-transactions Endpoint: GET https://api.kuti.pe/v1/payouts/{id}/transactions Lista los movimientos del ledger incluidos en un pago a tu cuenta. GET /payouts/{id}/transactions --- ## Listar pagos a tu cuenta URL: https://docs.kuti.pe/get-payouts Endpoint: GET https://api.kuti.pe/v1/payouts Lista las transferencias del saldo de KUTI hacia tu cuenta bancaria (payouts). Nota: status: PENDING, IN_TRANSIT, PAID, FAILED, CANCELED. Del más reciente al más antiguo. GET /payouts --- ## Listar endpoints URL: https://docs.kuti.pe/get-webhook-endpoints Endpoint: GET https://api.kuti.pe/v1/webhook-endpoints Lista los webhook endpoints del merchant en el modo de tu API key (test o live). Nota: No incluye signing_secret (solo se muestra al crear). No mezcla endpoints de test y live. GET /webhook-endpoints --- ## Métodos de pago URL: https://docs.kuti.pe/metodos-de-pago Al crear un cobro eliges uno o ambos métodos. KUTI genera lo necesario para cada uno. El cliente paga con cualquiera; el cobro queda SUCCEEDED cuando se confirma el pago. Método | Código API | Qué ve el cliente QR interoperable | INTEROPERABLE_QR | Escanea el QR desde Yape, Plin u otra billetera Pago de servicios | BANK_TRANSFER | Ingresa un código en su banca buscando la institución KUTI En la banca y en Yape (pago de servicios) la institución/empresa a buscar es siempre KUTI. No muestres nombres de proveedores internos en tu UI. QR interoperable Respuesta en payment_method.qr: payload EMVCo. Con ese string dibujas el QR en tu UI (el API no entrega una imagen PNG). - Montos típicos de billetera: hasta S/ 500 por operación. - Sirve para Yape, Plin y billeteras de bancos. - No es un código de “pago de servicios”: es un QR de cobro. Pago de servicios (banca) Respuesta en payment_method.payment_code.code. El cliente abre su banca o app → Pago de servicios → busca KUTI → ingresa el código → confirma el monto. - Ideal para montos mayores (desde ~S/ 50 según tu plan de comisiones). - También disponible el atajo “Yape servicios” en el checkout hospedado (abre Yape con el código ya cargado). - Instrucciones al cliente: institución = KUTI. Ambos a la vez payment_method_types ```json { "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"] } ``` KUTI crea un cargo por método. El cliente elige cómo pagar; cuando uno se completa, el cobro pasa a SUCCEEDED. Checkout hospedado Si no quieres armar la UI, usa checkout_url (pay.kuti.pe/c/…) o Checkout.js. El checkout muestra pestañas Escanea el QR, Banca y Yape servicios. Siguiente - [Crear cobro](/post-payment-intents) — QR y/o banca - [Checkout.js](/checkout-js) — modal o embed - [Webhooks](/webhooks) — confirmar el pago --- ## Editar cliente URL: https://docs.kuti.pe/patch-customer Endpoint: PATCH https://api.kuti.pe/v1/customers/{id} Edita nombre, razón social, correo o teléfono. Solo los campos enviados se modifican. Nota: El tipo (INDIVIDUAL/COMPANY) y el documento no se pueden cambiar. Enviar "" borra el valor. PATCH /customers/{id} --- ## Editar negocio URL: https://docs.kuti.pe/patch-merchant Endpoint: PATCH https://api.kuti.pe/v1/merchants/{id} Edita datos del negocio: razón social, nombre comercial, contacto y dirección. Solo los campos enviados se modifican. Nota: El RUC, el país y la moneda no se pueden cambiar. Enviar "" borra el valor (excepto legal_name). PATCH /merchants/{id} --- ## Crear sesión de checkout URL: https://docs.kuti.pe/post-checkout-sessions Endpoint: POST https://api.kuti.pe/v1/checkout-sessions Crea una sesión de checkout (cargo único) y el payment intent asociado. Devuelve checkout_url para Checkout.js o pay.kuti.pe. Nota: Autoriza con secret o publishable key. amount.currency: solo PEN por ahora. Idempotency-Key recomendada. El checkout_url ya incluye el client_secret para abrir el modal/embed. POST /checkout-sessions --- ## Crear cliente URL: https://docs.kuti.pe/post-customers Endpoint: POST https://api.kuti.pe/v1/customers Crea un customer (pagador) para el merchant autenticado por tu API key. Nota: type es obligatorio: INDIVIDUAL (persona) o COMPANY (empresa). Persona usa first_name/last_name; empresa usa company_name. Puedes enviar Idempotency-Key (UUID v4). POST /customers --- ## Crear negocio URL: https://docs.kuti.pe/post-merchants Endpoint: POST https://api.kuti.pe/v1/merchants Crea un merchant a partir de un RUC. La razón social, el país (PE), la moneda (PEN) y la dirección fiscal se consultan en SUNAT y no se envían en el body. Nota: Solo tax_id.value es obligatorio. No envíes legal_name, country, address ni default_currency: salen de SUNAT. Si el RUC no existe, responde 422. Puedes enviar Idempotency-Key (UUID v4) para reintentos seguros. POST /merchants --- ## Anular cobro URL: https://docs.kuti.pe/post-payment-intent-cancel Endpoint: POST https://api.kuti.pe/v1/payment-intents/{id}/cancel Cancela un payment intent. Pasa a status CANCELLED. Nota: Si el cobro ya está pagado, procesando o en un estado que no admite anulación, responde 409. POST /payment-intents/{id}/cancel --- ## Enviar link por WhatsApp URL: https://docs.kuti.pe/post-payment-intent-send-whatsapp Endpoint: POST https://api.kuti.pe/v1/payment-intents/{id}/send-whatsapp Envía el link de pago del cobro por WhatsApp (plantilla fija). Responde 204 si se envió. Nota: Este envío consume 1 moneda de tu saldo WhatsApp. Si el cobro no tiene cliente con teléfono, phone es obligatorio. customer_name personaliza el mensaje. POST /payment-intents/{id}/send-whatsapp --- ## Crear cobro URL: https://docs.kuti.pe/post-payment-intents Endpoint: POST https://api.kuti.pe/v1/payment-intents Crea un payment intent (cobro). Devuelve QR interoperable, código de pago de servicios (institución KUTI) y link de checkout. Nota: Solo PEN · amount como string · en banca: institución KUTI. Preferí customer.id; si no, type + datos. Idempotency-Key recomendada. POST /payment-intents --- ## Registrar cuenta bancaria URL: https://docs.kuti.pe/post-payout-accounts Endpoint: POST https://api.kuti.pe/v1/payout-accounts Registra una cuenta bancaria de liquidación. El titular lo toma KUTI del negocio (razón social + RUC). Nota: cci debe ser 20 dígitos. account_type: CHECKING o SAVINGS. POST /payout-accounts --- ## Reintentar entrega URL: https://docs.kuti.pe/post-webhook-delivery-retry Endpoint: POST https://api.kuti.pe/v1/webhook-deliveries/{id}/retry Reencola una entrega de webhook para envío inmediato. Nota: Útil si tu endpoint falló y ya lo corregiste. El id es whd_… (no el evt_…). POST /webhook-deliveries/{id}/retry --- ## Crear endpoint URL: https://docs.kuti.pe/post-webhook-endpoints Endpoint: POST https://api.kuti.pe/v1/webhook-endpoints Registra una URL HTTPS para recibir eventos firmados de KUTI. Nota: signing_secret se devuelve SOLO en esta respuesta. El endpoint queda ligado al modo de tu API key (test o live). events: lista de tipos o ["*"] para todos. POST /webhook-endpoints --- ## Prueba vs producción URL: https://docs.kuti.pe/prueba-vs-produccion | Prueba | Producción API key | kuti_test_… / kuti_pub_test_… | kuti_live_… / kuti_pub_live_… Dinero real | No | Sí Clientes / cobros | Aislados | Aislados Webhooks | Solo eventos de prueba | Solo eventos live Requisito | Negocio creado | Sello KUTI habilitado Modo prueba Integra y demuestra el flujo completo sin mover dinero. En el dashboard elige Modo prueba. En checkout verás el banner de modo prueba. El cliente busca la institución KUTI; puedes simular el pago desde el dashboard cuando corresponda. Modo producción Para emitir claves live y operar cobros reales el negocio debe tener el sello KUTI habilitado (verificación documental en Ajustes → Verificación KUTI). Sin ese sello, la API responde KYB_REQUIRED al crear claves live o al llamar endpoints en live. No mezcles keys: un cobro creado con kuti_test_… nunca aparece con kuti_live_…. Registra un webhook por cada modo si necesitas ambos. Publishable keys kuti_pub_test_… / kuti_pub_live_… solo para el navegador (Checkout.js / crear sesión de checkout). Nunca expongas una secret key en el frontend. Siguiente - [Errores](/errores) — códigos y body - [Crear cobro](/post-payment-intents) — con kuti_test_… - [Webhooks](/webhooks) — payment.succeeded --- ## Actualizar preferencias URL: https://docs.kuti.pe/put-payout-settings Endpoint: PUT https://api.kuti.pe/v1/payout-settings Actualiza el calendario y umbrales de liquidación hacia tu cuenta bancaria. PUT /payout-settings --- ## SDKs oficiales URL: https://docs.kuti.pe/sdks Los SDKs de KUTI son delgados sobre la API REST. Solo servidor: usan tu secret key (kuti_live_… / kuti_test_…). Nunca los importes en el navegador — para el frontend usa Checkout.js. - Node.js: @kuti-pe/node — npm. Node 18+. - PHP: kuti-pe/kuti-php — Composer. PHP 8.1+. - Python: kuti-pe — PyPI. Python 3.9+. Qué cubren Cobro (payment intent) ≠ checkout session. Usa paymentIntents.create para cobro directo (QR / código / link). Usa checkoutSessions.create solo si vas a abrir el modal con Checkout.js. - paymentIntents: create, list, retrieve, cancel, sendWhatsApp - checkoutSessions: create (Checkout.js) - Webhooks: verificar firma (X-Kuti-Signature) Misma superficie en Node, PHP y Python: mismos recursos, mismos campos y mismos códigos de error. ## Node.js npm ```bash npm install @kuti-pe/node ``` Resuelve el monto en tu backend. Usa idempotencyKey (ej. id de orden) al crear. Crear sesión ```ts import { KutiClient } from "@kuti-pe/node"; const kuti = new KutiClient({ secretKey: process.env.KUTI_SECRET_KEY! }); const session = await kuti.checkoutSessions.create( { amount: { amount: "249.90", currency: "PEN" }, paymentMethodTypes: ["INTEROPERABLE_QR", "BANK_TRANSFER"], description: "Zapatillas running talla 42", customer: { id: "cus_01ABC" }, // customer: { name: "María López", email: "maria@example.com" }, }, { idempotencyKey: `order-${orderId}` }, ); // window.Kuti.open({ checkoutUrl: session.checkoutUrl, onSuccess, onFailure }) ``` Confirmar pago ```ts const intent = await kuti.paymentIntents.retrieve(paymentIntentId); if (intent.status === "SUCCEEDED") { // fulfill order } ``` Webhook (Express) ```ts import { verifyWebhookSignature, KutiSignatureVerificationError } from "@kuti-pe/node"; import express from "express"; const app = express(); app.post("/webhooks/kuti", express.text({ type: "*/*" }), (req, res) => { try { verifyWebhookSignature( req.body, // raw body req.header("X-Kuti-Signature")!, req.header("X-Kuti-Timestamp")!, process.env.KUTI_WEBHOOK_SECRET!, ); } catch (err) { if (err instanceof KutiSignatureVerificationError) { return res.status(400).send("Invalid signature"); } throw err; } const event = JSON.parse(req.body); // payment.succeeded | checkout.session.completed res.sendStatus(200); }); ``` - [npm — @kuti-pe/node](https://www.npmjs.com/package/@kuti-pe/node) - [Código — github.com/kuti-pe/kuti-node](https://github.com/kuti-pe/kuti-node) ## PHP Composer ```bash composer require kuti-pe/kuti-php ``` Resuelve el monto en tu backend. Usa idempotencyKey (ej. id de orden) al crear. Crear sesión ```php use Kuti\KutiClient; use Kuti\Money; use Kuti\PaymentMethodType; use Kuti\CheckoutSessionCustomer; $kuti = new KutiClient($_ENV['KUTI_SECRET_KEY']); $session = $kuti->checkoutSessions->create( amount: new Money('249.90', 'PEN'), paymentMethodTypes: [PaymentMethodType::InteroperableQr, PaymentMethodType::BankTransfer], customer: new CheckoutSessionCustomer(id: 'cus_01ABC'), // customer: new CheckoutSessionCustomer(name: 'María López', email: 'maria@example.com'), description: 'Zapatillas running talla 42', idempotencyKey: "order-{$orderId}", ); echo json_encode(['checkoutUrl' => $session->checkoutUrl]); ``` Confirmar pago ```php $intent = $kuti->paymentIntents->retrieve($paymentIntentId); if ($intent->isPaid()) { // fulfill order } ``` Webhook ```php use Kuti\Webhooks; use Kuti\Exception\KutiSignatureVerificationException; $payload = file_get_contents('php://input'); // raw body try { Webhooks::verifySignature( $payload, $_SERVER['HTTP_X_KUTI_SIGNATURE'], $_SERVER['HTTP_X_KUTI_TIMESTAMP'], $_ENV['KUTI_WEBHOOK_SECRET'], ); } catch (KutiSignatureVerificationException $e) { http_response_code(400); exit('Invalid signature'); } $event = json_decode($payload, true); // payment.succeeded | checkout.session.completed http_response_code(200); ``` - [Packagist — kuti-pe/kuti-php](https://packagist.org/packages/kuti-pe/kuti-php) - [Código — github.com/kuti-pe/kuti-php](https://github.com/kuti-pe/kuti-php) ## Python pip ```bash pip install kuti-pe ``` Resuelve el monto en tu backend. Usa idempotency_key (ej. id de orden) al crear. Crear sesión ```python import os from kuti import KutiClient kuti = KutiClient(os.environ["KUTI_SECRET_KEY"]) session = kuti.checkout_sessions.create( amount={"amount": "249.90", "currency": "PEN"}, payment_method_types=["INTEROPERABLE_QR", "BANK_TRANSFER"], description="Zapatillas running talla 42", customer={"id": "cus_01ABC"}, # customer={"name": "María López", "email": "maria@example.com"}, idempotency_key=f"order-{order_id}", ) # window.Kuti.open({ checkoutUrl: session.checkout_url, onSuccess, onFailure }) ``` Confirmar pago ```python intent = kuti.payment_intents.retrieve(payment_intent_id) if intent.status == "SUCCEEDED": # fulfill order pass ``` Webhook (Flask) ```python from flask import Flask, request from kuti import verify_webhook_signature, KutiSignatureVerificationError import os app = Flask(__name__) @app.post("/webhooks/kuti") def kuti_webhook(): payload = request.get_data(as_text=True) # raw body try: verify_webhook_signature( payload, request.headers["X-Kuti-Signature"], request.headers["X-Kuti-Timestamp"], os.environ["KUTI_WEBHOOK_SECRET"], ) except KutiSignatureVerificationError: return "Invalid signature", 400 event = request.get_json(force=True) # payment.succeeded | checkout.session.completed return "", 200 ``` - [PyPI — kuti-pe](https://pypi.org/project/kuti-pe/) - [Código — github.com/kuti-pe/kuti-python](https://github.com/kuti-pe/kuti-python) Siguiente - [Checkout.js](/checkout-js) — abrir el checkoutUrl en el frontend - [Webhooks](/webhooks) — confirmar el pago en tu servidor --- ## Webhooks URL: https://docs.kuti.pe/webhooks Flujo: registras un endpoint → ocurre un evento → KUTI hace POST a tu URL → respondes 2xx. Si falla, reintenta; puedes forzar un reenvío desde la API o el dashboard. El endpoint queda ligado al modo de la API key con la que lo creas: kuti_test_… solo recibe eventos de test; kuti_live_… solo de live. signing_secret se muestra una sola vez al crear (whsec_… / whsec_test_…). Headers de cada entrega Header | Qué es X-Kuti-Id | Id del evento (evt_…) X-Kuti-Timestamp | Epoch en segundos X-Kuti-Signature | v1= Firma: HMAC-SHA256(signing_secret, "."). Rechaza si |now − timestamp| > 5 minutos. Verificar firma (Node.js) ```javascript const crypto = require("crypto"); function verifyKutiWebhook(rawBody, headers, signingSecret) { const id = headers["x-kuti-id"]; const ts = headers["x-kuti-timestamp"]; const sig = headers["x-kuti-signature"]; // "v1=..." if (!id || !ts || !sig) return false; const age = Math.abs(Date.now() / 1000 - Number(ts)); if (age > 300) return false; // 5 min const expected = "v1=" + crypto .createHmac("sha256", signingSecret) .update(`${ts}.${rawBody}`) .digest("hex"); try { return crypto.timingSafeEqual( Buffer.from(sig), Buffer.from(expected) ); } catch { return false; } } ``` Envelope Todas las entregas usan el mismo envelope. data. es el objeto del GET de ese recurso (p. ej. data.payment_intent == GET /payment-intents/{id}, sin fee_preview ni settlement). GET /events/{id} devuelve este mismo body. Forma del body ```json { "id": "evt_…", "type": "payment.succeeded", "merchant_id": "mer_…", "created_at": "2026-09-04T10:55:12.000Z", "data": { "": { } } } ``` Campo | Qué es id | Id estable del evento (evt_…). Úsalo para ser idempotente. type | Tipo canónico (dot-case). No cambia una vez publicado. merchant_id | Negocio dueño del evento. created_at | UTC ISO-8601 con Z. Momento en que ocurrió. data. | payment_intent | customer | checkout_session | balance_transaction | payout El JSON real incluye campos en null (para que un mapper no se rompa si espera la clave). En los ejemplos de abajo omitimos los null. payment.* no trae fee_preview ni settlement: esos solo salen en GET /payment-intents/{id}. Tipos de evento (v1) payment.* es el ciclo de vida del cobro. Ya no existen payment_intent.* ni payment.pending. Para marcar un pedido como pagado: payment.succeeded. Evento | Cuándo | data payment.created | Creaste el cobro (QR / código / link). | payment_intent payment.succeeded | El cliente pagó. Marca el pedido. | payment_intent payment.failed | El pago fue rechazado. | payment_intent payment.expired | Pasó expires_at sin pagar. | payment_intent payment.cancelled | Anulaste el cobro. | payment_intent checkout.session.completed | El pagador terminó el checkout embebido. | checkout_session customer.created | Se creó un cliente (API o al crear el cobro). | customer customer.updated | Editaste el cliente. | customer customer.deleted | Archivaste el cliente. | customer balance.transaction_created | El cobro pagado entró al ledger (PENDING). | balance_transaction balance.available | Ese dinero ya se puede pagar a tu CCI. | balance_transaction payout.created | Armamos un depósito a tu cuenta. | payout payout.paid | El depósito llegó a tu CCI. | payout payout.failed | El depósito falló. | payout Al crear el endpoint, events es un array de esos tipos, o ["*"] para todos. Integración mínima: ["payment.succeeded", "payment.failed"]. payment.created Cobro recién creado, esperando pago. Trae payment_method (QR y/o código) y client_secret / checkout_url. payment.created ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V1", "type": "payment.created", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T10:48:59.556906Z", "data": { "payment_intent": { "id": "pi_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "livemode": true, "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "INDIVIDUAL", "first_name": "María", "last_name": "López", "name": "María López", "document_type": "DNI", "document_value": "45678912", "email": "maria@example.com" }, "amount": { "amount": "50.00", "currency": "PEN" }, "status": "PENDING", "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "payment_method": { "qr": { "type": "INTEROPERABLE_QR", "payload": "00020101021226…", "expires_at": "2026-09-05T23:59:59Z" }, "payment_code": { "type": "BANK_TRANSFER", "code": "41041172", "expires_at": "2026-09-05T23:59:59Z" } }, "checkout_url": "https://pay.kuti.pe/c/A3F9K2P7QM", "client_secret": "pi_01J8…_secret_ab12cd34ef56gh78ij90kl12", "description": "Pedido #1042", "external_reference": "order-1042", "requires_customer_info": false, "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` payment.succeeded El cliente pagó. Este es el evento para marcar el pedido. paid_with dice con qué método; payment_id es el pago (pay_…). Ya no viene client_secret ni payment_method. payment.succeeded ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V2", "type": "payment.succeeded", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T10:55:12.000Z", "data": { "payment_intent": { "id": "pi_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "livemode": true, "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "INDIVIDUAL", "first_name": "María", "last_name": "López", "name": "María López", "document_type": "DNI", "document_value": "45678912", "email": "maria@example.com" }, "amount": { "amount": "50.00", "currency": "PEN" }, "status": "SUCCEEDED", "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "paid_with": { "method_type": "INTEROPERABLE_QR", "paid_at": "2026-09-04T10:55:12.000Z" }, "payment_id": "pay_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "checkout_url": "https://pay.kuti.pe/c/A3F9K2P7QM", "description": "Pedido #1042", "external_reference": "order-1042", "requires_customer_info": false, "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` payment.failed El cobro quedó FAILED (el pago fue rechazado). Usa external_reference o id para hallar el pedido. Si el cliente reintenta y paga, te llega payment.succeeded aparte. payment.failed ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "payment.failed", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T10:56:02.000Z", "data": { "payment_intent": { "id": "pi_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "livemode": true, "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "INDIVIDUAL", "first_name": "María", "last_name": "López", "name": "María López", "document_type": "DNI", "document_value": "45678912", "email": "maria@example.com" }, "amount": { "amount": "50.00", "currency": "PEN" }, "status": "FAILED", "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "payment_method": { "qr": { "type": "INTEROPERABLE_QR", "payload": "00020101021226…", "expires_at": "2026-09-05T23:59:59Z" }, "payment_code": { "type": "BANK_TRANSFER", "code": "41041172", "expires_at": "2026-09-05T23:59:59Z" } }, "checkout_url": "https://pay.kuti.pe/c/A3F9K2P7QM", "description": "Pedido #1042", "external_reference": "order-1042", "requires_customer_info": false, "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` payment.expired Pasó expires_at sin pago. Un cobro sin expires_at no expira. Si el dinero llega después, KUTI puede recuperarlo a SUCCEEDED (te llega payment.succeeded). payment.expired ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V4", "type": "payment.expired", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-05T23:59:59.000Z", "data": { "payment_intent": { "id": "pi_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "livemode": true, "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "INDIVIDUAL", "first_name": "María", "last_name": "López", "name": "María López", "document_type": "DNI", "document_value": "45678912", "email": "maria@example.com" }, "amount": { "amount": "50.00", "currency": "PEN" }, "status": "EXPIRED", "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "checkout_url": "https://pay.kuti.pe/c/A3F9K2P7QM", "expires_at": "2026-09-05T23:59:59Z", "description": "Pedido #1042", "external_reference": "order-1042", "requires_customer_info": false, "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` payment.cancelled Anulaste el cobro (POST /payment-intents/{id}/cancel). El QR y el código dejan de cobrar. payment.cancelled ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V5", "type": "payment.cancelled", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T11:10:00.000Z", "data": { "payment_intent": { "id": "pi_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "livemode": true, "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "INDIVIDUAL", "first_name": "María", "last_name": "López", "name": "María López", "document_type": "DNI", "document_value": "45678912", "email": "maria@example.com" }, "amount": { "amount": "50.00", "currency": "PEN" }, "status": "CANCELLED", "payment_method_types": ["INTEROPERABLE_QR", "BANK_TRANSFER"], "checkout_url": "https://pay.kuti.pe/c/A3F9K2P7QM", "description": "Pedido #1042", "external_reference": "order-1042", "requires_customer_info": false, "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` checkout.session.completed El pagador terminó el checkout embebido (Checkout.js). El cobro pagado llega aparte como payment.succeeded — usa payment_intent_id para cruzarlos. checkout.session.completed ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V6", "type": "checkout.session.completed", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T10:55:12.000Z", "data": { "checkout_session": { "id": "cs_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "customer_id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "payment_intent_id": "pi_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "amount": { "amount": "50.00", "currency": "PEN" }, "status": "COMPLETED", "description": "Pedido #1042", "success_url": "https://tienda.pe/pago/ok", "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` customer.created Se creó un cliente: POST /customers o al crear un cobro con datos nuevos. customer.updated y customer.deleted usan el mismo data.customer (updated = datos nuevos; deleted = el que se archivó). No incluye payment_intents_count (eso solo sale en GET /customers/{id}). customer.created ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V7", "type": "customer.created", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T10:48:59.556906Z", "data": { "customer": { "id": "cus_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "external_id": "cust_erp_4821", "type": "INDIVIDUAL", "first_name": "María", "last_name": "López", "document": { "country": "PE", "type": "DNI", "value": "45678912" }, "email": "maria@example.com", "phone": "+51987654321", "created_at": "2026-09-04T10:48:59.556906Z" } } } ``` balance.transaction_created El cobro pagado entró al ledger. status = PENDING hasta que el dinero quede disponible; entonces llega balance.available con el mismo objeto y status = AVAILABLE. source_id es el pago (pay_…). balance.transaction_created ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V8", "type": "balance.transaction_created", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-04T10:55:12.000Z", "data": { "balance_transaction": { "id": "btxn_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "type": "PAYMENT", "status": "PENDING", "currency": "PEN", "payment_method": "INTEROPERABLE_QR", "source_type": "PAYMENT", "source_id": "pay_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "gross": { "amount": "50.00", "currency": "PEN" }, "fee": { "amount": "-1.77", "currency": "PEN" }, "fee_tax": { "amount": "-0.27", "currency": "PEN" }, "net": { "amount": "48.23", "currency": "PEN" }, "available_on": "2026-09-06T00:00:00Z", "available_confirmed": false, "created_at": "2026-09-04T10:55:12.000Z" } } } ``` payout.paid El depósito a tu CCI se completó. payout.created y payout.failed usan el mismo data.payout (status PENDING o FAILED; si falló vienen failure_code y failure_message). payout.paid ```json { "id": "evt_01J8Z3K4M5N6P7Q8R9S0T1U2V9", "type": "payout.paid", "merchant_id": "mer_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "created_at": "2026-09-05T10:15:00.000Z", "data": { "payout": { "id": "pyt_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "source": "PROVIDER", "amount": { "amount": "1250.50", "currency": "PEN" }, "gross_amount": { "amount": "1280.00", "currency": "PEN" }, "fee_amount": { "amount": "-29.50", "currency": "PEN" }, "destination_bank": "BCP", "destination_account_masked": "••••••••9012", "status": "PAID", "payout_account_id": "pacc_01J8Z3K4M5N6P7Q8R9S0T1U2V3", "statement_descriptor": "KUTI", "arrival_expected_on": "2026-09-05", "initiated_at": "2026-09-04T18:00:00Z", "paid_at": "2026-09-05T10:15:00Z", "created_at": "2026-09-04T18:00:00Z" } } } ``` Buenas prácticas - Responde 2xx rápido; procesa en cola si hace falta. - Usa X-Kuti-Id (o el id del evento) para ser idempotente. - Verifica siempre la firma con el body crudo (raw body). - HTTPS obligatorio en la URL del endpoint. - Para saber si pagó: mira type === "payment.succeeded" y data.payment_intent.external_reference (o id). Siguiente - [Crear endpoint](/post-webhook-endpoints) — POST /webhook-endpoints - [Listar eventos](/get-events) — reconciliar entregas - [Crear cobro](/post-payment-intents) — disparar payment.succeeded ---