VerifikaDocs
Guías

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!».

El comprobante llega antes que el aviso del bancoTu clienteWhatsAppTu bottu servidorVerifikaapi.verifika.techBancodel negocioManda la foto del comprobanteLa imagen + tu metadataPOST /v1/verificationsRegistrado, esperando al banco201 · PENDING · AWAITING_BANK«Recibimos tu comprobante…»El banco todavía no avisa: pueden pasar unos minutosAviso del pagocorreo · SMS · pushCruza monto, referencia y horaWebhook firmado, con tu metadataverification.verified«¡Listo! Tu pago ya está confirmado»
  1. Tu cliente → Tu bot: Manda la foto del comprobante
  2. Tu bot → Verifika: La imagen + tu metadata (POST /v1/verifications)
  3. Verifika → Tu bot: Registrado, esperando al banco (201 · PENDING · AWAITING_BANK)
  4. Tu bot → Tu cliente: «Recibimos tu comprobante…»
  5. (El banco todavía no avisa: pueden pasar unos minutos)
  6. Banco → Verifika: Aviso del pago (correo · SMS · push)
  7. Verifika: Cruza monto, referencia y hora
  8. Verifika → Tu bot: Webhook firmado, con tu metadata (verification.verified)
  9. Tu bot → Tu cliente: «¡Listo! Tu pago ya está confirmado»
El comprobante llega antes que el aviso del banco · desliza el dibujo hacia los lados

Qué pasa por dentro

  1. Tu bot manda la imagen con POST /v1/verifications, con tu id de conversación en metadata o en reporter.externalId. Si el banco aún no avisó, la respuesta es 201 con status: "PENDING", code: "AWAITING_BANK", un message para el cliente y nextStep.until: hasta cuándo esperamos el aviso.
  2. 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.
  3. Si cuadra, la verificación pasa a VERIFIED y sale el webhook verification.verified.
  4. Si el banco nunca lo respalda, al vencer la espera pasa a NOT_FOUND y sale verification.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.

EventoQué pasóQué le dice tu bot al cliente
verification.verifiedEl 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_foundVenció 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.
verification.verified
{
  "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:

  1. 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.
  2. reporter.externalId — tu id del cliente o del chat. Además de volver en el webhook, se puede buscar: GET /v1/verifications?externalId=wa_573104567890 trae todas las de ese cliente.
  3. El id de la verificación — guárdalo junto a tu conversación al recibir el 201.

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

  1. Abre Integraciones → Webhooks → Agregar webhook. Tu plan tiene que incluir los webhooks (ver Empezar).
  2. En General, la URL de tu servidor —https y pública— y una descripción («Bot de WhatsApp · producción»). Si vas a probar con una llave vk_test_, enciende Recibir también eventos de prueba.
  3. En Eventos, marca verification.verified y verification.not_found.
  4. Si tu servidor pide credencial (un API gateway, un n8n con autenticación), configúrala en Autenticación. Ver Webhooks §3.
  5. Guarda y copia el secreto (whsec_…): con él compruebas la firma.
  6. Revisa cada entrega —lo que mandamos y lo que respondió tu servidor— en Webhooks → Historial.
app.verifika.tech/integraciones?seccion=webhooks&agregar=1
Agregar webhook → General: tu URL y, para probar, los eventos de prueba.
app.verifika.tech/integraciones?seccion=webhooks&agregar=1
Pestaña Eventos: marca las dos verificaciones por API.

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:

  1. Comprueba la firma sobre el cuerpo crudo, antes de parsear el JSON.
  2. Descarta lo repetido por id de evento: un reintento puede entregarte algo que ya procesaste.
  3. Mira livemode: un evento de una llave de prueba trae false (y la cabecera Verifika-Livemode: false). Nunca despaches un pedido por uno de esos.
  4. Responde 2xx en 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.
  5. 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.

En esta página