# API de Pagos — Scanntech > REST + JSON, un solo Bearer token. Enrolá tarjetas por iframe (sin scope PCI), cobrá con el instrumento y generá links de pago. ## Empezar ### Base URL | Ambiente | URL | |---|---| | Sandbox | — | | Producción | `https://procobro.akua.la` | Todas las peticiones usan `Content-Type: application/json`. ### Autenticación Obtené un access token con las credenciales que recibís en el onboarding (un par por ambiente): `POST /oauth/token` ```json { "client_id": "", "client_secret": "" } ``` Respuesta · 200: ```json { "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 43200 } ``` El token dura 12 horas — cachealo y renovalo antes de que expire. Todas las demás llamadas lo llevan en el header `Authorization`; sin token válido la API devuelve `401`. ```bash curl -X POST https://procobro.akua.la/v1/payments \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ ... }' ``` ## Pagos ### Crear un pago > **Nunca envíes el número de tarjeta a esta API.** El pago se cobra con un `instrument_id` — el token que obtenés al tokenizar/enrolar la tarjeta (ver **Campos embebidos** o **Enrolar una tarjeta**). Un request con `card` (número, vencimiento, …) se rechaza con **422**. Así tu integración queda fuera de alcance PCI. > **El CVV no se guarda en la bóveda** (ninguna bóveda de tarjetas lo hace — va contra PCI DSS) — a diferencia del número, tenés que reenviarlo **cifrado** en cada pago, incluso si el `instrument_id` ya existe de una tokenización anterior. Cifralo con tu clave (mismo mecanismo de **Campos embebidos**) y mandalo como `encrypted_cvv`; en texto plano se rechaza con **422**, igual que el resto de los datos de tarjeta. `POST /v1/payments` ```json { "amount": 1000, "currency": "COP", "order_id": "orden-83778", "description": "Orden 83778 - mitienda.com", "instrument_id": "ins-cu94vq1n8ql8u3v469gg", "encrypted_cvv": "" } ``` | Campo | Tipo | Req. | Notas | |---|---|---|---| | `amount` | number | ✓ | Monto en la unidad de la moneda | | `currency` | string | ✓ | ISO 4217 (`COP`, `USD`, …) | | `order_id` | string | ✓ | Tu referencia única — sirve para idempotencia | | `instrument_id` | string | ✓ | Token de la tarjeta enrolada (`ins-…`) | | `encrypted_cvv` | string | ✓ | CVV cifrado con tu clave — no se vaultea, hay que reenviarlo en cada pago | Respuesta · 201: ```json { "id": "orden-83778-c-xxxx", "status": "APPROVED", "response_code": "00", "amount": "1000", "approval_code": "079662" } ``` ### Obtener un pago `GET /v1/payments/{id}` ```json { "status": "AUTHORIZED" } ``` ### Listar pagos `GET /v1/payments?limit=25` ```json { "data": [ { "id": "pay-…", "status": "APPROVED" } ], "has_more": false } ``` ### Reembolsar un pago `POST /v1/refunds` ```json { "payment_id": "pay-cu94vq1n8ql8u3v469gg", "amount": 1000, "currency": "COP" } ``` Reembolsos parciales: enviá un `amount` menor al original. Solo aplica a pagos **capturados** (la captura automática tarda unos minutos); para deshacer un pago recién autorizado usá **cancelar**. ### Cancelar / Capturar `POST /v1/payments/{id}/cancel` Anula una autorización aún no capturada (void). Sin body. `POST /v1/payments/{id}/capture` Solo para captura diferida — por defecto los pagos se capturan automáticamente. ### Contracargos `GET /v1/chargebacks` ```json { "data": [ { "id": "chb-…", "payment_id": "pay-…", "status": "OPEN" } ], "has_more": false } ``` ## Pagos alternativos (APMs) ### Crear un pago APM Además de tarjeta aceptás **pagos alternativos** en Colombia: el cliente paga desde su banco o billetera. Los cuatro rails se crean con el mismo endpoint y se distinguen por `rail_id`. | Rail | Mecanismo | Cómo confirma el cliente | Vigencia | |---|---|---|---| | `PSE` | Redirección al portal del banco | Se autentica en su banco (`rail.payment_link`) | ~21 min | | `NEQUI` | Notificación push a la app | Aprueba con su PIN en Nequi | 45 min | | `DAVIPLATA` | OTP por SMS (sin app) | Ingresa el OTP en tu checkout (paso `confirm`) | 15 min | | `BRE_B` | QR / alias interoperable | Escanea el QR desde cualquier app bancaria | 15 min | > **Los APMs son asíncronos.** El `POST` devuelve el **estado inicial** (`IN_PROGRESS`), no el resultado. El resultado final llega **siempre por webhook** — nunca confirmes el pedido con la respuesta del `POST`. En PSE, además, hacé **polling** del estado. > Solo **COP**. Los pagos APM son **irrevocables** (sin contracargos); las devoluciones se procesan como un desembolso aparte. No hay tokenización ni cobros recurrentes. **Sandbox:** un monto cuyo entero termine en `00` (ej. `50000`) activa el mock del rail. `POST /v1/payments/intents` **Nequi** — billetera, cobro push. Solo el celular del cliente: ```json { "order_id": "orden-83790", "amount": 50000, "currency": "COP", "rail_id": "NEQUI", "rail_type": "WALLET", "description": "Orden 83790 - mitienda.com", "customer": { "phone_number": "3001234567" } } ``` **PSE** — débito bancario con redirección. Requiere `return_url`, el banco elegido (ver **Listar bancos**) y los datos del pagador: ```json { "order_id": "orden-83791", "amount": 200000, "currency": "COP", "rail_id": "PSE", "rail_type": "BANK_TRANSFER", "return_url": "https://mitienda.com/pagos/resultado", "customer": { "bank_id": "1007", "document": "1234567890", "document_type": "CC", "first_name": "Juan", "last_name": "Pérez", "email": "juan@ejemplo.com" } } ``` **Bre-B** — QR interoperable. Body mínimo; la respuesta trae el QR y el alias: ```json { "order_id": "orden-83792", "amount": 150000, "currency": "COP", "rail_id": "BRE_B", "rail_type": "BANK_TRANSFER" } ``` | Campo | Tipo | Req. | Notas | |---|---|---|---| | `order_id` | string | ✓ | Tu referencia única (idempotencia) | | `amount` | number | ✓ | Monto en COP, mayor a 0 | | `currency` | string | ✓ | Debe ser `COP` | | `rail_id` | string | ✓ | `PSE` · `NEQUI` · `DAVIPLATA` · `BRE_B` | | `rail_type` | string | ✓ | `WALLET` (Nequi/Daviplata) · `BANK_TRANSFER` (PSE/Bre-B) | | `description` | string | — | Aparece en el comprobante del cliente | | `return_url` | string | PSE | URL pública a la que vuelve el cliente tras el banco | | `customer.phone_number` | string | Nequi/Daviplata | Celular colombiano registrado en la billetera (`3001234567`) | | `customer.bank_id` | string | PSE | Banco elegido — `id` de `GET /api/banks` | | `customer.document` | string | PSE | Número de documento del pagador | | `customer.document_type` | string | PSE | `CC` · `CE` · `NIT` · `PASSPORT` | | `customer.first_name` | string | PSE | Nombre del pagador | | `customer.last_name` | string | PSE | Apellido del pagador | | `customer.email` | string | PSE | ACH Colombia envía el comprobante a este correo | Respuesta · 201 (PSE — redirección): ```json { "payment_id": "pay-xxxxxxxxxxxx", "transaction": { "id": "trx-xxxxxxxxxxxx", "type": "PURCHASE", "status": "IN_PROGRESS", "status_detail": "PENDING", "amount": 200000 }, "rail": { "id": "PSE", "payment_link": "https://pse.onepay.com/pay/session-abc123" } } ``` Respuesta · 201 (Bre-B — QR): ```json { "payment_id": "pay-xxxxxxxxxxxx", "transaction": { "status": "IN_PROGRESS", "status_detail": "PENDING", "amount": 150000 }, "rail": { "id": "BRE_B", "alias": "@STORE7X", "qr": { "image": "data:image/png;base64,...", "content": "00020101021226..." }, "payment_link": "https://pay.breb.com.co/t/abc123" } } ``` | Campo de la respuesta | Notas | |---|---| | `payment_id` | Guardalo — es el id del pago (para consultar estado y para el `confirm` de Daviplata) | | `transaction.status` / `status_detail` | Estado inicial — ver la tabla de estados abajo | | `rail.payment_link` | PSE/Bre-B: URL a la que redirigís al cliente | | `rail.qr.image` / `rail.qr.content` | Bre-B: QR para mostrar (imagen data-URI + contenido EMV) | | `rail.alias` | Bre-B: alias corto que el cliente puede tipear si no escanea | > Según el rail: redirigí al cliente a `rail.payment_link` (PSE), mostrale `rail.qr`/`rail.alias` (Bre-B), o esperá que apruebe en la app (Nequi). **Daviplata** necesita un paso extra de OTP — ver su sección. | `status` / `status_detail` | Significado | Acción | |---|---|---| | `IN_PROGRESS` / `PENDING` | El cliente aún no completó | Mostrar "procesando" | | `IN_PROGRESS` / `PENDING_PROVIDER` | El banco (PSE) está procesando | Polling cada 15–30s | | `APPROVED` / `SUCCESS` | Aprobado; fondos en tránsito | Confirmar el pedido | | `DECLINED` / `REJECTED` | Rechazado o expirado | Notificar; ofrecer alternativa | | `FAILED` | Error técnico | Registrar; no reintentar sin verificar | El resultado final se entrega por **webhook** (al menos una vez — hacé tu handler idempotente por el campo `id`; respondé `200` en <5s). Consultá el estado con `GET /v1/payments/{payment_id}`. | Evento del webhook | Significado | |---|---| | `payment.purchase.pending` | Intento creado; esperando al cliente | | `payment.purchase.succeeded` | Aprobado → confirmar el pedido | | `payment.purchase.rejected` | Rechazado o expirado | | `payment.purchase.failed` | Error técnico | | `payment.purchase.confirm.processed` | Daviplata: OTP confirmado, débito ejecutado | | `payment.purchase.confirm.failed.retryable` | Daviplata: OTP incorrecto, reintentable | Payload del webhook: ```json { "id": "evt-xxxxxxxxxxxx", "type": "payment.purchase.succeeded", "time": "2025-10-15T15:10:00Z", "data": { "payment": { "id": "pay-xxxxxxxxxxxx", "currency": "COP", "current_amount": 200000, "transaction": { "status": "APPROVED", "status_detail": "SUCCESS" } } } } ``` ### Listar bancos (PSE) Para PSE mostrás una lista de bancos y el cliente elige el suyo. **Obtené la lista dinámicamente** en cada checkout — no la hardcodees, cambia con el tiempo. `GET /api/banks?country=COL&rail_id=PSE` | Parámetro | Req. | Notas | |---|---|---| | `country` | ✓ | ISO 3166-1 alpha-3 — `COL` | | `rail_id` | ✓ | Rail para el que se listan las entidades — `PSE` | Respuesta: ```json [ { "id": "1007", "name": "BANCOLOMBIA" }, { "id": "1051", "name": "DAVIVIENDA" } ] ``` Usá el `id` del banco elegido como `customer.bank_id` al crear el intento. ### Daviplata (OTP en 2 pasos) Daviplata funciona por SMS, sin app. Es un flujo de **dos pasos**: creás el intento (dispara un OTP por SMS) y luego confirmás el código que el cliente ingresa en tu checkout. **Paso 1 — crear el intento** (dispara el SMS). Devuelve el `payment_id`: `POST /v1/payments/intents` ```json { "order_id": "orden-83793", "amount": 75000, "currency": "COP", "rail_id": "DAVIPLATA", "rail_type": "WALLET", "customer": { "phone_number": "3001234567", "document": "1234567890", "document_type": "CC" } } ``` | Campo | Tipo | Req. | Notas | |---|---|---|---| | `rail_id` | string | ✓ | Debe ser `DAVIPLATA` | | `rail_type` | string | ✓ | Debe ser `WALLET` | | `customer.phone_number` | string | ✓ | Celular colombiano registrado en Daviplata | | `customer.document` | string | — | Recomendado: sube la tasa de aprobación | | `customer.document_type` | string | — | `CC` · `CE` · `TI` | **Paso 2 — confirmar el OTP** que el cliente recibió por SMS (válido 15 min): `POST /v1/payments/intents/{payment_id}/confirm` ```json { "otp": "123456" } ``` > El `200` del `confirm` solo indica que Akua recibió el OTP — **no** garantiza la aprobación. El resultado final llega por webhook (`payment.purchase.confirm.processed` = aprobado). Si el OTP es incorrecto (`confirm.failed.retryable`), el intento sigue activo y el cliente puede reintentar con el mismo `payment_id`. ## Links de pago ### Crear un link Generá una URL de checkout hosteada — tu cliente paga sin que manejes datos de tarjeta. Compartila por WhatsApp, email o QR. `POST /v1/links` ```json { "type": "payment", "expires_in": 3600, "data": { "amount": { "value": 25000, "currency": "COP" }, "description": "Orden #12345", "redirect_url": "https://mitienda.com/gracias", "cancel_url": "https://mitienda.com/carrito" } } ``` Respuesta · 201: ```json { "id": "lnk-a1b2c3d4", "url": "https://checkout.akua.la/links/lnk-a1b2c3d4", "status": "created", "expires_at": "2026-07-02T14:00:00Z" } ``` Ciclo de vida: created → opened → used → expired > **Opciones útiles** — `multi_use: true` + `max_uses` para links reutilizables; `data.amount.type: "custom"` para que el cliente elija el monto (con `min_amount`/`max_amount`); `expires_in` en segundos (default 12 h, máximo 24 h). ### Consultar links `GET /v1/links/{id}` Devuelve el link con su `status` y, una vez pagado, el `payment_id` asociado. `GET /v1/links` lista todos, paginado. ## Tokenización ### Enrolar una tarjeta (iframe) El comercio **nunca toca datos de tarjeta**. Se genera un link de enrolamiento, se embebe en un iframe (formulario hosteado y certificado PCI de Akua), y al completarse recibís un `instrument_id` para cobrar. Es el equivalente a Smart Fields. `POST /v1/instruments/enrollments` ```json { "type": "card_enrollment", "expires_in": 3600, "data": { "webhook": "https://mitienda.com/webhooks/tarjeta", "api_key": "mi-api-key-secreta" }, "metadata": { "styleConfig": { "text": { "headerTitle": "Registrá tu tarjeta", "buttonText": "Guardar" }, "colors": { "primary": "#5b6ed9" } } } } ``` Respuesta · 201: ```json { "id": "lnk-d7ac0929", "url": "https://checkout.akua.la/links/lnk-d7ac0929", "type": "card_enrollment", "status": "created" } ``` Embebé la `url` en tu página. El formulario se personaliza con `metadata.styleConfig` (textos, colores, layout) para que combine con tu sitio: Embeber en iframe: ```bash ``` Cuando el cliente guarda la tarjeta, Akua hace `POST` a tu `webhook` con `{ "instrument_id": "ins-…" }` (header `Api-Key` para verificar). Ese `instrument_id` es el que usás en **Crear un pago**. Ciclo de vida: created → opened → used ### Administrar instrumentos Un instrumento enrolado se puede consultar o eliminar. Para cobrar, mandá su `instrument_id` en **Crear un pago**. | Operación | Endpoint | |---|---| | Obtener instrumento | `GET /v1/instruments/{id}` | | Listar instrumentos | `GET /v1/instruments` | | Eliminar instrumento | `DELETE /v1/instruments/{id}` | ## Campos embebidos ### ¿Qué son los campos embebidos? Cobrá con tarjeta **dentro de tu propio checkout**, con tu diseño, sin que ningún dato de tarjeta toque tu página ni tu servidor. **Akua Fields** monta el campo de tarjeta como un iframe servido desde un dominio seguro: tu página no puede leer lo que el cliente tipea (aislamiento cross-origin), así tu comercio queda en **SAQ A** — el nivel más liviano de PCI, el mismo que tendrías con un checkout hosteado. > **Dual Encryption es obligatorio.** El único formato soportado para campos embebidos es: tu backend genera su clave de cifrado (una vez) y la pasa a `mount()`. Número, vencimiento, nombre del titular **y CVV** se cifran con RSA-OAEP-256 **dentro del iframe**, antes de salir del navegador — ni tu página, ni Akua Fields, ni la red ven esos datos en texto plano. Ver **Setup paso a paso** abajo. Flujo: generás tu clave (una vez) → cliente tipea + cifra en el iframe → tokenize → instrument_id → pago → APPROVED **Todo lo que rodea al campo es tuyo**: layout, tipografías, botón, resumen de la orden. El campo hereda automáticamente la marca (color y fuente) y podés pisarla con `accent`. El cliente nunca sale de tu página. | Camino | Ideal para | ¿Tocás datos de tarjeta? | Scope PCI | |---|---|---|---| | **Links de pago** | Compartir por WhatsApp, email o QR — sin frontend | Nunca | SAQ A | | **Enrolamiento (iframe)** | Guardar tarjetas con el formulario hosteado completo | Nunca | SAQ A | | **Campos embebidos** | Checkout propio con tu diseño, pago en tu página | Nunca | SAQ A | | **Akua Direct** | Comercios ya certificados PCI que quieren campos 100% propios | Sí | SAQ D | > **Probalo ahora, sin escribir código** — abajo en **Ejemplos y demos** tenés un checkout funcionando en sandbox y una galería de variantes para copiar. ### Setup paso a paso > Primero tu backend, después tu página: **1) conseguí el JWT, 2) creá tu clave de cifrado** — con eso dos ya podés tokenizar una tarjeta (te lo probamos con `curl` más abajo). Recién después viene el HTML/SDK para embeberlo en tu checkout. **Paso 1 — Conseguí tu JWT (backend).** Es el mismo Bearer token de **Autenticación** (`POST /oauth/token` con tus credenciales). Mintealo **server-side** — nunca en el navegador: Paso 1 · JWT: ```bash curl -X POST https://procobro.akua.la/oauth/token \ -H "Content-Type: application/json" \ -d '{"grant_type":"client_credentials","client_id":"","client_secret":""}' # → 200 { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 43200 } ``` > **Nunca pongas tu `client_secret` en el navegador.** Tu página solo recibe el token ya minteado (dura 12 h). Si alguien lo inspecciona, ve un JWT que expira — no tus credenciales. **Paso 2 — Creá tu clave de cifrado (backend, obligatorio).** Con el mismo token, tu backend crea su par de claves RSA **una sola vez** (se rota una vez al año) — sin headers extra: Paso 2a · crear la clave (una sola vez, no en cada arranque): ```bash curl -X POST https://procobro.akua.la/v1/encryption/keys \ -H "Authorization: Bearer " # → 201 # { "kid": "key_…", "algorithm": "RSA-OAEP-256", "status": "active", # "public_key": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n", # "valid_from": "…", "valid_until": "…" } ``` > ⚠️ Ese `POST` **reemplaza** la clave activa si ya existe una — no lo llames en cada arranque de tu app. Guardá el `public_key` (PEM) de tu lado y, en arranques posteriores, traelo con `GET` (mismo objeto, no rota nada): Paso 2b · traer la clave activa (sin rotar): ```bash curl https://procobro.akua.la/v1/encryption/keys \ -H "Authorization: Bearer " ``` **Receta lista para pegar** — resuelve los pasos 1 y 2 juntos: mintea el token y trae/crea la clave (GET primero, POST solo si no hay clave activa), sin dependencias: Receta · Node.js — token + clave (server-side): ```json // corré esto en TU backend, nunca en el navegador async function mintToken() { const r = await fetch('https://procobro.akua.la/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'client_credentials', client_id: process.env.AKUA_CLIENT_ID, client_secret: process.env.AKUA_CLIENT_SECRET, }), }); const { access_token } = await r.json(); return access_token; } async function getOrCreateEncryptionKey(token) { // 1) reusar la clave activa si ya existe let r = await fetch('https://procobro.akua.la/v1/encryption/keys', { headers: { Authorization: `Bearer ${token}` }, }); if (r.ok) return (await r.json()).public_key; // 2) si no hay ninguna, crearla (esto pasa UNA vez en la vida del comercio) r = await fetch('https://procobro.akua.la/v1/encryption/keys', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }); const { public_key } = await r.json(); return public_key; } // Cacheá el resultado (memoria/DB) — no repitas esta llamada en cada request. const token = await mintToken(); const publicKey = await getOrCreateEncryptionKey(token); ``` > **Con el JWT y la clave ya podés tokenizar una tarjeta** — sin escribir una sola línea de HTML todavía. Esto es para que tu equipo de tecnología pruebe/entienda el mecanismo con `curl`; **no** es el camino de tu integración real (eso es el SDK, pasos 3 a 6). Cifrá cada campo con la `public_key` del paso 2 (RSA-OAEP, SHA-256) — **los 5 campos son obligatorios, incluido el CVV**: Cifrar cada campo (repetir por cada uno): ```bash echo -n "4111111111111111" | openssl pkeyutl -encrypt \ -pubin -inkey pub.pem -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 \ | base64 -w0 # repetí para expiration_month ("12"), expiration_year ("30"), holder_name ("Ana Gómez") y cvv ("123") ``` Tokenizar con los campos cifrados: ```bash curl -X POST https://procobro.akua.la/v1/instruments \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "type": "CARD", "encrypted_number": "", "encrypted_expiration_month": "", "encrypted_expiration_year": "", "encrypted_holder_name": "", "encrypted_cvv": "" }' # → 201 { "id": "ins-…", "status": "ACTIVE", "card": { "last_four_digits": "1111", … } } ``` Para tu checkout real **no armás vos el cifrado** — eso lo hace el SDK, dentro del iframe, con la misma clave. Seguí con los pasos 3 a 6. **Paso 3 — El HTML.** Un contenedor vacío donde se monta el campo, tu botón de pago (deshabilitado hasta que el campo esté listo) y el script del SDK: Paso 3 · HTML: ```bash
``` **Paso 4 — Montá el campo.** Apuntá `apiBase` al servicio de campos y pasá el token (paso 1) y la clave de cifrado (paso 2) — `encryptionPublicKey` es obligatorio, sin ella `mount()` no tokeniza: Paso 4 · JS: ```json var sf = AkuaFields.mount('#akua-card', { apiBase: 'https://akua-fields-akua-la.vercel.app', token: '', // paso 1 encryptionPublicKey: '', // paso 2 — obligatorio amount: 100000, currency: 'COP', // accent: '#e96024', // opcional: pisa el color de tu marca }); ``` **Paso 5 — Habilitá el botón cuando el campo esté completo.** `onReady` avisa que el iframe cargó (y que la clave de cifrado se aplicó); `onValidity` se dispara cada vez que el cliente tipea, con `true` cuando la tarjeta está completa y válida (número, vencimiento, nombre **y CVV**): Paso 5 · eventos: ```json AkuaFields.mount('#akua-card', { // ...opciones del paso 4, onReady: function () { pagar.disabled = false; }, onValidity: function (ok) { pagar.disabled = !ok; }, onResult: function (r) { // r = { instrument_id, payment_id, status, ... } if (r.status === 'APPROVED') { /* confirmá la orden */ } }, }); ``` **Paso 6 — Cobrá.** Conectá tu botón a `sf.pay()`. El SDK cifra número, vencimiento, nombre y CVV con tu clave **dentro del iframe** (el mismo mecanismo que probaste con `curl` en el paso 2), tokeniza, cobra, y te entrega el resultado en `onResult` — con el `instrument_id` ya guardado para cobros futuros: Paso 6 · pagar: ```json pagar.onclick = function () { pagar.disabled = true; sf.pay(); }; ``` **Opcional — asigná la tarjeta a un cliente.** Esto no tiene nada que ver con el cifrado (que ya es obligatorio y está resuelto en los pasos 3-4). Pasando `user`, la tarjeta queda guardada en su cuenta para cobros futuros. Y `sf.enroll()` guarda **sin cobrar** (ej. "agregar medio de pago"): Opcional · con cliente: ```json // cobrar y asociar la tarjeta al cliente: sf.pay({ user: { name: 'Ana Gómez', email: 'ana@example.com' } }); // o cliente existente: sf.pay({ user: { id: 'usr-…' } }); // guardar la tarjeta SIN cobrar: sf.enroll({ user: { email: 'ana@example.com' } }); // → { user_id, instrument_id, status: 'ENROLLED' } ``` > Probalo con la tarjeta `4111 1111 1111 1111`, venc `12/30`, CVV `123`. En producción, el dominio del servicio de campos te lo entrega tu proveedor junto con las credenciales. ### Ejemplos y demos Nada explica mejor que verlo andando. Los tres recursos corren contra **sandbox** — pagá con la tarjeta de prueba y mirá el resultado en vivo: - [Checkout demo · "Mi Tienda"](https://akua-fields-akua-la.vercel.app) — Un checkout completo de comercio, funcionando end-to-end en sandbox - [Galería de ejemplos](https://akua-fields-examples-akua-la.vercel.app) — Variantes listas para copiar: mínimo y tienda con marca - [Kit de inicio (GitHub)](https://github.com/akua-la/akua-fields-examples) — Checkout completo + backend de ejemplo — cloná, poné tus credenciales y cobrá El **ejemplo mínimo completo** — una página entera que cobra, en ~25 líneas. Tu backend le sirve el token y la clave de cifrado (ver **receta Node.js** en el paso anterior); copiala, apuntale a esos dos endpoints y ya estás cobrando en sandbox: checkout-minimo.html: ```bash

Pagar $100.000 COP

``` Y el **kit completo corriendo en tu máquina** — checkout + un backend de ejemplo sin dependencias que muestra exactamente qué implementa tu servidor (mintear el token, generar/reusar la clave de cifrado, crear el cliente, cobrar con reintento seguro): Clonar y correr (Node 18+): ```bash git clone https://github.com/akua-la/akua-fields-examples cd akua-fields-examples cp .env.example .env # completá tus credenciales de sandbox node server.js # abre http://localhost:4000 y pagá con 4111 1111 1111 1111 ``` ### Akua Direct — tus propios campos (PCI) > **Solo para comercios con certificación PCI DSS (SAQ D).** Con Akua Direct los datos de tarjeta pasan por tu frontend y tu servidor. Si no tenés esa certificación, usá **Campos embebidos** — la misma UX, sin el scope. Armás tus propios inputs (sin iframe, control total del DOM) y tokenizás la tarjeta contra la API. Obtenés el mismo `instrument_id` que con los otros caminos: `POST /v1/instruments` ```json { "type": "CARD", "card": { "number": "4111111111111111", "expiration_month": "12", "expiration_year": "30", "holder_name": "Ana Gómez", "cvv": "123" } } ``` Respuesta · 201: ```json { "id": "ins-cu94vq1n8ql8u3v469gg", "type": "CARD", "status": "ACTIVE", "card": { "last_four_digits": "1111", "bin_data": { "card_brand": "VISA" } } } ``` Con el `ins-…` cobrás igual que siempre en **Crear un pago**. Tokenizar dos veces la misma tarjeta devuelve el mismo instrumento (dedup por fingerprint), así que podés tokenizar sin miedo a duplicar. | Error | Qué hacer | |---|---| | `422` número/CVV/fecha inválidos | Mostrale la validación al cliente — no reintentes igual | | `429` rate limit | Reintentá con backoff exponencial | | `5xx` transitorio | Reintentá con backoff — tokenizar es idempotente por fingerprint | > **Camino intermedio:** si querés control del servicio sin certificar tu e-commerce, self-hosteá el servicio de campos (repo en **Ejemplos y demos**) en tu propia infra — el scope PCI lo absorbe ese servicio y tu checkout sigue en SAQ A. ## Referencia ### Estados de un pago | Estado | Significado | |---|---| | `APPROVED` | Aprobado por el emisor (respuesta de la autorización) | | `AUTHORIZED` | Autorizado, pendiente de captura | | `REJECTED` | Rechazado (ver `response_code`) | | `PENDING` | En proceso | | `CANCELLED` | Anulado antes de captura | | `REFUNDED` | Reembolsado | ### Errores | Código | Causa | |---|---| | `401` | Token inválido o vencido | | `404` | Recurso o ruta inexistente | | `422` | Body inválido o campo requerido faltante | | `429` | Rate limit — reintentá con backoff | | `5xx` | Error transitorio — reintentá con el mismo `order_id` (idempotente) | ### Tarjetas de prueba (sandbox) | Número | Resultado | |---|---| | `4111 1111 1111 1111` | Aprobada (Visa) | | `5186 1700 7000 1108` | Aprobada (Mastercard) |