Cómo funciona
- Tu sistema crea un cobro y recibe el QR con el monto exacto que debe pagar tu cliente.
- Le muestras el QR (o la llave y el monto). Tu cliente paga desde Nequi, Bancolombia o cualquier banco por Bre-B.
- Nosotros vemos el aviso de tu banco y confirmamos el pago. Te enteras consultando el cobro o, al instante, por webhook.
Base: https://app.comfirme.com/api/v1. Todo es JSON y los montos van en pesos enteros.
Autenticación
Manda tu llave en la cabecera Authorization: Bearer cf_live_…. Úsala solo desde tu servidor: nunca la pongas en el código de tu página web ni en la app.
cf_test_…: de prueba. No toca plata real y puedes simular pagos.cf_live_…: real. Usa tus llaves Bre-B y confirma con los avisos de tu banco.
Límite: 60 peticiones por minuto por llave (10 por minuto para subir comprobantes).
Tipos de cobro
automatico(por defecto): si nadie tiene apartado ese monto, tu cliente paga el precio exacto y se confirma solo con el aviso del banco (verificacion: "aviso"). Si ya hay otro cobro abierto por el mismo monto, este se valida con comprobante (verificacion: "comprobante"), también con el precio exacto. Mira siempre el campo verificacion para saber si debes pedirle la foto del comprobante a tu cliente.monto_unico: siempre por aviso. El monto a pagar es el precio menos unos pesos (por ejemplo $19.999 por un cobro de $20.000): esa diferencia es lo que identifica a tu cliente. Muestra montoAPagar, no tu precio.comprobante: siempre con comprobante. Tu cliente paga el precio exacto y tú subes la foto de su comprobante. La foto sola no aprueba nada: buscamos el aviso del banco que cuadre con ella (monto, nombre, hora y banco).
Tus llaves Bre-B
GET /llaves
Tus llaves en el orden de Ajustes → Mi Bre-B, con su id, su tipo (personal o negocios) y su monto máximo. Por cada correo puedes tener una llave personal y una de Negocios.
Al crear un cobro, llave elige por cuál se cobra. Si no la mandas (o mandas "automatica"), se elige sola entre las que aceptan el monto. Con una personal y una de Negocios caben dos cobros del mismo precio a la vez, los dos con el monto exacto, uno por cada llave: el aviso de tu banco dice a cuál llegó cada pago.
curl https://app.comfirme.com/api/v1/llaves \
-H "Authorization: Bearer $COMFIRME_LLAVE"{
"llaves": [
{ "id": "a1b2c3", "llave": "0093354356", "tipo": "negocios", "nombre": "Nequi Negocios",
"orden": 1, "verificada": true, "montoMaximo": null },
{ "id": "d4e5f6", "llave": "@tunegocio", "tipo": "personal", "nombre": "Personal de Nequi",
"orden": 2, "verificada": true, "montoMaximo": 2000000 }
]
}Crear un cobro
POST /cobros
| monto | Obligatorio. Pesos enteros, de $500 a $50.000.000. |
| tipo | automatico (por defecto), monto_unico o comprobante. |
| llave | Opcional. El id de una de tus llaves (GET /llaves), o automatica (por defecto). |
| descripcion | Opcional. Hasta 140 caracteres. |
| referencia | Opcional. Tu número de pedido. Si la repites mientras el cobro sigue abierto, te devolvemos el mismo (200) en vez de crear otro: protege contra doble clic y reintentos. |
| metadata | Opcional. Hasta 20 campos tuyos; te los devolvemos tal cual. |
curl -X POST https://app.comfirme.com/api/v1/cobros \
-H "Authorization: Bearer $COMFIRME_LLAVE" \
-H "Content-Type: application/json" \
-d '{"monto": 20000, "descripcion": "Corte + barba", "referencia": "pedido-1234"}'Respuesta (201):
{
"id": "cob_8f3a1c…",
"tipo": "automatico",
"verificacion": "aviso",
"estado": "pendiente",
"modo": "live",
"montoSolicitado": 20000,
"montoAPagar": 20000,
"qr": "000201010212…",
"qrImagen": "https://…/api/v1/cobros/cob_8f3a1c…/qr?firma=…",
"llave": "@tunegocio",
"llaveId": "a1b2c3",
"tipoLlave": "negocios",
"descripcion": "Corte + barba",
"referencia": "pedido-1234",
"metadata": { "cliente": "Juan" },
"creadoEn": "2026-10-05T19:30:00.000Z",
"expiraEn": "2026-10-05T19:45:00.000Z",
"pago": null
}qrImagen es un PNG listo para un <img> o para mandar por WhatsApp; no necesita la llave. qr es el texto del QR, por si lo quieres dibujar tú.
Verificar un cobro
GET /cobros/{id}
Consúltalo cada 3 a 5 segundos mientras tu cliente tiene el QR delante, o espera el webhook.
curl https://app.comfirme.com/api/v1/cobros/cob_8f3a1c… \
-H "Authorization: Bearer $COMFIRME_LLAVE"| pendiente | Esperando el pago. |
| pagado | Llegó el pago y cuadra. Ya puedes entregar. |
| revisar | Llegó el pago pero hay una duda (por ejemplo, el nombre). Revísalo en tu panel. |
| esperando_aviso | El comprobante se ve bien, pero el aviso del banco aún no llega. Se confirma solo cuando llegue. |
| rechazado | El comprobante no sirvió. Mira comprobante.motivo; puedes subir otro (hasta 5). |
| expirado | Pasó el tiempo sin pago (15 min por aviso, 24 h con comprobante). |
| cancelado | Lo cancelaste. |
Si alguien paga un cobro expirado (hasta 48 h después), igual pasa a pagado y te llega el webhook.
Subir un comprobante
POST /cobros/{id}/comprobante
Solo en cobros con verificacion: "comprobante". JPG, PNG o WebP de hasta 3 MB, como archivo (multipart/form-data, campo imagen) o en JSON como data URL. Leemos todo lo que se pueda (banco, monto, nombre, fecha, hora, referencia…) y lo devolvemos en comprobante.datos.
curl -X POST https://app.comfirme.com/api/v1/cobros/cob_8f3a1c…/comprobante \
-H "Authorization: Bearer $COMFIRME_LLAVE" \
-F "[email protected]"| no_es_comprobante | La imagen no es un comprobante de transferencia. |
| ilegible | No se alcanza a leer el monto. |
| transferencia_fallida | El comprobante dice que la transferencia falló. |
| monto_distinto | El monto no es el que había que pagar. |
| destinatario_distinto | Le pagaron a otra llave. |
| fecha_distinta | El comprobante es de antes de crear el cobro. |
| comprobante_repetido | Esa misma imagen ya se usó en otro cobro. |
| aviso_ya_usado | El pago de ese comprobante ya se le acreditó a otro cobro. |
| ambiguo | Hay varios pagos que cuadran y no se puede saber cuál es. |
| aviso_no_llego | Pasaron 48 h y el aviso del banco nunca llegó. |
Cancelar un cobro
POST /cobros/{id}/cancelar
Cancela los cobros que tu cliente abandonó: así el monto exacto queda libre enseguida para el siguiente y no tiene que pagar con comprobante ni con pesos de menos.
curl -X POST https://app.comfirme.com/api/v1/cobros/cob_8f3a1c…/cancelar \
-H "Authorization: Bearer $COMFIRME_LLAVE"Simular un pago (solo pruebas)
POST /cobros/{id}/simular — solo con llaves cf_test_.
- En un cobro por aviso lo marca como pagado. Manda
{"resultado": "revisar"}para probar el otro caso. - En uno con comprobante crea un aviso de banco falso (
remitente,banco,monto). Después sube un comprobante de verdad con ese nombre y monto, y verás cómo cruza.
# Cobro por aviso: lo marca como pagado
curl -X POST https://app.comfirme.com/api/v1/cobros/cob_8f3a1c…/simular \
-H "Authorization: Bearer $COMFIRME_LLAVE_PRUEBA" \
-H "Content-Type: application/json" \
-d '{"resultado": "pagado", "remitente": "Juan Pérez"}'
# Cobro con comprobante: crea el aviso falso con el que cruzará tu comprobante
curl -X POST https://app.comfirme.com/api/v1/cobros/cob_8f3a1c…/simular \
-H "Authorization: Bearer $COMFIRME_LLAVE_PRUEBA" \
-H "Content-Type: application/json" \
-d '{"remitente": "Juan Pérez", "banco": "Nequi", "monto": 20000}'Webhooks
Configura tu URL en Ajustes → Desarrolladores. Te mandamos un POST con JSON cuando un cobro cambia: cobro.pagado, cobro.revisar, cobro.esperando_aviso, cobro.comprobante_rechazado y cobro.cancelado. El cuerpo trae el cobro completo, igual que en la consulta.
{
"id": "evt_4b2e…",
"tipo": "cobro.pagado",
"modo": "live",
"creadoEn": "2026-10-05T19:32:10.000Z",
"cobro": {
"id": "cob_8f3a1c…",
"estado": "pagado",
"montoAPagar": 20000,
"referencia": "pedido-1234",
"pago": { "monto": 20000, "remitente": "VICTOR GELVES", "banco": "Nequi", "recibidoEn": "…" },
"…": "el resto del cobro"
}
}Responde con un 2xx en menos de 10 segundos. Si falla, reintentamos a 1, 5 y 30 minutos. Puede llegar el mismo evento dos veces: usa su id para no procesarlo dos veces. Antes de entregar un pedido, si tienes dudas, consulta el cobro.
Verifica la firma: la cabecera Comfirme-Firma: t=…,v1=… es el HMAC-SHA256 de `${t}.${cuerpo}` con tu secreto. Usa el cuerpo crudo, sin volver a convertirlo en JSON.
import crypto from "node:crypto";
// Express: app.post("/webhooks/comfirme", express.raw({ type: "application/json" }), manejar)
function firmaValida(cuerpoCrudo, cabecera, secreto) {
const partes = Object.fromEntries(cabecera.split(",").map((p) => p.split("=")));
const esperada = crypto
.createHmac("sha256", secreto)
.update(`${partes.t}.${cuerpoCrudo}`)
.digest("hex");
const vieja = Math.abs(Date.now() / 1000 - Number(partes.t)) > 300; // 5 minutos
return !vieja && crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(partes.v1 ?? ""));
}
function manejar(req, res) {
if (!firmaValida(req.body.toString(), req.get("Comfirme-Firma") ?? "", process.env.COMFIRME_WEBHOOK_SECRETO)) {
return res.sendStatus(400);
}
const evento = JSON.parse(req.body.toString());
if (evento.tipo === "cobro.pagado") {
// marca el pedido evento.cobro.referencia como pagado
}
res.sendStatus(200);
}Errores
Siempre con la forma {"error": {"codigo": "…", "mensaje": "…"}}.
| 400 | json_invalido |
| 401 | llave_faltante, llave_invalida |
| 403 | sin_llave_breb, qr_bloqueado, solo_en_pruebas |
| 404 | cobro_no_existe |
| 409 | sin_montos_disponibles, sin_llave_para_monto, llave_no_acepta_monto, referencia_en_uso, estado_no_permite, sin_intentos |
| 413 | imagen_muy_grande |
| 422 | monto_invalido, tipo_invalido, llave_no_existe, datos_invalidos, imagen_invalida |
| 429 | demasiadas_peticiones (mira Retry-After) |
| 502 | analisis_no_disponible: no pudimos leer el comprobante ahora; reintenta |
¿Dudas con la integración? Escríbenos a [email protected].