Procobro
Comenza a aceptar todo tipo de pagos Apuntá tu integración a procobro.akua.la
Empezá en 7 pasos
De cero a tu primera operación en minutos, directo desde la terminal. Solo necesitás las credenciales de sandbox que te da Scanntech.
Obtené tu token
Canjeá el client_id y client_secret que recibís en el onboarding de Scanntech por un Bearer token. Dura 12 horas: cachealo y renovalo antes de que expire.
<CLIENT_ID> y <CLIENT_SECRET> por tus credenciales de sandbox. Guardá el access_token de la respuesta — va en el header Authorization de todos los pasos siguientes.curl -X POST https://procobro.akua.la/oauth/token \
-H "Content-Type: application/json" \
-d '
{
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>"
}
'{
"access_token": "eyJhbGciOi…",
"token_type": "Bearer",
"expires_in": 43200
}Creá tu clave de cifrado
Antes de tokenizar necesitás tu propia clave RSA. No podés tokenizar ni cobrar sin esto — es un paso obligatorio, no opcional. La creás una sola vez con el mismo token (se rota una vez al año).
POST reemplaza la clave activa si ya existe una — no lo repitas en cada arranque. Guardá el public_key (PEM) de tu lado; para reusarla sin rotar, hacé GET a este mismo path.curl -X POST https://procobro.akua.la/v1/encryption/keys \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"kid": "key_d965l7hg7c83rpt50pag",
"algorithm": "RSA-OAEP-256",
"status": "active",
"public_key": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n",
"valid_until": "2027-07-07T01:47:11Z"
}Tokenizá una tarjeta (Campos embebidos)
Con tu clave, cifrá cada campo (RSA-OAEP, SHA-256) antes de tokenizar — número, vencimiento, nombre del titular y CVV: los 5 son obligatorios. En tu checkout esto lo hace solo el SDK de Campos embebidos, dentro del iframe; acá mostramos el contrato con los valores ya cifrados.
encrypted_* (incluido encrypted_cvv) la tokenización se rechaza con 422. El id de la respuesta es el instrument_id que usás para cobrar en el paso siguiente.curl -X POST https://procobro.akua.la/v1/instruments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"type": "CARD",
"encrypted_number": "<cifrado>",
"encrypted_expiration_month": "<cifrado>",
"encrypted_expiration_year": "<cifrado>",
"encrypted_holder_name": "<cifrado>",
"encrypted_cvv": "<cifrado>"
}
'{
"id": "ins-cu94vq1n8ql8u3v469gg",
"type": "CARD",
"status": "ACTIVE",
"card": {
"last_four_digits": "1111",
"bin_data": { "card_brand": "VISA" }
}
}Alternativa — enrolá una tarjeta (iframe hosteado)
Si en cambio preferís el formulario 100% hosteado por Akua (sin SDK en tu página), generá un link de enrolamiento y embebelo en un iframe — el cliente carga la tarjeta ahí y recibís un instrument_id por webhook, sin tocar datos de tarjeta ni manejar cifrado vos.
url de la respuesta en un <iframe>. Al completarse, Akua hace POST a tu webhook con { "instrument_id": "ins-…" } — ese token es el que usás para cobrar.curl -X POST https://procobro.akua.la/v1/instruments/enrollments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"type": "card_enrollment",
"expires_in": 3600,
"data": {
"webhook": "https://mitienda.com/webhooks/tarjeta",
"api_key": "mi-api-key-secreta"
}
}
'{
"id": "lnk-d7ac0929",
"url": "https://checkout.akua.la/links/lnk-d7ac0929",
"status": "created"
}Cobrá con el instrumento
Con el instrument_id que te devolvió el paso anterior (tokenización cifrada o enrolamiento), cobrá sin tocar el número de tarjeta y mirá el APPROVED en la respuesta.
card a esta API — se rechaza con 422. El CVV no se guarda en la bóveda — a diferencia del número, tenés que reenviarlo cifrado (encrypted_cvv) en cada pago, aunque el instrument_id ya exista. Usá tu propio order_id como clave de idempotencia.curl -X POST https://procobro.akua.la/v1/payments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"amount": 1000,
"currency": "COP",
"order_id": "orden-0001",
"instrument_id": "ins-cu94vq1n8ql8u3v469gg",
"encrypted_cvv": "<cifrado>"
}
'{
"id": "orden-0001-c-xxxx",
"status": "APPROVED",
"response_code": "00",
"approval_code": "079662"
}Generá un link de pago
Si preferís no tocar datos de tarjeta, creá una URL de checkout hosteada y compartila por WhatsApp, email o QR.
url de la respuesta es lo que le mandás a tu cliente. Cuando paga, el link te devuelve el payment_id asociado.curl -X POST https://procobro.akua.la/v1/links \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"type": "payment",
"data": {
"amount": {
"value": 25000,
"currency": "COP"
},
"description": "Orden #12345",
"redirect_url": "https://mitienda.com/gracias"
}
}
'{
"id": "lnk-a1b2c3d4",
"url": "https://checkout.akua.la/links/lnk-a1b2c3d4",
"status": "created"
}Salí a producción
Validaste tus flujos en sandbox: pedile a Scanntech tus credenciales productivas y la base URL de producción. Mismos endpoints, mismos contratos.
/llms-full.txt — toda esta doc en markdown, lista para que tu agente escriba la integración. Y para buscar acá, apretá /.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):
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.
curl -X POST https://procobro.akua.la/oauth/token \
-H "Content-Type: application/json" \
-d '
{
"client_id": "<CLIENT_ID>",
"client_secret": "<CLIENT_SECRET>"
}
'{
"access_token": "eyJhbGciOi…",
"token_type": "Bearer",
"expires_in": 43200
}curl -X POST https://procobro.akua.la/v1/payments \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{ ... }'Crear un pago
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.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.| 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 |
curl -X POST https://procobro.akua.la/v1/payments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"amount": 1000,
"currency": "COP",
"order_id": "orden-83778",
"description": "Orden 83778 - mitienda.com",
"instrument_id": "ins-cu94vq1n8ql8u3v469gg",
"encrypted_cvv": "<cifrado>"
}
'{
"id": "orden-83778-c-xxxx",
"status": "APPROVED",
"response_code": "00",
"amount": "1000",
"approval_code": "079662"
}Obtener un pago
curl -X GET https://procobro.akua.la/v1/payments/{id} \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"status": "AUTHORIZED"
}Listar pagos
curl -X GET https://procobro.akua.la/v1/payments?limit=25 \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"data": [ { "id": "pay-…", "status": "APPROVED" } ],
"has_more": false
}Reembolsar un pago
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.
curl -X POST https://procobro.akua.la/v1/refunds \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"payment_id": "pay-cu94vq1n8ql8u3v469gg",
"amount": 1000,
"currency": "COP"
}
'Cancelar / Capturar
Anula una autorización aún no capturada (void). Sin body.
Solo para captura diferida — por defecto los pagos se capturan automáticamente.
curl -X POST https://procobro.akua.la/v1/payments/{id}/cancel \
-H "Authorization: Bearer $ACCESS_TOKEN"curl -X POST https://procobro.akua.la/v1/payments/{id}/capture \
-H "Authorization: Bearer $ACCESS_TOKEN"Contracargos
curl -X GET https://procobro.akua.la/v1/chargebacks \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"data": [ { "id": "chb-…", "payment_id": "pay-…", "status": "OPEN" } ],
"has_more": false
}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 |
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.00 (ej. 50000) activa el mock del rail.Nequi — billetera, cobro push. Solo el celular del cliente:
PSE — débito bancario con redirección. Requiere return_url, el banco elegido (ver Listar bancos) y los datos del pagador:
Bre-B — QR interoperable. Body mínimo; la respuesta trae el QR y el alias:
| 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 |
| 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 |
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 |
curl -X POST https://procobro.akua.la/v1/payments/intents \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"order_id": "orden-83790",
"amount": 50000,
"currency": "COP",
"rail_id": "NEQUI",
"rail_type": "WALLET",
"description": "Orden 83790 - mitienda.com",
"customer": { "phone_number": "3001234567" }
}{
"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"
}
}{
"order_id": "orden-83792",
"amount": 150000,
"currency": "COP",
"rail_id": "BRE_B",
"rail_type": "BANK_TRANSFER"
}{
"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"
}
}{
"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"
}
}{
"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.
| Parámetro | Req. | Notas |
|---|---|---|
country | ✓ | ISO 3166-1 alpha-3 — COL |
rail_id | ✓ | Rail para el que se listan las entidades — PSE |
Usá el id del banco elegido como customer.bank_id al crear el intento.
curl -X GET https://procobro.akua.la/api/banks?country=COL&rail_id=PSE \
-H "Authorization: Bearer $ACCESS_TOKEN"[
{ "id": "1007", "name": "BANCOLOMBIA" },
{ "id": "1051", "name": "DAVIVIENDA" }
]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:
| 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):
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.curl -X POST https://procobro.akua.la/v1/payments/intents \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"order_id": "orden-83793",
"amount": 75000,
"currency": "COP",
"rail_id": "DAVIPLATA",
"rail_type": "WALLET",
"customer": {
"phone_number": "3001234567",
"document": "1234567890",
"document_type": "CC"
}
}
'curl -X POST https://procobro.akua.la/v1/payments/intents/{payment_id}/confirm \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"otp": "123456"
}
'Crear un link
Generá una URL de checkout hosteada — tu cliente paga sin que manejes datos de tarjeta. Compartila por WhatsApp, email o QR.
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).curl -X POST https://procobro.akua.la/v1/links \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"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"
}
}
'{
"id": "lnk-a1b2c3d4",
"url": "https://checkout.akua.la/links/lnk-a1b2c3d4",
"status": "created",
"expires_at": "2026-07-02T14:00:00Z"
}Consultar links
Devuelve el link con su status y, una vez pagado, el payment_id asociado. GET /v1/links lista todos, paginado.
curl -X GET https://procobro.akua.la/v1/links/{id} \
-H "Authorization: Bearer $ACCESS_TOKEN"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.
Embebé la url en tu página. El formulario se personaliza con metadata.styleConfig (textos, colores, layout) para que combine con tu sitio:
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.
curl -X POST https://procobro.akua.la/v1/instruments/enrollments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"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"
}
}
}
}
'{
"id": "lnk-d7ac0929",
"url": "https://checkout.akua.la/links/lnk-d7ac0929",
"type": "card_enrollment",
"status": "created"
}<iframe
src="https://checkout.akua.la/links/lnk-d7ac0929"
width="100%" height="600" frameborder="0"
style="max-width:500px;margin:0 auto;display:block;border:none">
</iframe>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} |
¿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.
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.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 |
Setup paso a paso
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 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:
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):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:
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:
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 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 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 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:
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"):
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.curl -X POST https://procobro.akua.la/oauth/token \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_id":"<CLIENT_ID>","client_secret":"<CLIENT_SECRET>"}'
# → 200 { "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 43200 }curl -X POST https://procobro.akua.la/v1/encryption/keys \
-H "Authorization: Bearer <ACCESS_TOKEN>"
# → 201
# { "kid": "key_…", "algorithm": "RSA-OAEP-256", "status": "active",
# "public_key": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----\n",
# "valid_from": "…", "valid_until": "…" }curl https://procobro.akua.la/v1/encryption/keys \
-H "Authorization: Bearer <ACCESS_TOKEN>"// 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);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")curl -X POST https://procobro.akua.la/v1/instruments \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"type": "CARD",
"encrypted_number": "<base64 de arriba>",
"encrypted_expiration_month": "<base64>",
"encrypted_expiration_year": "<base64>",
"encrypted_holder_name": "<base64>",
"encrypted_cvv": "<base64>"
}'
# → 201 { "id": "ins-…", "status": "ACTIVE", "card": { "last_four_digits": "1111", … } }<div id="akua-card"></div>
<button id="pagar" disabled>Pagar</button>
<script src="https://akua-fields-akua-la.vercel.app/akua-fields.js"></script>var sf = AkuaFields.mount('#akua-card', {
apiBase: 'https://akua-fields-akua-la.vercel.app',
token: '<JWT>', // paso 1
encryptionPublicKey: '<PEM>', // paso 2 — obligatorio
amount: 100000,
currency: 'COP',
// accent: '#e96024', // opcional: pisa el color de tu marca
});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 */ }
},
});pagar.onclick = function () {
pagar.disabled = true;
sf.pay();
};// 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' }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:
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:
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):
<!doctype html>
<html lang="es">
<body>
<h1>Pagar $100.000 COP</h1>
<div id="akua-card"></div>
<button id="pay" disabled>Pagar</button>
<script src="https://akua-fields-akua-la.vercel.app/akua-fields.js"></script>
<script>
// token y clave de cifrado: los sirve TU backend (nunca minteados en el navegador)
Promise.all([
fetch('/token').then(r => r.json()),
fetch('/encryption-key').then(r => r.json()),
]).then(([t, k]) => {
var sf = AkuaFields.mount('#akua-card', {
apiBase: 'https://akua-fields-akua-la.vercel.app',
token: t.token,
encryptionPublicKey: k.public_key, // obligatorio
amount: 100000, currency: 'COP',
onReady: function () { pay.disabled = false; },
onValidity: function (ok) { pay.disabled = !ok; },
onResult: function (r) { alert(r.status); },
});
pay.onclick = function () { pay.disabled = true; sf.pay(); };
});
</script>
</body>
</html>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 1111Akua Direct — tus propios campos (PCI)
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:
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 |
curl -X POST https://procobro.akua.la/v1/instruments \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '
{
"type": "CARD",
"card": {
"number": "4111111111111111",
"expiration_month": "12",
"expiration_year": "30",
"holder_name": "Ana Gómez",
"cvv": "123"
}
}
'{
"id": "ins-cu94vq1n8ql8u3v469gg",
"type": "CARD",
"status": "ACTIVE",
"card": {
"last_four_digits": "1111",
"bin_data": { "card_brand": "VISA" }
}
}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) |