# Documentación de Lane — completa > Toda la documentación técnica de Lane en un archivo, en texto plano. > Generado desde el mismo contenido que se publica en https://lane.money/docs. --- # Documentación de Lane > API REST para cobrar en Colombia: links de pago, checkout, medios instantáneos, cobro recurrente y webhooks firmados. Con sandbox y llaves de prueba. Una API para mover plata en Colombia. Creas un cobro, mandas al comprador a pagarlo y te avisamos cuando queda aprobado. Lane expone un solo servicio HTTP con recursos en `/v1`. Hablas con él desde tu servidor con una llave secreta, y él se encarga del resto: la pantalla de pago, la comunicación con la red de tarjetas y el aviso de vuelta. **Un cobro completo** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 11900000, "currency": "COP", "description": "Pedido #4821" }' ``` _Respuesta 201_ ```json { "id": "3f0c…", "object": "checkout.session", "environment": "test", "amount": 11900000, "currency": "COP", "status": "pending", "checkout_url": "https://lane.money/l/a7Kd2", "payment": { "id": "9b41…", "status": "pending" } } ``` ## Por dónde empezar - [Tu primer cobro](https://lane.money/docs/empezar): De cero a un pago aprobado en sandbox, sin escribir una línea de código de tu lado. - [Llaves y entornos](https://lane.money/docs/autenticacion): Cuál llave usar dónde, y por qué la secreta nunca sale del servidor. - [Montos](https://lane.money/docs/montos): Todo va en centavos. Es el error de integración más común y el más caro. - [Webhooks](https://lane.money/docs/webhooks): Cómo te avisamos que un cobro cambió de estado, y cómo verificas que fuimos nosotros. ## Las tres formas de cobrar por API | Camino | Cuándo sirve | Qué le llega al comprador | | --- | --- | --- | | [Link de pago](https://lane.money/docs/guias/link-de-pago) | Ventas por WhatsApp, cotizaciones, cobros sueltos. | Una URL corta que abre la pantalla de pago. | | [Checkout](https://lane.money/docs/guias/checkout) | Una tienda o una app que ya tiene carrito. | La misma pantalla, con vuelta automática a tu sitio. | | [Tarjeta guardada](https://lane.money/docs/guias/cobro-recurrente) | Mensualidades, membresías, recompras con un clic. | Nada: el cobro sale de tu servidor contra una tarjeta ya autorizada. | ## Lo que no hay Preferimos decirlo acá y no que lo descubras integrando. Hoy la API **no** tiene claves de idempotencia, ni reglas antifraude configurables (solo 3DS), ni consulta de liquidaciones: puedes ver cuándo un cobro quedó compensado por la red, pero no cuándo cae la plata en tu cuenta. > **Nota: ¿Estás integrando con un asistente?.** Cada página de esta documentación existe también en markdown limpio: agrégale `.md` a la URL. El índice completo está en [/docs/llms.txt](https://lane.money/docs/llms.txt) y todo junto en un solo archivo en [/docs/llms-full.txt](https://lane.money/docs/llms-full.txt). --- # Tu primer cobro > Crea una llave de prueba, genera un link de pago y págalo en sandbox. El camino más corto para ver la API funcionando de punta a punta. Cinco pasos hasta ver un cobro aprobado. Todo en sandbox, sin plata real. ## 1. Activa tu comercio de prueba En el panel, entra a **Configuración → Comercio y sandbox** y registra tu comercio. Sin este paso la API responde, pero el link de pago no se puede crear: no hay a nombre de quién cobrar. ## 2. Crea una llave secreta En **Desarrolladores → Llaves de API**, crea una llave `sk_test_…`. Se muestra una sola vez: guárdala apenas la veas. Si la pierdes, revócala y crea otra. ## 3. Crea el cobro Un link de pago es la forma más corta de probar: no necesita carrito ni página de retorno. El monto va en centavos, así que `11900000` son $ 119.000 COP. **Crear el link** _cURL_ ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 11900000, "currency": "COP", "description": "Pedido #4821" }' ``` _Node_ ```js const res = await fetch("https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links", { method: "POST", headers: { Authorization: `Bearer ${process.env.LANE_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 11_900_000, // $ 119.000 COP currency: "COP", description: "Pedido #4821", }), }); const session = await res.json(); console.log(session.checkout_url); ``` _Python_ ```python import os, requests res = requests.post( "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links", headers={"Authorization": f"Bearer {os.environ['LANE_SECRET_KEY']}"}, json={ "amount": 11_900_000, # $ 119.000 COP "currency": "COP", "description": "Pedido #4821", }, ) print(res.json()["checkout_url"]) ``` _PHP_ ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("LANE_SECRET_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 11900000, // $ 119.000 COP "currency" => "COP", "description" => "Pedido #4821", ]), ]); $session = json_decode(curl_exec($ch), true); echo $session["checkout_url"]; ``` _Respuesta 201_ ```json { "id": "3f0c8a1e-…", "object": "checkout.session", "environment": "test", "amount": 11900000, "currency": "COP", "status": "pending", "checkout_url": "https://lane.money/l/a7Kd2", "payment": { "id": "9b41d0c7-…", "status": "pending" } } ``` ## 4. Pagalo Abre el `checkout_url` en el navegador y paga con una tarjeta de prueba. En sandbox no se mueve plata. | Para qué | Número | Vence | CVV | | --- | --- | --- | --- | | Aprobar un cobro (débito) | `4111111111111111` | 12/30 | 123 | | Consultar un BIN de crédito | `518617` · `541333` | — | — | > **Cuidado: La tarjeta de prueba es débito.** Sirve para aprobar un cobro, pero no para probar cuotas: el emisor puede aprobar con una sola cuota aunque pidas tres. Para [cuotas](https://lane.money/docs/guias/cuotas) necesitas una tarjeta de crédito. Y no hay tarjetas de rechazo con un número fijo: pídele a soporte los datos que fuerzan un rechazo en tu entorno. ## 5. Verificá el resultado Consulta el cobro con el `id` que te devolvió `payment.id`. Debería estar en `succeeded`. **Consultar el cobro** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments/9b41d0c7-…" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "payment", "id": "9b41d0c7-…", "amount": 11900000, "currency": "COP", "status": "succeeded", "payment_method": "CARD", "processed_at": "2026-09-06T18:22:41.000Z" } ``` Eso es todo el ciclo. De acá en adelante, lo que cambia es cómo llega el comprador a la pantalla de pago y cómo te enterás del resultado sin preguntar. ## El siguiente paso - [Dejá de preguntar](https://lane.money/docs/webhooks): Configura un webhook y entérate del cambio de estado en el momento. - [Mételo en tu tienda](https://lane.money/docs/guias/checkout): El checkout devuelve al comprador a tu sitio cuando termina. --- # Llaves y entornos > Cómo autenticar contra la API de Lane con llaves secretas, qué diferencia hay entre sandbox y producción, y qué hacer si una llave se filtra. Toda petición lleva una llave secreta en la cabecera. La llave decide el comercio y el entorno. La API usa `Bearer` sobre HTTPS. No hay firma de petición ni nada que calcular: mandas la llave tal cual. **Cabecera** ```http Authorization: Bearer sk_test_4f8a… ``` ## Los dos entornos El entorno no es un parámetro ni una URL distinta: **lo determina la llave**. La misma ruta con una `sk_test_` cobra en sandbox y con una `sk_live_` cobra de verdad. | Prefijo | Entorno | Dónde vive | | --- | --- | --- | | `sk_test_` | Sandbox. No se mueve plata. | Tu servidor y tu entorno de desarrollo. | | `sk_live_` | Producción. Se mueve plata real. | Solo tu servidor de producción. | | `pk_test_` · `pk_live_` | Llaves públicas, para el navegador. | Pueden ir en el front. No sirven para cobrar. | > **Importante: La llave secreta no toca el navegador.** Una `sk_` en el front es una llave publicada: cualquiera que abra las herramientas de desarrollo puede cobrar, reembolsar y leer tus clientes. Si eso pasa, revócala en el panel y crea otra — no hay forma de rotarla sin cortar. ## Crear y revocar Las llaves se crean en **Desarrolladores → Llaves de API**. Se guardan cifradas, así que el valor completo se muestra una sola vez, al crearla. Después solo vas a ver los últimos caracteres. ## Varias tiendas, varias llaves Una cuenta puede tener varios comercios. Cada llave queda atada a uno, y los cobros hechos con esa llave se le acreditan a ese comercio. Para ver cuáles tienes, lista los comercios. **Listar comercios** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/merchants" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "has_more": false, "next_cursor": null, "data": [ { "id": "b21f…", "name": "Café Vecino", "alias": "cafe-vecino", "default_currency": "COP" } ] } ``` ## Cuando la llave no sirve | HTTP | Cuerpo | Qué pasó | | --- | --- | --- | | `401` | `{"error":"Missing or invalid secret key"}` | Falta la cabecera, o no empieza con `Bearer sk_`. | | `401` | `{"error":"Invalid API key"}` | La llave no existe o fue revocada. | | `401` | `{"error":"API key has expired"}` | La llave tenía fecha de vencimiento y ya pasó. | El resto de los códigos está en [Errores](https://lane.money/docs/errores). --- # Montos y monedas > Los montos de la API de Lane van en centavos, como enteros. Tabla de conversión, moneda por defecto y los topes por medio de pago. Todos los montos son enteros en centavos. No hay decimales en ningún campo de plata. > **Importante: Cien veces.** Mandar `119000` en vez de `11900000` cobra $ 1.190 en lugar de $ 119.000. La API no puede detectarlo: los dos son montos válidos. Es el error de integración más común y el más caro. La conversión es siempre la misma: `amount = pesos × 100`. El peso colombiano no se fracciona en la práctica, pero el campo se expresa igual en centavos para que la misma API sirva en los mercados que sí lo hacen. | Lo que ve el comprador | Lo que mandas | | --- | --- | | $ 1.000 COP | `100000` | | $ 10.000 COP | `1000000` | | $ 119.000 COP | `11900000` | | $ 1.500.000 COP | `150000000` | ## Moneda `currency` es opcional y por defecto es `COP`. Hoy se liquida en Colombia; se aceptan tarjetas emitidas en cualquier país, pero el cobro se hace en pesos. ## Topes por medio de pago Las tarjetas no tienen tope de red: el límite lo pone el emisor. Los medios instantáneos sí, y un monto fuera de rango devuelve `422`. | Medio | Mínimo | Máximo | | --- | --- | --- | | `pse` | — | Sin tope de red | | `nequi` | — | ~$ 12.000.000 COP | | `daviplata` | — | $ 3.000.000 COP | | `breb` | $ 1.000 COP | — | Cómo se cobra con cada uno está en [Medios instantáneos](https://lane.money/docs/guias/medios-instantaneos). ## Cero como monto Solo un caso admite `amount: 0`: verificar que una tarjeta guardada sigue viva, con `intent: "account-status"`. No mueve plata y no le aparece nada al tarjetahabiente. Cualquier otro endpoint rechaza el cero con `400`. --- # El recorrido de un cobro > Los siete estados por los que pasa un pago en Lane: pending, authorized, processing, succeeded, failed, cancelled y refunded. Cuál es final y cuál no. Un cobro es una máquina de estados. Saber en cuál está te dice qué puedes hacer con él. _Recorrido de un cobro: pending → authorized → processing → succeeded, o failed / cancelled / refunded. Esta sección documenta el estado `succeeded`._ El campo `status` aparece en todas las respuestas de pago y es el mismo vocabulario en toda la API: en el objeto que devuelve un cobro, en el listado y en el nombre del evento del webhook (`payment.succeeded`, `payment.failed`). ## Los siete estados | Estado | Qué pasó | ¿Final? | | --- | --- | --- | | `pending` | El cobro existe pero nadie lo ha pagado todavía. Es el estado en el que nace. | No | | `authorized` | El emisor reservó la plata, pero aún no se cobró. Solo ocurre con captura manual. | No | | `processing` | La captura está en curso. No hace falta hacer nada: va a resolverse sola. | No | | `succeeded` | Aprobado. Es el único estado en el que puedes despachar. | Sí | | `failed` | El emisor rechazó. La plata nunca se movió. | Sí | | `cancelled` | Se anuló antes de capturar. Se liberó la reserva. | Sí | | `refunded` | Se devolvió la plata al comprador, total o parcialmente. | Sí | > **Importante: Despacha solo en `succeeded`.** Volver a tu `success_url` no significa que el cobro se aprobó: significa que el comprador volvió. La única fuente de verdad es el `status` del cobro, y la forma correcta de conocerlo es el [webhook](https://lane.money/docs/webhooks). ## Qué se puede hacer en cada estado | Operación | Estado requerido | Resultado | | --- | --- | --- | | `POST /v1/payments/{id}/capture` | `authorized` | Cobra la plata reservada → `succeeded` | | `POST /v1/payments/{id}/cancel` | `authorized` | Libera la reserva → `cancelled` | | `POST /v1/payments/{id}/adjust` | `authorized` | Cambia el monto antes de capturar | | `POST /v1/payments/{id}/refund` | `succeeded` | Devuelve la plata → `refunded` | | `POST /v1/payments/{id}/cancel-or-refund` | `authorized` o `succeeded` | Elige la operación correcta según el estado | Si no sabes en cuál de los dos está —porque la captura fue automática y pasó rápido— usa `cancel-or-refund` y deja que el servidor decida. Es una llamada menos y no puede equivocarse. ## Compensado no es liquidado Un cobro `succeeded` puede traer `cleared_at`: la fecha en que la red compensó la transacción. **No es la fecha en la que la plata cae en tu cuenta.** El depósito depende del calendario de liquidación de tu comercio, y la API no lo expone. ## Los medios instantáneos no se devuelven PSE, Nequi, Daviplata y Bre-B mueven la plata en el momento y son irrevocables: un cobro por esos medios no admite `refund` ni `cancel`. Para devolver el dinero tienes que hacer una transferencia aparte al comprador. Está explicado en [Medios instantáneos](https://lane.money/docs/guias/medios-instantaneos). ## Forzar una actualización Consultar un cobro lee lo que Lane tiene guardado. Si sospechas que quedó atrás, agrega `?sync=true` y se consulta contra el procesador antes de responder. Es más lento; úsalo para depurar, no en cada carga de página. **Consultar sincronizando** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments/9b41d0c7-…?sync=true" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "payment", "id": "9b41d0c7-…", "amount": 11900000, "currency": "COP", "status": "succeeded", "status_detail": "CAPTURED", "payment_method": "CARD", "cleared_at": "2026-09-07T04:00:00.000Z", "processed_at": "2026-09-06T18:22:41.000Z" } ``` --- # Errores > Los códigos HTTP que devuelve la API de Lane, qué significa cada uno y cuáles conviene reintentar. Todos los errores tienen el mismo cuerpo: un objeto JSON con la llave `error` y una frase en inglés. **Forma de un error** ```json { "error": "amount must be a positive integer in minor units" } ``` El mensaje describe el problema con precisión y está pensado para tu log, no para mostrárselo al comprador. Cuando el error viene del procesador, la respuesta trae además un `message` con el detalle. ## Códigos | Código | Significa | ¿Reintentar? | | --- | --- | --- | | `400` | El cuerpo no cumple el contrato: falta un campo, sobra otro o el tipo no corresponde. | No, hasta corregirlo | | `401` | La llave falta, no existe o venció. Ver [Llaves y entornos](https://lane.money/docs/autenticacion). | No | | `404` | `{"error":"Route not found"}` — la ruta no existe. También cuando el recurso no es de tu organización. | No | | `422` | El contrato está bien pero la operación es imposible: monto fuera del tope de un medio instantáneo, por ejemplo. | No | | `500` | Algo se rompió de nuestro lado. | Sí, con espera creciente | > **Cuidado: Un recurso ajeno responde 404, no 403.** Si pides un cobro que existe pero es de otra organización, la API contesta `404`. Es a propósito: un `403` confirmaría que ese identificador existe. ## Errores de validación más frecuentes | Mensaje | Qué corregir | | --- | --- | | `amount must be a positive integer in minor units` | Manda centavos, como entero. Ver [Montos](https://lane.money/docs/montos). | | `instrument_id or provider_instrument_id is required` | Un cobro contra tarjeta guardada necesita saber qué tarjeta. | | `cvv is required when initiator is customer (CIT)` | Si el cliente está presente, el CVV es obligatorio. | | `cvv must not be sent when initiator is merchant (MIT)` | En un cobro automático no hay quien lo escriba: no lo mandes. | | `parent_transaction_data is required for series MIT` | Un cobro recurrente tiene que apuntar al primero de la serie. | | `installments.quantity must be an integer >= 2` | Para un pago sin cuotas, omite el objeto `installments`. | | `success_url must be a valid URL` | Tiene que ser `http` o `https` absoluta. | ## Reintentos La API no tiene claves de idempotencia. Un `POST` reintentado crea un cobro nuevo, no repite el anterior. Por eso solo conviene reintentar cuando la petición **no llegó** —un timeout de red o un `500`— y aún así vale la pena guardar tu propio identificador en `metadata` para poder detectar el duplicado. **Tu identificador viaja en metadata** ```json { "amount": 11900000, "metadata": { "order_id": "4821" } } ``` Después puedes buscarlo: `metadata` vuelve en el objeto del cobro y en el evento del webhook, tal como lo mandaste. --- # Cobrar con un link de pago > Crea una URL de pago con la API de Lane, con vencimiento, cupo de usos y QR. La forma más corta de cobrar sin tener tienda online. Una URL corta que abre la pantalla de pago. Se manda por WhatsApp, se pega en una factura o se imprime como QR. Un link de pago no necesita que tengas sitio web. Lo creas desde tu servidor, se lo pasas al comprador y Lane se encarga de cobrarle. ### POST /v1/payment_links Crea el link y devuelve su URL. **Cuerpo** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `amount` | entero | sí | En centavos. `11900000` son $ 119.000 COP. | | `currency` | texto | no | Por defecto `COP`. | | `description` | texto | no | Lo que ve el comprador en la pantalla de pago. | | `expires_in` | entero | no | Segundos de vida del link. Por defecto `43200` (12 horas). | | `multi_use` | booleano | no | `false` (por defecto) lo cierra después del primer pago. `true` lo deja abierto. | | `max_uses` | entero | no | Cuántos pagos admite si `multi_use` es `true`. Omitido, no tiene tope. | | `customer` | objeto | no | Datos del comprador. Con `email` alcanza para que quede asociado a un cliente. | | `metadata` | objeto | no | Lo que quieras guardar. Vuelve intacto en el cobro y en el webhook. | **Crear un link** _cURL_ ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 4890000, "currency": "COP", "description": "Pedido #4821", "expires_in": 86400, "customer": { "email": "cliente@ejemplo.com" }, "metadata": { "order_id": "4821" } }' ``` _Node_ ```js const res = await fetch("https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links", { method: "POST", headers: { Authorization: `Bearer ${process.env.LANE_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: 4_890_000, // $ 48.900 COP currency: "COP", description: "Pedido #4821", expires_in: 86_400, // 24 horas customer: { email: "cliente@ejemplo.com" }, metadata: { order_id: "4821" }, }), }); const { checkout_url, payment } = await res.json(); // Guarda payment.id contra tu pedido: es como vas a reconocer el webhook. ``` _Respuesta 201_ ```json { "id": "3f0c8a1e-…", "object": "checkout.session", "environment": "test", "amount": 4890000, "currency": "COP", "status": "pending", "checkout_url": "https://lane.money/l/a7Kd2", "payment": { "id": "9b41d0c7-…", "status": "pending" } } ``` > **Nota: Guarda `payment.id`, no solo la URL.** El webhook llega con el identificador del cobro. Si no lo guardaste contra tu pedido, vas a recibir un aviso de que alguien pagó sin saber qué. ## Un link para varios pagos Por defecto el link se cierra con el primer pago. Para un QR pegado en el mostrador o un cobro de inscripción que muchas personas pagan, ábrelo con `multi_use`. **Link reutilizable con tope** ```json { "amount": 15000000, "description": "Inscripción torneo", "multi_use": true, "max_uses": 40, "expires_in": 604800 } ``` ## Refrescar el estado Si el webhook no llegó o quieres reconciliar a mano, pide una sincronización del link contra el procesador. ### POST /v1/payment_links/{id}/sync Vuelve a leer el estado del link y actualiza el cobro. **Sincronizar** ```bash curl -X POST "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payment_links/3f0c8a1e-…/sync" \ -H "Authorization: Bearer sk_test_…" ``` ## La pantalla de pago El `checkout_url` abre una página de Lane con el nombre del negocio, el monto y el detalle del cobro. Hoy su apariencia no es configurable por API: el comercio que aparece y el logo salen de lo que cargaste en el panel. Para cobrar con PSE, Nequi, Daviplata o Bre-B en vez de tarjeta, mira [Medios instantáneos](https://lane.money/docs/guias/medios-instantaneos). --- # Checkout en tu tienda > Integra el cobro en el carrito de tu tienda: crea una sesión de checkout, manda al comprador a pagar y recíbelo de vuelta en tu sitio. Igual que un link de pago, pero con vuelta a tu sitio cuando el comprador termina. La sesión de checkout y el link de pago comparten contrato: mismo cuerpo, misma respuesta. La diferencia es que acá defines a dónde vuelve el comprador. ### POST /v1/checkout/sessions Crea la sesión y devuelve la URL a la que mandas al comprador. **Además de los campos del link** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `success_url` | URL | no | A dónde vuelve el comprador después de pagar. | | `cancel_url` | URL | no | A dónde vuelve si abandona. | | `error_url` | URL | no | A dónde vuelve si el emisor rechaza la tarjeta. | | `line_items` | lista | no | El detalle del carrito, si quieres conservarlo con el cobro. | ## Las URL de retorno admiten marcadores Lane reemplaza `{PAYMENT_ID}` y `{CHECKOUT_SESSION_ID}` en tus URL de retorno antes de mandar al comprador. Así tu página de gracias sabe de qué cobro habla sin que tengas que arrastrar el identificador por la sesión. **Crear la sesión** _cURL_ ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/checkout/sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 11900000, "currency": "COP", "description": "Pedido #4821", "success_url": "https://mitienda.com/gracias?pago={PAYMENT_ID}", "cancel_url": "https://mitienda.com/carrito", "error_url": "https://mitienda.com/carrito?error=1", "customer": { "email": "cliente@ejemplo.com" }, "metadata": { "order_id": "4821" } }' ``` _Node_ ```js // En tu servidor, cuando el comprador aprieta "Pagar". app.post("/checkout", async (req, res) => { const orden = await crearOrden(req.body); const lane = await fetch("https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/checkout/sessions", { method: "POST", headers: { Authorization: `Bearer ${process.env.LANE_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: orden.totalCentavos, currency: "COP", description: `Pedido #${orden.numero}`, success_url: `https://mitienda.com/gracias?pago={PAYMENT_ID}`, cancel_url: "https://mitienda.com/carrito", metadata: { order_id: orden.id }, }), }).then((r) => r.json()); await orden.guardarPagoId(lane.payment.id); res.redirect(303, lane.checkout_url); }); ``` _Respuesta 201_ ```json { "id": "3f0c8a1e-…", "object": "checkout.session", "environment": "live", "amount": 11900000, "currency": "COP", "status": "pending", "checkout_url": "https://lane.money/l/a7Kd2", "payment": { "id": "9b41d0c7-…", "status": "pending" } } ``` > **Importante: La página de gracias no confirma nada.** Volver a tu `success_url` prueba que el navegador volvió, no que el cobro se aprobó. Un comprador puede cerrar la pestaña antes de volver, o escribir la URL a mano. Marca el pedido como pagado cuando llegue `payment.succeeded` por [webhook](https://lane.money/docs/webhooks), y usa la página de gracias solo para mostrar el estado. ## Embeber en vez de redirigir La respuesta trae `embed_url`, que es la misma URL, pensada para usarla como `src` de un iframe si prefieres que el comprador no salga de tu sitio. La pantalla mide su propio ancho, así que dale al iframe el ancho completo del contenedor. ## Ya tienes tienda en Shopify Si tu tienda es Shopify, no hace falta escribir nada de esto: la conexión se hace desde el panel. La API es para tiendas propias o para cuando necesitas control sobre el flujo. --- # Medios instantáneos > Cobra con PSE, Nequi, Daviplata o Bre-B desde la API de Lane. Cómo paga el comprador con cada uno, los topes de red y por qué no admiten reembolso. Cuatro rieles que mueven la plata en el momento, sin tarjeta. Se piden con un solo campo. Agrega `payment_method` al crear el cobro y la pantalla de pago cambia: en vez de pedir una tarjeta, pide únicamente lo que ese riel necesita. **Cobrar por PSE** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/checkout/sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 25000000, "currency": "COP", "payment_method": "pse", "description": "Pedido #4821", "success_url": "https://mitienda.com/gracias?pago={PAYMENT_ID}" }' ``` _Respuesta 201_ ```json { "id": "3f0c8a1e-…", "object": "checkout.session", "amount": 25000000, "currency": "COP", "status": "pending", "checkout_url": "https://lane.money/pay/3f0c8a1e-…", "payment": { "id": "9b41d0c7-…", "status": "pending" } } ``` ## Cómo paga el comprador con cada uno | Valor | Qué hace el comprador | Cómo te enteras | | --- | --- | --- | | `pse` | Se autentica en el portal de su banco. | Vuelve a tu sitio, y además llega el webhook. | | `nequi` | Aprueba una notificación en la app de Nequi. | Solo webhook. Puede tardar hasta 45 minutos. | | `daviplata` | Escribe un código que le llega por SMS. | Solo webhook. El código vale 15 minutos. | | `breb` | Escanea un QR o usa su llave desde cualquier app bancaria. | Solo webhook. El QR vale 15 minutos. | > **Cuidado: Un cobro, un medio.** Cada cobro usa un solo riel: `checkout_url` apunta a una pantalla que pide únicamente los datos de ese medio. Para ofrecerle varias opciones al comprador, crea una sesión por medio y deja que él elija en tu sitio antes de mandarlo a pagar. ## Topes de red Los pone cada red, no Lane. Se validan antes de crear el cobro, así que un monto fuera de rango devuelve `422` con un mensaje que dice cuál es el límite — el comprador no llega a descubrirlo cuando ya se decidió a pagar. | Medio | Mínimo | Máximo | | --- | --- | --- | | `pse` | — | Sin tope de red | | `nequi` | — | $ 12.000.000 COP | | `daviplata` | — | $ 3.000.000 COP | | `breb` | $ 1.000 COP | — | Nequi además aplica un tope mensual por persona que no podemos consultar: un cobro dentro del tope por transacción puede fallar igual si el comprador ya agotó su cupo del mes. ## No se devuelven > **Importante: Irrevocables.** Estos rieles mueven la plata en el momento y no admiten `refund` ni `cancel` por la API. Para devolverle el dinero a un comprador tienes que hacer una transferencia aparte. Si vendes algo con devolución frecuente, la tarjeta te va a dar menos trabajo. ## Y tampoco hay cuotas Las cuotas son un producto de la tarjeta de crédito. Ningún medio instantáneo las admite: si necesitas ofrecerlas, el cobro tiene que ir por tarjeta. Ver [Cuotas](https://lane.money/docs/guias/cuotas). --- # Cobrar a cuotas > Cómo ofrecer pago a cuotas con tarjeta de crédito en Lane: abrir la opción en el checkout, fijar la cantidad en un cobro directo y verificar el BIN antes. Solo la tarjeta de crédito hace cuotas, y el plan lo decide el banco emisor, no tú. Hay dos formas de llegar a un cobro a cuotas, y no funcionan igual. En la pantalla de pago solo puedes **abrir o cerrar la puerta**: si la abres, el comprador elige el plan con su banco. En un cobro directo contra una tarjeta guardada sí puedes **fijar la cantidad**. ## En la pantalla de pago **Abrir la opción de cuotas** ```json { "amount": 89000000, "currency": "COP", "description": "Pedido #4821", "checkout_options": { "allow_installments": true } } ``` Con eso, la pantalla le ofrece cuotas a quien pague con crédito. Cuántas y a qué interés lo define el emisor: Lane no impone el plan ni lo conoce de antemano. ## En un cobro directo Contra una tarjeta ya guardada sí puedes pedir una cantidad concreta. El mínimo es 2: para un pago normal, omite el objeto `installments` en vez de mandar `1`. **Tres cuotas** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 89000000, "currency": "COP", "instrument_id": "c1a9…", "initiator": "customer", "cvv": "123", "order_type": "purchase", "installments": { "quantity": 3 } }' ``` ## Verifica el BIN antes de ofrecer Una tarjeta débito puede aprobar el cobro con una sola cuota aunque hayas pedido tres, sin avisar. Consulta el BIN —los primeros 6 a 8 dígitos— y ofrécele cuotas al comprador solo si `supports_installments` viene en `true`. **Consultar un BIN** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/bins/518617" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "bin", "bin_number": "518617", "brand": "MASTERCARD", "product": "CREDIT", "country": "CO", "issuer": "…", "supports_installments": true } ``` > **Cuidado: La tarjeta de prueba no sirve para esto.** La `4111111111111111` del sandbox es débito. Aprueba cobros, pero no prueba cuotas. Para homologar el flujo necesitas una tarjeta de crédito real en sandbox: pídesela a soporte. --- # Cobro con tarjeta guardada > Guarda una tarjeta con una sesión de inscripción y cóbrala después sin el cliente presente: el flujo CIT/MIT para mensualidades y recompras en Lane. El cliente autoriza su tarjeta una vez. Después cobras desde tu servidor, sin él. Las redes distinguen dos situaciones: el cliente está frente a la pantalla, o no está. Los nombres del contrato son `initiator: "customer"` y `initiator: "merchant"`, y cada uno tiene reglas distintas. Confundirlos es la causa más común de rechazo en un cobro recurrente. | | Cliente presente | Cliente ausente | | --- | --- | --- | | `initiator` | `customer` | `merchant` | | CVV | Obligatorio | Prohibido — la API lo rechaza | | `order_type` | `purchase` y los de serie | Cualquiera menos `purchase` | | Enlace al primer cobro | No aplica | Obligatorio en cobros de serie | ## 1. Guarda la tarjeta Una sesión de inscripción abre una pantalla donde el cliente entrega su tarjeta. No cobra nada: solo la deja autorizada para después. ### POST /v1/enrollment/sessions Crea la pantalla donde el cliente guarda su tarjeta. **Crear la inscripción** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/enrollment/sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customer": { "email": "cliente@ejemplo.com" }, "success_url": "https://mitienda.com/tarjeta-guardada", "expires_in": 3600 }' ``` _Respuesta 201_ ```json { "id": "7d2b…", "object": "enrollment.session", "environment": "test", "status": "pending", "enrollment_url": "https://…" } ``` `expires_in` va entre 60 y 86400 segundos. Cuando el cliente termina, llega el evento `instrument.enrolled` y la tarjeta aparece en el listado de instrumentos. ## 2. Encuentra el instrumento **Listar tarjetas guardadas** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/instruments" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "data": [ { "object": "payment.instrument", "id": "c1a9f3e0-…", "status": "active", "customer": { "email": "cliente@ejemplo.com" }, "bin": "518617", "last4": "4242", "brand": "MASTERCARD", "product": "CREDIT", "supports_installments": true, "enrolled_at": "2026-09-06T18:20:11.000Z" } ] } ``` ## 3. El primer cobro, con el cliente El primero de la serie lo inicia el cliente: está mirando la pantalla y escribe su CVV. Ese cobro es el que la red va a usar como referencia de todos los siguientes, así que **guarda su identificador**. **Primer cobro (CIT)** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 8900000, "currency": "COP", "instrument_id": "c1a9f3e0-…", "initiator": "customer", "cvv": "123", "order_type": "recurring" }' ``` ## 4. Los siguientes, sin él Un cobro de serie sin el cliente presente tiene que apuntar al primero. Manda `parent_transaction_data` con el `first_payment_id` que guardaste, o con los datos de red que devolvió aquel cobro. Sin eso la API responde `400`, antes de intentar nada. **Cobro recurrente (MIT)** _cURL_ ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments \ -H "Authorization: Bearer sk_live_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 8900000, "currency": "COP", "instrument_id": "c1a9f3e0-…", "initiator": "merchant", "order_type": "recurring", "parent_transaction_data": { "first_payment_id": "9b41d0c7-…" } }' ``` _Node_ ```js // El día 1 de cada mes, para cada suscripción activa. async function cobrarMensualidad(sub) { const res = await fetch("https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments", { method: "POST", headers: { Authorization: `Bearer ${process.env.LANE_SECRET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: sub.montoCentavos, currency: "COP", instrument_id: sub.instrumentId, initiator: "merchant", // el cliente no está order_type: "recurring", parent_transaction_data: { first_payment_id: sub.primerCobroId, }, }), }); const pago = await res.json(); if (!res.ok) throw new Error(pago.error); return pago; // status: succeeded | failed | processing } ``` ## Los tipos de orden | `order_type` | Para qué | Quién lo inicia | | --- | --- | --- | | `purchase` | Una compra suelta. | Solo el cliente | | `recurring` | Mensualidad de monto fijo. | Ambos | | `installment` | Una compra dividida en pagos. | Ambos | | `standing_order` | Cobro periódico de monto variable. | Ambos | | `unscheduled` | Recarga automática, sin fecha fija. | Ambos | | `deferred` · `delayed` | Cobro que se hace después de la compra. | Ambos | | `no_show` · `resubmission` | Penalidad por no presentarse, reintento de un rechazo. | Solo el comercio | Los cobros de serie —`recurring`, `installment`, `standing_order`, `deferred`, `delayed`— son los que exigen el enlace al primero. `unscheduled`, `no_show` y `resubmission` no. ## Verificar que la tarjeta sigue viva Antes de un ciclo de cobros puedes preguntar si una tarjeta responde, sin cobrar nada: `intent: "account-status"` con `amount: 0`. Al tarjetahabiente no le aparece nada. **Verificación sin cobro** ```json { "amount": 0, "instrument_id": "c1a9f3e0-…", "intent": "account-status", "initiator": "customer", "cvv": "123" } ``` > **Cuidado: Una tarjeta reemitida deja de servir.** Si al cliente le cambian la tarjeta por vencimiento o robo, el instrumento guardado queda muerto y no se actualiza solo. Cuando un cobro recurrente falle por eso, la salida es pedirle al cliente que vuelva a inscribir su tarjeta. --- # Convenciones de la API > Cómo se ven los listados, cómo se pagina con cursor y qué campos comparten todos los objetos de la API de Lane. Tres reglas que valen para toda la API. Sabiéndolas, el resto de la referencia se lee solo. ## 1. La URL base **Base** ```text https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api ``` Todos los recursos cuelgan de `/v1`. No hay una URL distinta para sandbox: el entorno lo decide [la llave](https://lane.money/docs/autenticacion). ## 2. Los listados Un listado siempre devuelve un sobre con `object: "list"` y los resultados en `data`. Los que paginan traen además `has_more` y `next_cursor`. **Sobre de listado** ```json { "object": "list", "has_more": true, "next_cursor": "2026-09-06T18:22:41.000Z", "data": [ … ] } ``` **Parámetros comunes** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `limit` | entero | no | Cuántos traer. Por defecto 25, máximo 100. | | `starting_after` | cursor | no | El `next_cursor` de la página anterior. | Para pedir la página siguiente, pasa el `next_cursor` tal cual en `starting_after`. Cuando `has_more` es `false`, terminaste. **Recorrer todas las páginas** ```js async function* todosLosCobros() { let cursor = null; while (true) { const url = new URL("https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("starting_after", cursor); const page = await fetch(url, { headers: { Authorization: `Bearer ${process.env.LANE_SECRET_KEY}` }, }).then((r) => r.json()); yield* page.data; if (!page.has_more) return; cursor = page.next_cursor; } } ``` > **Nota: No todos los listados paginan.** Cobros, suscripciones y disputas usan cursor. Instrumentos y comercios devuelven todo de una (hasta 100) y no traen `next_cursor`; clientes trae `has_more` pero no cursor. ## 3. Los identificadores Todos los identificadores de Lane son UUID. Los que empiezan por `provider_` son del procesador y sirven para conciliar con un extracto; no los uses como clave en tu base. ## Orden y fechas Los listados vienen del más nuevo al más viejo. Todas las fechas son ISO 8601 en UTC. Colombia es UTC−5 todo el año: un cobro de las 8 de la noche aparece como la 1 de la mañana del día siguiente. --- # Cobros > Referencia del recurso payments de Lane: crear un cobro con tarjeta guardada, consultarlo, listarlo, capturar, anular, ajustar y reembolsar. El objeto central de la API. Todo lo demás existe para crear uno o para entenderlo. _Recorrido de un cobro: pending → authorized → processing → succeeded, o failed / cancelled / refunded. Esta sección documenta el estado `authorized`._ ## Crear un cobro ### POST /v1/payments Cobra contra una tarjeta ya guardada. Para cobrar con el comprador presente, usa una [sesión de checkout](https://lane.money/docs/api/sesiones). **Cuerpo** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `amount` | entero | sí | En centavos. Cero solo con `intent: "account-status"`. | | `currency` | texto | no | Por defecto `COP`. | | `instrument_id` | UUID | condicional | La tarjeta guardada en Lane. Obligatorio si no mandas `provider_instrument_id`; no mandes los dos. | | `provider_instrument_id` | texto | condicional | El identificador de la tarjeta en el procesador, para migraciones. | | `initiator` | `customer` · `merchant` | no | Quién dispara el cobro. Por defecto `merchant`. | | `cvv` | texto | condicional | 3 o 4 dígitos. Obligatorio con `initiator: "customer"`, prohibido con `merchant`. | | `order_type` | texto | no | El tipo de orden para la red. Ver [Cobro recurrente](https://lane.money/docs/guias/cobro-recurrente). | | `intent` | `authorization` · `pre-authorization` · `account-status` | no | Por defecto `authorization`. | | `capture` | objeto | no | `{ "mode": "AUTOMATIC" \| "MANUAL" \| "SCHEDULED" }`. Manual deja el cobro en `authorized`. | | `installments` | objeto | no | `{ "quantity": 3 }`, mínimo 2. Omítelo para un pago sin cuotas. | | `parent_transaction_data` | objeto | condicional | Enlace al primer cobro de la serie. Obligatorio en cobros recurrentes sin el cliente presente. | | `metadata` | objeto | no | Vuelve intacto en el cobro y en el webhook. | **Cobrar una tarjeta guardada** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 8900000, "currency": "COP", "instrument_id": "c1a9f3e0-…", "initiator": "merchant", "order_type": "recurring", "parent_transaction_data": { "first_payment_id": "9b41d0c7-…" } }' ``` _Respuesta 200_ ```json { "object": "payment", "id": "a02e5b19-…", "amount": 8900000, "currency": "COP", "status": "succeeded", "status_detail": "CAPTURED", "payment_method": "CARD", "processed_at": "2026-09-06T18:22:41.000Z" } ``` ## Consultar un cobro ### GET /v1/payments/{id} Devuelve el cobro con su estado actual. **Query** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `sync` | booleano | no | `true` consulta al procesador antes de responder. Más lento; para depurar. | **Consultar** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments/a02e5b19-…" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "payment", "id": "a02e5b19-…", "amount": 8900000, "currency": "COP", "status": "succeeded", "status_detail": "CAPTURED", "payment_method": "CARD", "customer_id": "44f1…", "checkout_session_id": null, "metadata": { "order_id": "4821" }, "cleared_at": "2026-09-07T04:00:00.000Z", "processed_at": "2026-09-06T18:22:41.000Z", "created_at": "2026-09-06T18:22:39.000Z" } ``` ## Listar cobros ### GET /v1/payments Los cobros del entorno de la llave, del más nuevo al más viejo. **Query** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `status` | texto | no | Uno o varios separados por coma: `succeeded,refunded`. | | `customer_id` | UUID | no | Solo los de ese cliente. | | `created_after` | fecha | no | ISO 8601. Desde esa fecha en adelante. | | `limit` | entero | no | Por defecto 25, máximo 100. | | `starting_after` | cursor | no | El `next_cursor` de la página anterior. | **Cobros aprobados de esta semana** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments?status=succeeded&created_after=2026-09-01T00:00:00Z&limit=100" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "has_more": false, "next_cursor": null, "data": [ { "object": "payment", "id": "a02e5b19-…", "amount": 8900000, "currency": "COP", "status": "succeeded", "payment_method": "CARD", "created_at": "2026-09-06T18:22:39.000Z" } ] } ``` ## Operar sobre un cobro Las cinco operaciones son `POST` sin cuerpo obligatorio. Un reembolso o un ajuste parcial admiten `{ "amount": … }` en centavos; sin ese campo, la operación es por el total. | Ruta | Requiere | Deja el cobro en | | --- | --- | --- | | `POST /v1/payments/{id}/capture` | `authorized` | `succeeded` | | `POST /v1/payments/{id}/cancel` | `authorized` | `cancelled` | | `POST /v1/payments/{id}/adjust` | `authorized` | `authorized`, con otro monto | | `POST /v1/payments/{id}/refund` | `succeeded` | `refunded` | | `POST /v1/payments/{id}/cancel-or-refund` | `authorized` o `succeeded` | Según cuál fuera | **Reembolso parcial** ```bash curl -X POST "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/payments/a02e5b19-…/refund" \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 2000000 }' ``` > **Cuidado: Solo la tarjeta se devuelve.** Un cobro por PSE, Nequi, Daviplata o Bre-B no admite `refund` ni `cancel`. Ver [Medios instantáneos](https://lane.money/docs/guias/medios-instantaneos). --- # Sesiones y links > Referencia de las sesiones de checkout, los links de pago y las sesiones de inscripción de tarjeta en la API de Lane. Tres endpoints que crean una pantalla de pago. Comparten casi todo el contrato. ## Sesión de checkout ### POST /v1/checkout/sessions Crea la pantalla de pago y devuelve su URL. Responde `201`. **Cuerpo** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `amount` | entero | sí | En centavos, mayor que cero. | | `currency` | texto | no | Por defecto `COP`. | | `description` | texto | no | Lo que ve el comprador. | | `payment_method` | `pse` · `nequi` · `daviplata` · `breb` | no | Omitido, el cobro es con tarjeta. Ver [Medios instantáneos](https://lane.money/docs/guias/medios-instantaneos). | | `success_url` | URL | no | Admite `{PAYMENT_ID}` y `{CHECKOUT_SESSION_ID}`. | | `cancel_url` | URL | no | A dónde vuelve si abandona. | | `error_url` | URL | no | A dónde vuelve si el emisor rechaza. | | `customer` | objeto | no | Con `email` queda asociado a un cliente. | | `line_items` | lista | no | El detalle del carrito. | | `metadata` | objeto | no | Vuelve intacto en el cobro y en el webhook. | | `expires_in` | entero | no | Segundos de vida. Por defecto `43200` (12 horas). | | `multi_use` | booleano | no | `true` deja la pantalla abierta para varios pagos. | | `max_uses` | entero | no | Tope de pagos cuando `multi_use` es `true`. | | `checkout_options` | objeto | no | `require_3ds`, `allow_installments`, `additional_fields`. | **Respuesta** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `id` | UUID | no | La sesión. | | `status` | texto | no | Nace en `pending`. | | `checkout_url` | URL | no | A dónde mandas al comprador. | | `embed_url` | URL | no | La misma URL, para usar como `src` de un iframe. | | `payment` | objeto | no | `{ id, status }` del cobro asociado. **Guarda este `id`**: es el que llega en el webhook. | | `environment` | `test` · `live` | no | El entorno de la llave con la que creaste la sesión. | ## Link de pago ### POST /v1/payment_links Mismo contrato que la sesión de checkout. La diferencia es de uso, no de forma: un link se manda, una sesión se enlaza desde un carrito. ### POST /v1/payment_links/{id}/sync Relee el estado del link contra el procesador y actualiza el cobro. ## Sesión de inscripción ### POST /v1/enrollment/sessions Crea la pantalla donde el cliente guarda su tarjeta, sin cobrar nada. **Cuerpo** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `customer` | objeto | no | De quién es la tarjeta. | | `success_url` | URL | no | A dónde vuelve cuando termina. | | `cancel_url` | URL | no | A dónde vuelve si abandona. | | `expires_in` | entero | no | Entre 60 y 86400 segundos. | | `metadata` | objeto | no | Vuelve en el instrumento. | **Inscribir una tarjeta** ```bash curl -X POST https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/enrollment/sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customer": { "email": "cliente@ejemplo.com" }, "success_url": "https://mitienda.com/tarjeta-guardada" }' ``` _Respuesta 201_ ```json { "id": "7d2b…", "object": "enrollment.session", "environment": "test", "status": "pending", "enrollment_url": "https://…", "embed_url": "https://…" } ``` --- # Instrumentos > Las tarjetas guardadas de tus clientes en Lane: cómo listarlas, qué trae cada una y cómo saber si admite cuotas. Una tarjeta que un cliente autorizó y que puedes volver a cobrar. ### GET /v1/instruments Las tarjetas activas del entorno, hasta 100, de la más nueva a la más vieja. ### GET /v1/instruments/{id} Una tarjeta, con su detalle completo. **El objeto** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `id` | UUID | no | El que mandas como `instrument_id` al cobrar. | | `status` | texto | no | El listado solo trae las `active`. | | `bin` | texto | no | Los primeros dígitos. Sirven para consultar el producto. | | `last4` | texto | no | Para que el cliente reconozca cuál es. | | `brand` | texto | no | `VISA`, `MASTERCARD`… | | `product` | texto | no | `CREDIT` o `DEBIT`. | | `supports_installments` | booleano | no | `true` solo si es crédito. | | `customer` | objeto | no | De quién es. | | `enrolled_at` | fecha | no | Cuándo la guardó. | **Listar tarjetas** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/instruments" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "data": [ { "object": "payment.instrument", "id": "c1a9f3e0-…", "status": "active", "bin": "518617", "last4": "4242", "brand": "MASTERCARD", "product": "CREDIT", "supports_installments": true, "customer": { "email": "cliente@ejemplo.com" }, "enrolled_at": "2026-09-06T18:20:11.000Z" } ] } ``` > **Nota: No se actualizan solas.** Si al cliente le reemiten la tarjeta, el instrumento deja de servir y no hay forma de renovarlo por API: hay que pedirle que la inscriba de nuevo. Cómo se guardan y se cobran está en [Cobro recurrente](https://lane.money/docs/guias/cobro-recurrente). ## Consultar un BIN ### GET /v1/bins/{bin} De 6 a 8 dígitos. Dice qué producto es la tarjeta antes de cobrarla. **Consultar** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/bins/518617" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "bin", "bin_number": "518617", "brand": "MASTERCARD", "product": "CREDIT", "product_type": "…", "country": "CO", "region": "LATAM", "issuer": "…", "supports_installments": true } ``` --- # Clientes > Los compradores que han pagado a tu comercio: cómo listarlos, buscarlos por correo y ver todo lo que tienen asociado. No se crean a mano. Un cliente aparece la primera vez que pagas algo a su nombre. Cuando mandas `customer` al crear un cobro, Lane busca si ya existe uno con ese correo y lo reutiliza. Por eso el listado se llena solo. ### GET /v1/customers Los clientes del entorno. **Query** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `email` | texto | no | Busca por correo exacto. | | `limit` | entero | no | Por defecto 25, máximo 100. | ### GET /v1/customers/{id} Un cliente, con sus totales y los identificadores de lo que tiene asociado. El detalle devuelve identificadores, no objetos completos: la respuesta se mantiene chica y expandes lo que te interese con el endpoint correspondiente. **Buscar por correo** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/customers?email=cliente@ejemplo.com" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "has_more": false, "data": [ { "object": "customer", "id": "44f1…", "email": "cliente@ejemplo.com", "name": "Ana Restrepo", "created_at": "2026-08-14T15:02:00.000Z" } ] } ``` --- # Disputas > Los contracargos abiertos contra tu comercio: cómo listarlos, filtrar los que necesitan respuesta y hasta cuándo tienes para responder. Un comprador desconoció un cobro. Hay un plazo para responder y perderlo es perder la plata. ### GET /v1/disputes Las disputas del entorno. **Query** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `needs_response` | booleano | no | `true` deja solo las que están esperando algo de tu parte. | | `status` | texto | no | Filtra por estado. | | `payment_id` | UUID | no | Las disputas de un cobro. | | `limit` | entero | no | Por defecto 25, máximo 100. | ### GET /v1/disputes/{id} Una disputa, con su historial, comentarios y adjuntos. **El objeto** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `needs_response` | booleano | no | El campo que decide si el caso se gana o se pierde. | | `evidence_due_at` | fecha | no | Hasta cuándo se admite evidencia. | | `reason_code` | texto | no | El código de la red. | | `reason_description` | texto | no | Qué alega el comprador. | | `amount` | entero | no | En centavos. | | `payment_id` | UUID | no | El cobro disputado. | **Las que esperan respuesta** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/disputes?needs_response=true" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "has_more": false, "next_cursor": null, "data": [ { "object": "dispute", "id": "e77c…", "status": "needs_response", "needs_response": true, "evidence_due_at": "2026-09-20T23:59:59.000Z", "amount": 11900000, "currency": "COP", "reason_code": "…", "payment_id": "a02e5b19-…" } ] } ``` > **Cuidado: La evidencia se sube desde el panel.** Hoy la API deja consultar disputas, no responderlas. Para adjuntar la evidencia entra a **Disputas** en el panel. El listado paginado devuelve 25 por página. --- # Suscripciones > Consulta las suscripciones activas de tu comercio en Lane, su ciclo actual y cuándo sale el próximo cobro. Se crean desde el panel. La API las lee. ### GET /v1/subscriptions Las suscripciones del entorno. **Query** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `status` | texto | no | Filtra por estado. | | `customer_id` | UUID | no | Las de un cliente. | | `limit` | entero | no | Por defecto 25, máximo 100. | | `starting_after` | cursor | no | Paginación. | **El objeto** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `plan_id` | UUID | no | El plan al que está suscrito. | | `payment_instrument_id` | UUID | no | La tarjeta que se cobra. | | `current_period_start` | fecha | no | Inicio del ciclo en curso. | | `current_period_end` | fecha | no | Fin del ciclo en curso. | | `next_charge_at` | fecha | no | Cuándo sale el próximo cobro. | | `cancelled_at` | fecha | no | Cuándo se canceló, si se canceló. | **Listar** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/subscriptions?status=active" \ -H "Authorization: Bearer sk_test_…" ``` > **Nota: Los avisos importantes llegan por webhook.** Cuando un cobro recurrente falla llega `subscription.payment_failed` con la fecha del próximo intento, y cuando se agotan los reintentos, `subscription.past_due`. Ver [Webhooks](https://lane.money/docs/webhooks). --- # Comercios > Los comercios de tu cuenta en Lane: cómo listarlos, consultar uno y actualizar sus datos. Una cuenta puede tener varias tiendas. Cada llave cobra a nombre de una. ### GET /v1/merchants Los comercios de tu organización. ### GET /v1/merchants/{id} Un comercio. Responde `404` si no es tuyo. ### PUT /v1/merchants/{id} Actualiza sus datos. **El objeto** | Campo | Tipo | Obligatorio | Descripción | | --- | --- | --- | --- | | `name` | texto | no | Razón social o nombre del negocio. | | `alias` | texto | no | Nombre corto, el que ve el comprador. | | `email` | texto | no | Contacto del comercio. | | `phone` | texto | no | Contacto del comercio. | | `website` | URL | no | Su sitio. | | `activity` | texto | no | A qué se dedica. | | `default_currency` | texto | no | `COP`. | | `supported_currencies` | lista | no | Las que acepta. | | `billing_address` | objeto | no | Dirección de facturación. | | `location_address` | objeto | no | Dirección física. | | `tax_information` | objeto | no | Identificación tributaria. | **Listar comercios** ```bash curl "https://gxxazsplhgyezwxghrnn.supabase.co/functions/v1/blink-api/v1/merchants" \ -H "Authorization: Bearer sk_test_…" ``` _Respuesta 200_ ```json { "object": "list", "data": [ { "id": "b21f…", "name": "Café Vecino S.A.S.", "alias": "Café Vecino", "default_currency": "COP", "supported_currencies": ["COP"] } ] } ``` --- # Webhooks > Recibe los cambios de estado de tus cobros en tu servidor: catálogo de eventos de Lane, cómo verificar la firma HMAC y cómo funcionan los reintentos. La única forma correcta de saber que un cobro se aprobó. Todo lo demás es adivinar. Registra una URL en **Desarrolladores → Webhooks** y elige qué eventos quieres. Cuando pasa algo, mandamos un `POST` con el evento. Al crear el endpoint se muestra una vez un secreto `whsec_…`: con ese secreto verificas que el aviso lo mandamos nosotros. ## Los eventos | Evento | Qué significa | Qué hacer | | --- | --- | --- | | `payment.succeeded` | El cobro quedó aprobado. | Despacha el pedido. Es el único que lo autoriza. | | `payment.failed` | El emisor rechazó la tarjeta. | No despaches. Ofrece otro medio. | | `payment.cancelled` | Se anuló antes de capturar. | Libera el inventario reservado. | | `payment.refunded` | Se devolvió la plata. | Registra la devolución. | | `subscription.payment_failed` | Falló un cobro recurrente. Trae la fecha del próximo intento. | Avísale al cliente; Lane va a reintentar. | | `subscription.past_due` | Se agotaron los reintentos. | Lane dejó de cobrar sola: contacta al cliente o cancela. | | `instrument.enrolled` | Un cliente guardó su tarjeta. | Ya puedes cobrarle sin él presente. | Los eventos de pago se nombran `payment.`, con el mismo vocabulario de [estados](https://lane.money/docs/estados). Suscríbete solo a los que vas a manejar: un endpoint que responde error a un evento que no le importa entra igual en la cola de reintentos. ## La petición | Cabecera | Contenido | | --- | --- | | `Blink-Event` | El tipo de evento, por ejemplo `payment.succeeded`. | | `Blink-Timestamp` | Momento del envío, en segundos Unix. | | `Blink-Signature` | `t=,v1=` | ## Verificar la firma La firma es un **HMAC-SHA256 en hexadecimal** sobre el texto `.`, con el secreto del endpoint como llave. El punto va entre los dos: no es una concatenación directa. > **Importante: El cuerpo crudo, no el objeto.** Hay que firmar el texto exacto que llegó. Si tu framework ya parseó el JSON y lo vuelves a serializar, el orden de las llaves y los espacios pueden cambiar, y la firma no va a cuadrar nunca. En Express, `express.raw()`; en Next.js, lee el `request.text()`. **Verificación** _Node_ ```js import crypto from "node:crypto"; import express from "express"; const app = express(); app.post( "/webhooks/lane", express.raw({ type: "application/json" }), // el cuerpo crudo, sin parsear (req, res) => { const firma = req.get("Blink-Signature") ?? ""; const evento = req.get("Blink-Event"); const cuerpo = req.body.toString("utf8"); if (!firmaValida(cuerpo, firma, process.env.LANE_WEBHOOK_SECRET)) { return res.status(400).send("firma inválida"); } // Responde rápido y procesa después: si tardas, entramos a reintentar. res.sendStatus(200); encolar(evento, JSON.parse(cuerpo)); }, ); function firmaValida(cuerpo, cabecera, secreto) { const partes = Object.fromEntries( cabecera.split(",").map((p) => p.split("=")), ); const { t, v1 } = partes; if (!t || !v1) return false; // La firma cubre ".", no solo el cuerpo. const esperada = crypto .createHmac("sha256", secreto) .update(`${t}.${cuerpo}`) .digest("hex"); const a = Buffer.from(esperada); const b = Buffer.from(v1); if (a.length !== b.length) return false; // Comparación en tiempo constante: una comparación normal filtra la firma // carácter por carácter a quien mida cuánto tarda en fallar. return crypto.timingSafeEqual(a, b); } ``` _Python_ ```python import hmac, hashlib, os from flask import Flask, request app = Flask(__name__) @app.post("/webhooks/lane") def lane(): cuerpo = request.get_data(as_text=True) # crudo, sin parsear if not firma_valida(cuerpo, request.headers.get("Blink-Signature", "")): return "firma inválida", 400 encolar(request.headers.get("Blink-Event"), request.get_json()) return "", 200 def firma_valida(cuerpo: str, cabecera: str) -> bool: partes = dict(p.split("=", 1) for p in cabecera.split(",") if "=" in p) t, v1 = partes.get("t"), partes.get("v1") if not t or not v1: return False esperada = hmac.new( os.environ["LANE_WEBHOOK_SECRET"].encode(), f"{t}.{cuerpo}".encode(), hashlib.sha256, ).hexdigest() return hmac.compare_digest(esperada, v1) ``` _PHP_ ```php El entorno de pruebas de Lane: tarjetas de prueba, qué se puede probar sin plata real y qué diferencias tiene con producción. El mismo código, la misma URL, otra llave. En sandbox no se mueve plata. Sandbox no es un servidor aparte: es el mismo, respondiendo distinto según la llave. Una `sk_test_` cobra en pruebas y una `sk_live_` cobra de verdad, sobre las mismas rutas. Los datos están separados: un cobro de prueba no aparece en los listados de producción. ## Antes de empezar 1. Registra tu comercio de prueba en **Configuración → Comercio y sandbox**. Sin esto no se puede crear ningún cobro. 2. Crea una llave `sk_test_` en **Desarrolladores → Llaves de API**. 3. Crea un endpoint de webhook y guarda su secreto `whsec_…`. ## Tarjetas | Para qué | Número | Vence | CVV | | --- | --- | --- | --- | | Aprobar un cobro | `4111111111111111` | 12/30 | 123 | | BIN de crédito, para probar cuotas | `518617` · `541333` | — | — | > **Cuidado: No hay tarjetas de rechazo estándar.** Los números de rechazo que usan otras pasarelas —`4000000000000002` y parientes— acá pueden aprobar igual. Para probar un rechazo, pídele a soporte los datos que lo fuerzan en tu entorno. ## Qué se puede probar | | Sandbox | | --- | --- | | Cobro con tarjeta | Sí, de punta a punta | | Guardar una tarjeta y cobrarla después | Sí | | Webhooks con firma real | Sí | | Cuotas | Solo con una tarjeta de crédito; la de prueba es débito | | PSE, Nequi, Daviplata, Bre-B | Depende del riel: algunos no tienen simulador | | Contracargos | No se pueden originar; se consultan si existen | ## Pasar a producción - Cambia la llave por una `sk_live_`. Nada más cambia en tu código. - Crea el endpoint de webhook de producción: los endpoints no se comparten entre entornos. - Revisa que ningún `sk_` haya quedado en el repositorio ni en el bundle del front. - Haz un cobro real de monto bajo y reembólsalo. Es la única prueba que cuenta. --- # Servidor MCP > El servidor MCP de Lane deja que un asistente como Claude o Cursor consulte y cree cobros por ti. En construcción, con la lista de espera abierta. Conectar tu cuenta a un asistente para que consulte cobros y cree links por ti. > **Cuidado: En construcción.** El contrato de herramientas que describe esta página está definido y versionado en el repositorio, pero el servidor todavía no está publicado. Lo que sigue es lo que va a exponer, para que puedas diseñar tu integración desde ya. La lista de espera está abierta. MCP —Model Context Protocol— es la forma estándar de darle herramientas a un asistente. Con el servidor de Lane conectado, en vez de abrir el panel puedes pedirle a tu asistente que revise los cobros de ayer o que arme un link para un cliente. ## Las herramientas | Herramienta | Qué hace | Escribe | | --- | --- | --- | | `listar_cobros` | Los cobros del comercio, con filtros por estado y fecha. | No | | `consultar_cobro` | El detalle de un cobro por su identificador. | No | | `crear_link_de_pago` | Un link de pago con monto y descripción. | Sí | | `reembolsar_cobro` | Devuelve la plata de un cobro aprobado, total o parcial. | Sí | | `listar_disputas` | Los contracargos, con las que esperan respuesta primero. | No | | `listar_clientes` | Los clientes del comercio, con búsqueda por correo. | No | | `buscar_en_docs` | Busca en esta documentación y devuelve el fragmento. | No | Las que escriben van a pedir confirmación antes de ejecutarse, y ninguna acepta llaves de producción sin que las habilites explícitamente. ## Mientras tanto Un asistente puede trabajar con esta documentación hoy mismo, sin servidor: está publicada en markdown limpio. | Recurso | Qué trae | | --- | --- | | [/llms.txt](https://lane.money/llms.txt) | El resumen del sitio y el índice de páginas. | | [/docs/llms.txt](https://lane.money/docs/llms.txt) | Solo la documentación técnica, página por página. | | [/docs/llms-full.txt](https://lane.money/docs/llms-full.txt) | Toda la documentación en un archivo. | | `.md` | Esa página en markdown. Por ejemplo `/docs/webhooks.md`. | **Darle la documentación a un asistente** ```bash curl https://lane.money/docs/llms-full.txt ``` ## Cómo escribimos esta documentación Está pensada para que la lea una máquina sin perder nada: cada endpoint es autocontenido —no dice «como se vio arriba»—, los campos van en tablas y no en párrafos, cada ejemplo se puede copiar y ejecutar tal cual, y los montos aparecen siempre en las dos formas, el entero que se manda y los pesos que ve el comprador. Para probar la API desde el asistente hoy, la llave secreta y `curl` alcanzan: la referencia empieza en [Convenciones](https://lane.money/docs/api/paginacion).