Guía de integración

Procobro

Comenza a aceptar todo tipo de pagos Apuntá tu integración a procobro.akua.la

Montevideo, URY

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.

Reemplazá <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.

Ver la referencia completa →

Probalo
curl -X POST https://procobro.akua.la/oauth/token \
  -H "Content-Type: application/json" \
  -d '
{
  "client_id": "<CLIENT_ID>",
  "client_secret": "<CLIENT_SECRET>"
}
'
Respuesta · 200
{
  "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).

Este 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.

Ver la referencia completa →

Probalo
curl -X POST https://procobro.akua.la/v1/encryption/keys \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Respuesta · 201
{
  "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.

Sin los 5 campos 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.

Ver la referencia completa →

Probalo
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>"
}
'
Respuesta · 201
{
  "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.

Embebé la 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.

Ver la referencia completa →

Probalo
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"
  }
}
'
Respuesta · 201
{
  "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.

Nunca mandes 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.

Ver la referencia completa →

Probalo
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>"
}
'
Respuesta · 201
{
  "id": "orden-0001-c-xxxx",
  "status": "APPROVED",
  "response_code": "00",
  "approval_code": "079662"
}

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.

Ver la referencia completa →

¿Integrás con un asistente de IA? Pasale /llms-full.txt — toda esta doc en markdown, lista para que tu agente escriba la integración. Y para buscar acá, apretá /.

Base URL

AmbienteURL
Sandbox
Producciónhttps://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

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.

Probalo
curl -X POST https://procobro.akua.la/oauth/token \
  -H "Content-Type: application/json" \
  -d '
{
  "client_id": "<CLIENT_ID>",
  "client_secret": "<CLIENT_SECRET>"
}
'
Respuesta · 200
{
  "access_token": "eyJhbGciOi…",
  "token_type": "Bearer",
  "expires_in": 43200
}
Ejemplo
curl -X POST https://procobro.akua.la/v1/payments \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{ ... }'

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
CampoTipoReq.Notas
amountnumberMonto en la unidad de la moneda
currencystringISO 4217 (COP, USD, …)
order_idstringTu referencia única — sirve para idempotencia
instrument_idstringToken de la tarjeta enrolada (ins-…)
encrypted_cvvstringCVV cifrado con tu clave — no se vaultea, hay que reenviarlo en cada pago
Probalo
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>"
}
'
Respuesta · 201
{
  "id": "orden-83778-c-xxxx",
  "status": "APPROVED",
  "response_code": "00",
  "amount": "1000",
  "approval_code": "079662"
}

Obtener un pago

GET/v1/payments/{id}
Probalo
curl -X GET https://procobro.akua.la/v1/payments/{id} \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Respuesta
{
  "status": "AUTHORIZED"
}

Listar pagos

GET/v1/payments?limit=25
Probalo
curl -X GET https://procobro.akua.la/v1/payments?limit=25 \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Respuesta
{
  "data": [ { "id": "pay-…", "status": "APPROVED" } ],
  "has_more": false
}

Reembolsar un pago

POST/v1/refunds

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.

Probalo
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

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.

Probalo
curl -X POST https://procobro.akua.la/v1/payments/{id}/cancel \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Probalo
curl -X POST https://procobro.akua.la/v1/payments/{id}/capture \
  -H "Authorization: Bearer $ACCESS_TOKEN"

Contracargos

