Cuando el comprobante llega antes que el banco
El caso de todos los días — el cliente manda el comprobante, el banco todavía no avisa — y cómo tu bot le confirma minutos después con un webhook.
El caso
Son las 3:04 p. m. Un cliente le manda a tu bot de WhatsApp la foto de una transferencia de $85.000 por Nequi. Tu bot se la pasa a Verifika… y el banco todavía no le ha avisado al negocio. El correo de Nequi llega a las 3:06.
Esto no es un error: es lo normal. El comprobante viaja en el momento; el aviso del banco tarda desde segundos hasta unos minutos. Lo que no puede pasar es que tu bot se quede callado, ni que le diga al cliente «no encontramos tu pago» cuando el pago sí entró.
La solución es en dos tiempos: tu bot contesta «recibimos tu comprobante, esperamos al
banco» en el momento, y cuando el banco avisa, Verifika llama a tu servidor (un webhook)
con la verificación y tu metadata, y tu bot le escribe a ese mismo cliente «¡listo, tu
pago ya está confirmado!».
- Tu cliente → Tu bot: Manda la foto del comprobante
- Tu bot → Verifika: La imagen + tu metadata (POST /v1/verifications)
- Verifika → Tu bot: Registrado, esperando al banco (201 · PENDING · AWAITING_BANK)
- Tu bot → Tu cliente: «Recibimos tu comprobante…»
- (El banco todavía no avisa: pueden pasar unos minutos)
- Banco → Verifika: Aviso del pago (correo · SMS · push)
- Verifika: Cruza monto, referencia y hora
- Verifika → Tu bot: Webhook firmado, con tu metadata (verification.verified)
- Tu bot → Tu cliente: «¡Listo! Tu pago ya está confirmado»
Qué pasa por dentro
- Tu bot manda la imagen con
POST /v1/verifications, con tu id de conversación enmetadatao enreporter.externalId. Si el banco aún no avisó, la respuesta es201constatus: "PENDING",code: "AWAITING_BANK", unmessagepara el cliente ynextStep.until: hasta cuándo esperamos el aviso. - Llega el aviso del banco (correo, SMS o notificación push, según cómo conectó el negocio su banco). Verifika lo cruza contra los pagos pendientes por monto, referencia y hora.
- Si cuadra, la verificación pasa a
VERIFIEDy sale el webhookverification.verified. - Si el banco nunca lo respalda, al vencer la espera pasa a
NOT_FOUNDy saleverification.not_found. El pago queda además en Pagos sospechosos del negocio.
¿Cuánto espera Verifika antes de rendirse?
Lo que diga nextStep.until en la respuesta: la ventana de espera de tu plan
(limits.retry_pending_minutes en GET /v1/plan). Una
barrida corre cada 5 minutos, así que el NOT_FOUND puede llegar hasta 5 minutos después de esa
hora.
Si el negocio no tiene ningún canal del banco conectado, el code es AWAITING_REVIEW y
nextStep.type es MANUAL_REVIEW: nadie lo va a cruzar solo, y nunca se marca como no
encontrado —sin banco, un pendiente es «sin verificar», no fraude—. Si una persona del negocio lo
aprueba en el panel, llega verification.verified. Por eso
conectar el banco es el paso que no se
puede saltar.
Si el banco ya había avisado cuando mandaste el comprobante, el cruce ocurre en la misma
llamada: la respuesta trae status: "VERIFIED" y no hay nada que esperar.
Qué eventos escuchar
Suscribe verification.verified y verification.not_found. Llevan la verificación
entera —con tu metadata, tu reporter y el message nuevo para el cliente—, así que tu
handler sabe a quién escribirle sin consultar nada más.
| Evento | Qué pasó | Qué le dice tu bot al cliente |
|---|---|---|
verification.verified | El banco confirmó el pago (o una persona del negocio lo aprobó). | data.object.message: «¡Listo! Tu pago por $85.000 ya está confirmado por el banco. ✅» |
verification.not_found | Venció la espera y el banco nunca lo respaldó. | data.object.message: «No encontramos en el banco el pago por $85.000 de tu comprobante. Si ya pagaste, comunícate con el negocio para revisarlo.» Y avísale a una persona de tu equipo. |
{
"id": "evt_cmuh3x7z90042x8w1p5q6r7st",
"type": "verification.verified",
"created": "2026-10-12T20:06:12.000Z",
"apiVersion": "v1",
"companyId": "cmt7xbfrg0001q3w1itst7uwo",
"livemode": true,
"data": {
"object": {
"object": "verification",
"id": "vrf_3f9a1c0e7b2d4e8fa61c5d90b7e24a13",
"livemode": true,
"status": "VERIFIED",
"code": "VERIFIED",
"message": "¡Listo! Tu pago por $85.000 ya está confirmado por el banco. ✅",
"mode": "IMAGE",
"previousStatus": "PENDING",
"previousIncomeStatus": "PENDING",
"createdAt": "2026-10-12T20:00:06.000Z",
"resolvedAt": "2026-10-12T20:06:12.000Z",
"extracted": { "isReceipt": true, "amount": 85000, "reference": "M48213907", "bankLabel": "Nequi", "…": "…" },
"income": { "object": "income", "id": "cmuh2k7v40031x8w1d9q2n5rt", "status": "VERIFIED", "bankChannels": ["EMAIL"], "…": "…" },
"reporter": { "phone": "+573104567890", "name": "Laura Restrepo", "externalId": "wa_573104567890" },
"metadata": { "chatId": "wa_573104567890", "pedido": "PED-10293" },
"nextStep": null,
"warnings": []
}
}
}data.object.id es el mismo id (vrf_…) que te devolvió POST /v1/verifications, y
metadata vuelve tal cual la mandaste.
¿Ya escuchabas income.verified?
Sigue saliendo, igual que antes, para cualquier pago del negocio. Pero lleva el ingreso: ni el
id de la verificación, ni tu metadata, ni quién lo reportó. Para un bot que tiene que avisarle a
un cliente concreto, los verification.* son los que sirven.
Cómo reconocer tu conversación
Tienes tres formas, de la más directa a la más manual:
metadata— manda en la verificación lo que tu bot necesita para responder:{ "chatId": "wa_573104567890", "pedido": "PED-10293" }. Vuelve en el webhook. Hasta 20 claves, valores simples, 4 KB.reporter.externalId— tu id del cliente o del chat. Además de volver en el webhook, se puede buscar:GET /v1/verifications?externalId=wa_573104567890trae todas las de ese cliente.- El
idde la verificación — guárdalo junto a tu conversación al recibir el201.
Con metadata no hace falta guardar nada de tu lado para responder. Aun así, conviene
guardar el id: es con lo que preguntas GET /v1/verifications/{id} si se te perdió un webhook.
Configura el webhook en el panel
- Abre Integraciones → Webhooks → Agregar webhook. Tu plan tiene que incluir los webhooks (ver Empezar).
- En General, la URL de tu servidor —
httpsy pública— y una descripción («Bot de WhatsApp · producción»). Si vas a probar con una llavevk_test_, enciende Recibir también eventos de prueba. - En Eventos, marca
verification.verifiedyverification.not_found. - Si tu servidor pide credencial (un API gateway, un n8n con autenticación), configúrala en Autenticación. Ver Webhooks §3.
- Guarda y copia el secreto (
whsec_…): con él compruebas la firma. - Revisa cada entrega —lo que mandamos y lo que respondió tu servidor— en Webhooks → Historial.
O por API, si prefieres dejarlo en tu código de despliegue (hace falta una llave real con
webhooks.write; una de prueba no crea webhooks):
curl -X POST https://api.verifika.tech/v1/webhook-endpoints \
-H "Authorization: Bearer $VERIFIKA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://tubot.com/verifika/webhook",
"description": "Bot de WhatsApp · producción",
"events": ["verification.verified", "verification.not_found"],
"testEvents": true
}'El secret viene en la respuesta.
El handler, completo
Cinco reglas, y el código las sigue en orden:
- Comprueba la firma sobre el cuerpo crudo, antes de parsear el JSON.
- Descarta lo repetido por
idde evento: un reintento puede entregarte algo que ya procesaste. - Mira
livemode: un evento de una llave de prueba traefalse(y la cabeceraVerifika-Livemode: false). Nunca despaches un pedido por uno de esos. - Responde
2xxen menos de 5 segundos y manda el WhatsApp después. Si tardas más, para Verifika es un fallo y te lo vuelve a mandar. - No confíes en el orden de llegada. Usa
data.object.status, no «el último que llegó».
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRETO = process.env.VERIFIKA_WEBHOOK_SECRET; // whsec_…
function firmaValida(cuerpoCrudo, cabecera, secreto, toleranciaSeg = 300) {
const partes = cabecera.split(',').map((p) => p.trim());
const t = partes.find((p) => p.startsWith('t='))?.slice(2);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > toleranciaSeg) return false;
const esperada = crypto.createHmac('sha256', secreto).update(`${t}.${cuerpoCrudo}`).digest();
// Durante una rotación del secreto llegan dos `v1=`: basta con que una cuadre.
return partes
.filter((p) => p.startsWith('v1='))
.some((p) => {
const got = Buffer.from(p.slice(3), 'hex');
return got.length === esperada.length && crypto.timingSafeEqual(got, esperada);
});
}
// `express.raw` en ESTA ruta: la firma se calcula sobre los bytes tal cual llegaron.
app.post('/verifika/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const crudo = req.body.toString('utf8');
if (!firmaValida(crudo, req.get('Verifika-Signature') ?? '', SECRETO)) {
return res.status(400).send('firma inválida');
}
const evento = JSON.parse(crudo);
if (await db.eventoYaProcesado(evento.id)) return res.sendStatus(200); // repetido
await db.marcarEventoProcesado(evento.id);
res.sendStatus(200); // contesta YA; lo lento va después
if (!evento.type.startsWith('verification.')) return;
const verificacion = evento.data.object;
const { chatId, pedido } = verificacion.metadata ?? {};
if (!chatId) return;
// El mensaje para el cliente ya viene escrito.
await whatsapp.enviar(chatId, verificacion.message);
if (evento.type === 'verification.verified' && evento.livemode) {
await despacharPedido(pedido);
} else if (evento.type === 'verification.not_found') {
await alertarAlEquipo(verificacion);
}
});Pruébalo sin esperar a un pago real
Con una llave de prueba,
manda cualquier comprobante con "reference": "TEST_PENDING_VERIFIED": la respuesta es
PENDING y a los 30 segundos te llega verification.verified con livemode: false. Con
TEST_PENDING_NOT_FOUND llega verification.not_found. Tu webhook tiene que tener encendido
«Recibir también eventos de prueba». Todo el detalle está en Modo de prueba.
Reintentos: qué pasa si tu servidor no contesta
Si tu endpoint no responde 2xx en 5 segundos, Verifika reintenta con espera creciente —1 min,
5 min, 30 min, 2 h, 6 h, 12 h, 24 h—: ocho intentos en unas 48 horas. Cada entrega queda en el
historial de entregas
con lo que mandamos y lo que respondió tu servidor, y se puede reenviar a mano. Con 20 fallos
seguidos, el webhook se desactiva solo y se le avisa al dueño. El detalle está en
Webhooks.
Un reintento cuenta lo que pasó entonces
Si te llega un reintento de hace dos horas, cuenta lo que pasó hace dos horas. Antes de una acción
irreversible (despachar un pedido grande), confirma el estado actual con
GET /v1/verifications/{id}.
Mientras esperas, no preguntes cada minuto
Consultar GET /v1/verifications/{id} en bucle gasta tu
límite de peticiones y llega tarde de todos modos. Si tu bot de verdad
no puede recibir webhooks (corre detrás de un firewall sin entrada, por ejemplo), consulta como
mucho cada 30 segundos y para cuando status deje de ser PENDING o se pase nextStep.until.
Verifica desde tu propio bot
Tu bot recibe la foto del comprobante, se la pasa a Verifika y recibe el veredicto con la frase lista para contestarle al cliente.
Conciliación por API
Sube el extracto del banco, mira qué cuadra con el libro, resuelve lo que falta en lote y descarga el reporte — lo mismo que la pantalla de Conciliación, desde tu ERP o tu bot.