Documentación · API v1
Cobra en tu web con validación bancaria al instante
Tu cliente paga por pago móvil o Botón de Pago directo a TU cuenta bancaria — el dinero nunca pasa por nosotros. Lo que hace ArmorPay es confirmar contra el banco, en segundos, que ese pago llegó, alcanza y no se usó antes. Tu pedido se confirma cuando el banco confirma — nunca antes.
Antes de empezar
- 1
Tu comercio tiene que estar ACTIVO
El alta se hace una sola vez en armorpay.net/registro: subes tus documentos, registras la cuenta bancaria de tu empresa y nosotros la verificamos contra el banco. Mientras tu comercio no esté activo, la API responde 401 a todo — no pierdas tiempo depurando tu código si aún estás en revisión. - 2
Crea tu llave de API
En tu panel: API → Crear llave. Se muestra UNA sola vez — guárdala en la configuración de tu servidor. Empieza conak_live_. - 3
Registra tu webhook (recomendado)
En API → Webhooks: pon la URL de tu servidor y guarda el secreto (whsec_...). Es la vía en la que tu tienda se entera de cada pago confirmado sin preguntar. Puedes integrar sin webhook usandoGET /intents/{id}, pero el webhook es lo que hace la confirmación instantánea.
¿Cómo pruebo? (no hay modo sandbox)
La API valida contra pagos reales del banco, así que la prueba honesta es un pago real chiquito: crea un intent de 1,00 Bs, paga por pago móvil a tu propia cuenta desde otro banco, y valida la referencia. Es un pago a tu propia empresa: no pierdes nada y pruebas el circuito completo, webhook incluido.
Las 3 vías de integración
De menos a más código. Las tres confirman con las mismas reglas — elige por comodidad, no por seguridad.
| Vía | Código que escribes | Úsala si… |
|---|---|---|
| Plugin WooCommerce | Ninguno: instalar y pegar 2 valores | Tu tienda es WordPress + WooCommerce. |
| Checkout alojado | 1 llamada + 1 redirección | Cualquier carrito propio: nosotros ponemos el formulario de pago. |
| API completa | Tu propio formulario + 2-3 llamadas | Quieres la experiencia 100% con tu marca, o cobras desde una app. |
El flujo completo, de punta a punta
Sea cual sea la vía, por debajo siempre pasan estas cuatro cosas, en este orden:
- 1
Tu servidor crea el intent (el cobro que esperas)
Con el monto que TU sistema calculó — nunca un monto que declare el navegador del cliente. El intent vence a los 30 minutos. - 2
Tu cliente paga
Por pago móvil a tu cuenta (y te da los últimos dígitos de la referencia del comprobante), o por Botón de Pago C2P (genera una clave en su banco y el débito es al instante). - 3
ArmorPay valida contra el banco
El pago existe, el monto alcanza y esa referencia no se usó antes — ni en tu web ni en tus cajas físicas. Es el mismo árbitro antifraude para todos tus canales. - 4
Tu tienda se entera y entrega
Por el webhook firmado (instantáneo) o consultando el intent. Cuando el status es CONFIRMED, entregas el pedido.
En curl, la integración mínima con checkout alojado son DOS pasos:
# 1. Crear el intent (server-to-server, desde tu backend)
curl -X POST https://armorpay.net/api/v1/intents \
-H "Authorization: Bearer ak_live_TU_LLAVE" \
-H "Idempotency-Key: pedido-8812" \
-H "Content-Type: application/json" \
-d '{ "externalRef": "8812", "amountVES": "1450.00", "concepto": "Pedido 8812" }'
# → 201 { "intent": { "id": "cmm...", "status": "PENDING", ... } }
# 2. Redirigir al cliente a la página de pago
https://armorpay.net/pay/cmm...
# 3. (automático) Al confirmarse te llega el webhook intent.confirmed
# — o consultas tú mismo:
curl https://armorpay.net/api/v1/intents/cmm... \
-H "Authorization: Bearer ak_live_TU_LLAVE"
# → 200 { "intent": { "status": "CONFIRMED", "method": "REFERENCIA", ... } }Autenticación
Todas las llamadas llevan tu llave en el header Authorization. La llave es secreta y solo de servidor: nunca la pongas en el navegador de tus clientes, en el código fuente visible de tu tienda ni en una app instalable. Si se te filtra, revócala y crea otra desde el panel — al instante.
Base: https://armorpay.net/api/v1 Autorización: Authorization: Bearer ak_live_... Límites: 60 peticiones/min por llave · 15 intentos/5 min por IP en validación de referencia. Al superarlos: 429 con Retry-After. # La API es server-to-server: no hay CORS abierto. Si intentas llamarla # con fetch() desde el navegador, fallará — y así debe ser: proteger tu # llave es proteger tu dinero.
Cobros (intents)
Un intent es un cobro que esperas recibir. Lo creas server-to-server con el monto que TÚ decides — la validación compara contra ese monto, nunca contra lo que declare el cliente final.
POST/api/v1/intents
GET/api/v1/intents/{id}
POST /api/v1/intents
Authorization: Bearer ak_live_...
Idempotency-Key: pedido-8812 # obligatorio: único por pedido
Content-Type: application/json
{
"externalRef": "8812", # el id del pedido en TU sistema
"amountVES": "1450.00", # máx. 2 decimales; string o número
"concepto": "Tienda X pedido 8812" # opcional, ≤40 tras sanear
}
# ¿Tus precios están en dólares? Manda amountUSD EN VEZ de amountVES:
# congelamos el monto en Bs con la tasa BCV del momento, y la validación
# acepta también USD × tasa vigente (el que paga con la tasa de hoy no falla).
# { "externalRef": "8812", "amountUSD": "25.00" }
# → el intent trae además amountUSD y exchangeRateUsed.
→ 201
{
"intent": {
"id": "cmm...", # úsalo para validar o redirigir a /pay
"externalRef": "8812",
"amountVES": "1450.00",
"concepto": "Tienda X pedido 8812",
"method": null, # REFERENCIA | C2P al confirmarse
"status": "PENDING", # ver ciclo de vida abajo
"referencia": null,
"overpaidVES": null,
"expiresAt": "2026-08-06T21:30:00.000Z",
"confirmedAt": null,
"createdAt": "2026-08-06T21:00:00.000Z"
}
}
# Reintentar con la MISMA Idempotency-Key devuelve el mismo intent (200):
# un timeout de red nunca duplica un cobro. Usa el id de TU pedido como
# key y el reintento sale gratis.
GET /api/v1/intents/{id}
→ 200 { "intent": { ...la misma forma... } }
# Consúltalo al volver el cliente a tu tienda o como respaldo del webhook.
# Es de lectura: consultarlo no cambia nada.La Idempotency-Key sale del PEDIDO, no de cada carga de la página
Es el error de integración más común que hemos visto en producción: la key se genera con un id nuevo en cada render, así que un F5 del comprador —o un doble clic en «Pagar»— abre un cobro nuevo en vez de recuperar el que ya existía. Los intents huérfanos vencen y ensucian tus reportes; el comprador termina con dos pantallas de pago para el mismo carrito.
✗ Idempotency-Key: armorpay-${crypto.randomUUID()} // nueva en cada render
✓ Idempotency-Key: pedido-8812 // el id de TU pedidoCon la key del pedido, recargar devuelve el mismo intent con 200. Y si ese intent ya venció (30 min), ahí sí toca uno nuevo: agrégale un sufijo de intento — pedido-8812-2 — en vez de un id al azar.
Ciclo de vida del intent
PENDING ──(pago validado)──────────→ CONFIRMED (final: entrega el pedido) │ └──(30 min sin confirmarse)─────→ EXPIRED (final: crea uno nuevo)
CONFIRMED y EXPIRED son finales: un intent nunca se confirma dos veces ni «revive» después de vencer. Si el cliente quiere pagar un pedido vencido, crea un intent nuevo con otra Idempotency-Key (por ejemplo pedido-8812-2).
Validar una referencia
Tu cliente ya pagó por pago móvil a tu cuenta y te da los últimos dígitos de la referencia de su comprobante (pídele 6 o más). Nosotros confirmamos que el pago existe, alcanza y no se usó antes — el mismo árbitro antifraude que usan las cajas físicas.
Manda lo que te dé el cliente, tal cual: el banco nos notifica la referencia en su forma y cada banco pagador la muestra a su manera. Emparejamos por el final, así que da igual si viene con ceros de más, con espacios o con guiones. Lo único que exigimos son 6 dígitos de verdad.
POST/api/v1/intents/{id}/validate-reference
{
"referencia": "789123" # 6 a 20 dígitos, del comprobante
} # sirve completa o solo el final: los
# ceros de adelante y los separadores
# se limpian de nuestro lado
→ 200 (confirmado)
{
"intent": { ... "status": "CONFIRMED", "method": "REFERENCIA" ... },
"pago": {
"referencia": "000000789123",
"banco": "BDT", # banco receptor
"bancoPagador": "0134 · Banesco",
"montoVES": "1450.00",
"overpaidVES": null, # sobrepago aceptado y registrado
"fecha": "2026-08-06",
"hora": "153000"
}
}
Reglas de monto: se acepta un faltante de hasta max(1 Bs, 0.5%).
Subpago → 422 INSUFFICIENT_AMOUNT (con faltanteVES).
Sobrepago → se confirma y queda en overpaidVES.
Referencia ya cobrada (en caja o por otro intent) → 409 REFERENCE_ALREADY_USED.El campo de tu formulario: 6 a 20 dígitos, y no recortes
Cada banco pagador le muestra la referencia a su manera: unos dan 9 dígitos, otros 12 con ceros por delante, otros la separan con espacios. Nosotros emparejamos por el final en los dos sentidos, así que da igual cuál de las dos venga más larga — pero solo si tu formulario deja escribir o pegar lo que el banco le mostró al comprador.
✗ <input maxlength="9" pattern="\d{6,9}"> // el comprador no puede pegar la suya
✓ <input inputmode="numeric"> // manda lo pegado tal cualNo le quites espacios ni ceros antes de mandárnosla: eso lo hacemos nosotros. Y no la recortes a los últimos 6 «por si acaso» — mientras más dígitos manda el comprador, menos ambigüedad hay si tiene dos pagos parecidos.
404 PAYMENT_NOT_FOUND no siempre es un error del cliente
La notificación del banco tarda unos segundos en llegarnos después de que tu cliente paga. Si validas en el instante siguiente al pago, puede responder 404. El patrón correcto: reintenta la misma llamada cada 5-10 segundos durante 1-2 minutos antes de decirle al cliente que verifique su pago. Si tras 2 minutos sigue en 404, lo más probable es que el pago haya ido a otra cuenta.
Cobro C2P (Botón de Pago)
Cobro activo: tu cliente genera una clave de pago (OTP) desde la app o banca en línea de su banco, te la da junto a su celular y cédula, y el débito ocurre al instante — sin comprobantes ni referencias que copiar. Requiere que tu comercio tenga C2P habilitado (se tramita con nosotros; en tu panel se ve si ya lo tienes).
POST/api/v1/intents/{id}/c2p
{
"celular": "04121234567", # 04 + 9 dígitos (0412, 0422, …)
"bancoPagador": "0102", # del catálogo C2P (ver Bancos)
"cedula": "V12345678",
"otp": "12345678" # clave dinámica que generó tu cliente
}
→ 200 confirmado: { "intent": {...CONFIRMED...}, "cobro": { "referencia", "montoComision", ... } }
→ 422 C2P_REJECTED: rechazo del banco, con "hint" en español y
"retriable": true — puedes reintentar con una clave nueva
mientras el intent no venza.
→ 502 BANK_UNAVAILABLE: el banco no respondió. NO asumas rechazo:
verifica con tu cliente antes de reintentar.
El monto y el concepto salen del intent — el body nunca los lleva.
Pobla el select de bancos con GET /banks?service=c2p (los códigos del
catálogo C2P no siempre coinciden con los del BCV).Muéstrale al comprador el motivo, no «error al procesar»
El rechazo más común del C2P es la clave dinámica mal escrita o vencida — y se arregla en 10 segundos si el comprador se entera. Nosotros traducimos lo que responde el banco; píntalo tal cual y deja el formulario listo para reintentar con una clave nueva.
→ 422
{
"code": "C2P_REJECTED",
"message": "Clave de pago incorrecta", # titular, ya en español
"hint": "La clave dinámica está mal escrita, venció o ya se usó.
Genera una nueva desde tu banco e intenta otra vez.",
"codres": "C2P0104", # el código crudo del banco
"retriable": true # el intent sigue vivo
}Si el banco responde algo que no conocemos, en hint va su texto crudo: preferimos decirte lo que dijo el banco antes que inventarte un motivo. Con retriable: true el intent sigue vivo hasta que venza — no hace falta crear otro.
La marca del banco en tu checkout
El Banco del Tesoro pide que su logo acompañe el flujo del Botón de Pago. Nuestra página /pay ya lo muestra; si cobras C2P con tu propia interfaz sobre esta API, muéstralo junto al formulario. Sírvelo directo de nuestro dominio: https://armorpay.net/bancos/bt-marca.png (marca a color, para fondos claros) o https://armorpay.net/bancos/bt-blanco.png (marca y nombre en blanco, para fondos oscuros).
Tasa BCV
Fija tus precios con la misma tasa con la que nosotros congelamos y validamos: cero discrepancias entre tu carrito y el cobro.
GET/api/v1/exchange-rate
→ 200
{ "currency": "USD/VES", "rate": "168.4200", "source": "BCV",
"fetchedAt": "2026-08-06T14:00:00.000Z" }
# Sin tasa utilizable: 503 RATE_UNAVAILABLE — nunca inventamos una.Cumplimiento en Venezuela
Si tu catálogo muestra precios en divisas, la norma exige que el precio en bolívares esté exhibido y que la conversión sea a tasa oficial BCV, con la moneda y la tasa claramente informadas — nunca una tasa paralela, y nunca precios distintos según el método de pago. Nuestra página de pago ya lo resuelve en el paso de cobro (Bs como monto principal + «Ref. USD … · tasa oficial BCV …»); para tu catálogo, usa este endpoint y muestra ambos. Esto es una guía, no asesoría legal.
Bancos
GET/api/v1/banks
GET /api/v1/banks # lista BCV — para mostrar el banco pagador
GET /api/v1/banks?service=c2p # catálogo PROPIO del C2P — para poblar el
# select de un cobro C2P (sus códigos no
# siempre coinciden con los del BCV)
→ 200 { "service": "...", "banks": [{ "code": "0102", "name": "..." }] }Checkout alojado (la vía rápida)
Si no quieres construir el formulario: crea el intent y redirige (o abre en iframe) nuestra página de pago. Muestra tu razón social y tu logo, guía al cliente por referencia o C2P según lo que tu comercio tenga habilitado, reintenta sola mientras llega la notificación del banco, y confirma con las mismas reglas de la API.
Redirección: https://armorpay.net/pay/{intentId}
# No lleva parámetros de retorno: la página no redirige de vuelta sola.
# Pon tú un enlace/botón «volver a la tienda» en tu página de gracias, o
# usa el iframe para quedarte en tu dominio:
En iframe, te avisamos por postMessage:
window.addEventListener("message", (e) => {
const a = e.data?.armorpay;
if (a?.event === "confirmed") { /* pagado: a.intentId, a.externalRef */ }
if (a?.event === "expired") { /* venció sin pagar */ }
});
# El postMessage es UX (cerrar el modal, mostrar el check): la señal de
# VERDAD para entregar el pedido es el webhook o GET /intents/{id}.
# Un navegador puede fabricar un postMessage; tu servidor no debe creerle.Plugin WooCommerce
Todo lo de arriba, sin escribir código: el plugin crea el intent al hacer el pedido, manda al cliente a la página de pago, recibe el webhook firmado y marca el pedido como pagado — con un respaldo por consulta cuando el cliente vuelve a la tienda.
- 1
Instala el plugin
Descarga el .zip y súbelo en Plugins → Añadir nuevo → Subir plugin. Se actualiza solo cuando publicamos versiones nuevas. - 2
Pega tus 2 valores
En WooCommerce → Ajustes → Pagos → ArmorPay: tu Llave de API (ak_live_...) y el Secreto del webhook (whsec_...). El título y la descripción que ve tu cliente en el checkout también se editan ahí. - 3
Registra el webhook apuntando a tu tienda
En tu panel de ArmorPay (API → Webhooks), la URL es tu tienda más/?wc-api=armorpay— por ejemplohttps://mitienda.com/?wc-api=armorpay. El secreto que te dé el panel es el que pegas en el paso 2. - 4
Prueba con un pedido real de 1 Bs
Crea un producto de prueba, cómpralo tú mismo pagando 1 Bs a tu cuenta, y verifica que el pedido pase a «Procesando». Luego borra el producto.
Webhooks firmados
Registra tu URL en tu panel (API → Webhooks) y te avisamos a tu servidor cada confirmación o vencimiento — con firma, para que verifiques que fuimos nosotros.
POST a tu URL
x-armorpay-timestamp: 1754516096 # epoch en segundos
x-armorpay-signature: hex(HMAC-SHA256(secreto, timestamp + "." + body))
{ "event": "intent.confirmed", # o "intent.expired"
"intent": { ...la misma forma de la API... } }
— Verificación en Node.js —
const crypto = require("node:crypto");
function verificar(secreto, timestamp, firma, bodyCrudo) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const esperada = crypto.createHmac("sha256", secreto)
.update(timestamp + "." + bodyCrudo).digest("hex");
return crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(firma));
}
— Verificación en PHP —
function verificar($secreto, $timestamp, $firma, $bodyCrudo) {
if (abs(time() - (int)$timestamp) > 300) return false;
$esperada = hash_hmac("sha256", $timestamp . "." . $bodyCrudo, $secreto);
return hash_equals($esperada, $firma);
}
# Usa el body CRUDO (antes de parsear el JSON): re-serializarlo
# cambia bytes y la firma deja de coincidir.Reglas de la casa
- Responde 2xx rápido (y procesa después si tu trabajo es lento). Sin 2xx, reintentamos 5 veces con espera creciente: 1 min, 5 min, 30 min, 2 h y 12 h — después la entrega queda marcada fallida y puedes reenviarla a mano desde tu panel.
- Procesa una sola vez: entre reintentos y reenvíos, el mismo evento puede llegarte dos veces. Usa
intent.id + eventcomo clave: si ya lo procesaste, responde 200 y no hagas nada. - ¿Rotaste el secreto? Desde el panel puedes rotarlo cuando quieras; actualiza tu servidor en el momento — las entregas siguientes ya van firmadas con el nuevo.
Errores
Toda respuesta de error trae un code estable (programa contra él) y un message en español (muéstralo si te sirve).
| HTTP | code | Qué hacer |
|---|---|---|
| 401 | UNAUTHORIZED | Revisa la llave: inválida, inactiva o el comercio no está activo. |
| 429 | RATE_LIMITED | Espera lo que diga Retry-After y reintenta. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | Manda el header Idempotency-Key al crear intents. |
| 400 | VALIDATION / INVALID_AMOUNT | El body no cumple el formato; el detalle viene en issues. |
| 404 | INTENT_NOT_FOUND | Ese intent no existe (o no es tuyo). |
| 410 | INTENT_EXPIRED | Venció: crea un intent nuevo. |
| 404 | PAYMENT_NOT_FOUND | El pago aún no llegó (o la referencia es de otra cuenta). Reintenta cada 5-10 s durante 1-2 min. |
| 422 | INSUFFICIENT_AMOUNT | Subpago: faltanteVES dice cuánto falta. |
| 409 | AMBIGUOUS_REFERENCE | Pide más dígitos de la referencia. |
| 409 | REFERENCE_ALREADY_USED | Ese pago ya se cobró; cobradoPor dice dónde. |
| 422 | C2P_NOT_ENABLED | El comercio no tiene C2P habilitado todavía. |
| 422 | C2P_REJECTED | El banco rechazó: muestra message y hint tal cual, y deja reintentar con clave nueva. |
| 502 | BANK_UNAVAILABLE | El banco no respondió: verifica antes de reintentar. |
| 422 | MERCHANT_NOT_READY | El comercio no tiene cuentas activas. |
| 503 | RATE_UNAVAILABLE | Sin tasa BCV utilizable: reintenta o cobra en VES. |
Checklist antes de salir a producción
- ✓El monto lo calcula tu servidor — y va en el intent. Nada del navegador del cliente decide cuánto se cobra.
- ✓La llave vive solo en tu servidor — no en JavaScript del navegador, no en el repositorio público, no en una app.
- ✓Verificas la firma de cada webhook — con el body crudo, y descartas timestamps de más de 5 minutos.
- ✓Entregas pedidos solo con CONFIRMED — del webhook o de GET /intents/{id} — nunca por el postMessage ni porque el cliente 'volvió' a tu tienda.
- ✓Manejas PAYMENT_NOT_FOUND con reintentos — la notificación del banco tarda segundos; no lo trates como fallo definitivo.
- ✓Tu campo de referencia acepta 6 a 20 dígitos — sin maxlength de 9 y sin recortar lo que el comprador pega: cada banco se la muestra distinto.
- ✓La Idempotency-Key sale del pedido — no de un id nuevo por render — si no, un F5 abre un cobro nuevo.
- ✓Le muestras al comprador el motivo del rechazo — message y hint del 422, sobre todo en C2P: casi siempre es solo la clave dinámica.
- ✓Procesas cada evento una sola vez — mismo intent.id + event repetido = responder 200 sin repetir la entrega.
- ✓Hiciste una compra real de 1 Bs — de punta a punta, webhook incluido, antes de anunciar el botón de pago.
Tus ventas en línea, sus estados y cada webhook entregado o fallido los ves en tu panel (Ventas y API → Webhooks) — la misma fuente que usa esta API.
¿Algo no cuadra entre estas docs y la API? Es un bug nuestro — escríbenos a info@armorpay.net.