GET/v1/chargebacks
Probalo
curl -X GET https://procobro.akua.la/v1/chargebacks \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Respuesta
{
  "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.

RailMecanismoCómo confirma el clienteVigencia
PSERedirección al portal del bancoSe autentica en su banco (rail.payment_link)~21 min
NEQUINotificación push a la appAprueba con su PIN en Nequi45 min
DAVIPLATAOTP por SMS (sin app)Ingresa el OTP en tu checkout (paso confirm)15 min
BRE_BQR / alias interoperableEscanea el QR desde cualquier app bancaria15 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:

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:

CampoTipoReq.Notas
order_idstringTu referencia única (idempotencia)
amountnumberMonto en COP, mayor a 0
currencystringDebe ser COP
rail_idstringPSE · NEQUI · DAVIPLATA · BRE_B
rail_typestringWALLET (Nequi/Daviplata) · BANK_TRANSFER (PSE/Bre-B)
descriptionstringAparece en el comprobante del cliente
return_urlstringPSEURL pública a la que vuelve el cliente tras el banco
customer.phone_numberstringNequi/DaviplataCelular colombiano registrado en la billetera (3001234567)
customer.bank_idstringPSEBanco elegido — id de GET /api/banks
customer.documentstringPSENúmero de documento del pagador
customer.document_typestringPSECC · CE · NIT · PASSPORT
customer.first_namestringPSENombre del pagador
customer.last_namestringPSEApellido del pagador
customer.emailstringPSEACH Colombia envía el comprobante a este correo
Campo de la respuestaNotas
payment_idGuardalo — es el id del pago (para consultar estado y para el confirm de Daviplata)
transaction.status / status_detailEstado inicial — ver la tabla de estados abajo
rail.payment_linkPSE/Bre-B: URL a la que redirigís al cliente
rail.qr.image / rail.qr.contentBre-B: QR para mostrar (imagen data-URI + contenido EMV)
rail.aliasBre-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`SignificadoAcción
IN_PROGRESS / PENDINGEl cliente aún no completóMostrar "procesando"
IN_PROGRESS / PENDING_PROVIDEREl banco (PSE) está procesandoPolling cada 15–30s
APPROVED / SUCCESSAprobado; fondos en tránsitoConfirmar el pedido
DECLINED / REJECTEDRechazado o expiradoNotificar; ofrecer alternativa
FAILEDError técnicoRegistrar; 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 webhookSignificado
payment.purchase.pendingIntento creado; esperando al cliente
payment.purchase.succeededAprobado → confirmar el pedido
payment.purchase.rejectedRechazado o expirado
payment.purchase.failedError técnico
payment.purchase.confirm.processedDaviplata: OTP confirmado, débito ejecutado
payment.purchase.confirm.failed.retryableDaviplata: OTP incorrecto, reintentable
Probalo
curl -X POST https://procobro.akua.la/v1/payments/intents \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Respuesta
{
  "order_id": "orden-83790",
  "amount": 50000,
  "currency": "COP",
  "rail_id": "NEQUI",
  "rail_type": "WALLET",
  "description": "Orden 83790 - mitienda.com",
  "customer": { "phone_number": "3001234567" }
}
Respuesta
{
  "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"
  }
}
Respuesta
{
  "order_id": "orden-83792",
  "amount": 150000,
  "currency": "COP",
  "rail_id": "BRE_B",
  "rail_type": "BANK_TRANSFER"
}
Respuesta · 201 (PSE — redirección)
{
  "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)
{
  "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"
  }
}
Payload del webhook
{
  "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ámetroReq.Notas
countryISO 3166-1 alpha-3 — COL
rail_idRail para el que se listan las entidades — PSE

Usá el id del banco elegido como customer.bank_id al crear el intento.

Probalo
curl -X GET https://procobro.akua.la/api/banks?country=COL&rail_id=PSE \
  -H "Authorization: Bearer $ACCESS_TOKEN"
Respuesta
[
  { "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:

POST/v1/payments/intents
CampoTipoReq.Notas
rail_idstringDebe ser DAVIPLATA
rail_typestringDebe ser WALLET
customer.phone_numberstringCelular colombiano registrado en Daviplata
customer.documentstringRecomendado: sube la tasa de aprobación
customer.document_typestringCC · CE · TI

Paso 2 — confirmar el OTP que el cliente recibió por SMS (válido 15 min):

POST/v1/payments/intents/{payment_id}/confirm
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.
Probalo
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"
  }
}
'
Probalo
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"
}
'

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

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.

Ciclo de vidacreatedopenedused
Probalo
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"
      }
    }
  }
}
'
Respuesta · 201
{
  "id": "lnk-d7ac0929",
  "url": "https://checkout.akua.la/links/lnk-d7ac0929",
  "type": "card_enrollment",
  "status": "created"
}
Embeber en iframe
<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ónEndpoint
Obtener instrumentoGET /v1/instruments/{id}
Listar instrumentosGET /v1/instruments
Eliminar instrumentoDELETE /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.

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.
Flujogenerás tu clave (una vez)cliente tipea + cifra en el iframetokenizeinstrument_idpagoAPPROVED

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.

CaminoIdeal para¿Tocás datos de tarjeta?Scope PCI
Links de pagoCompartir por WhatsApp, email o QR — sin frontendNuncaSAQ A
Enrolamiento (iframe)Guardar tarjetas con el formulario hosteado completoNuncaSAQ A
Campos embebidosCheckout propio con tu diseño, pago en tu páginaNuncaSAQ A
Akua DirectComercios ya certificados PCI que quieren campos 100% propiosSAQ 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:

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:

⚠️ 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):

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:

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:

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"):

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.
Paso 1 · JWT
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 }
Paso 2a · crear la clave (una sola vez, no en cada arranque)
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": "…" }
Paso 2b · traer la clave activa (sin rotar)
curl https://procobro.akua.la/v1/encryption/keys \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
Receta · Node.js — token + clave (server-side)
// 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);
Cifrar cada campo (repetir por cada uno)
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
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", … } }
Paso 3 · HTML
<div id="akua-card"></div>
<button id="pagar" disabled>Pagar</button>

<script src="https://akua-fields-akua-la.vercel.app/akua-fields.js"></script>
Paso 4 · JS
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
});
Paso 5 · eventos
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 · pagar
pagar.onclick = function () {
  pagar.disabled = true;
  sf.pay();
};
Opcional · con cliente
// 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):

checkout-minimo.html
<!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>
Clonar y correr (Node 18+)
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

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.

ErrorQué hacer
422 número/CVV/fecha inválidosMostrale la validación al cliente — no reintentes igual
429 rate limitReintentá con backoff exponencial
5xx transitorioReintentá 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.
Probalo
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"
  }
}
'
Respuesta · 201
{
  "id": "ins-cu94vq1n8ql8u3v469gg",
  "type": "CARD",
  "status": "ACTIVE",
  "card": {
    "last_four_digits": "1111",
    "bin_data": { "card_brand": "VISA" }
  }
}

Estados de un pago

EstadoSignificado
APPROVEDAprobado por el emisor (respuesta de la autorización)
AUTHORIZEDAutorizado, pendiente de captura
REJECTEDRechazado (ver response_code)
PENDINGEn proceso
CANCELLEDAnulado antes de captura
REFUNDEDReembolsado

Errores

CódigoCausa
401Token inválido o vencido
404Recurso o ruta inexistente
422Body inválido o campo requerido faltante
429Rate limit — reintentá con backoff
5xxError transitorio — reintentá con el mismo order_id (idempotente)

Tarjetas de prueba (sandbox)

NúmeroResultado
4111 1111 1111 1111Aprobada (Visa)
5186 1700 7000 1108Aprobada (Mastercard)