VerifikaDocs
Guías

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.

Si ya tienes un bot que recibe comprobantes por WhatsApp, lo que te falta no es el bot: es saber si ese comprobante es real. Basta con mandar la imagen. Verifika la lee con IA (monto, referencia, banco, fecha, quién pagó), revisa que no se haya usado antes, la cruza contra el aviso que el banco le mandó al negocio y te devuelve el veredicto con un message listo para reenviarle a tu cliente.

Verificar un comprobante con la imagenTu clienteWhatsAppTu bottu servidorVerifikaapi.verifika.techManda la foto del comprobanteLa imagen, tal cualPOST /v1/verificationsLee con IA y busca duplicadosCruza con el aviso del bancoVeredicto + frase para el clientestatus · code · messageLe reenvía el message
  1. Tu cliente → Tu bot: Manda la foto del comprobante
  2. Tu bot → Verifika: La imagen, tal cual (POST /v1/verifications)
  3. Verifika: Lee con IA y busca duplicados
  4. Verifika: Cruza con el aviso del banco
  5. Verifika → Tu bot: Veredicto + frase para el cliente (status · code · message)
  6. Tu bot → Tu cliente: Le reenvía el message
Verificar un comprobante con la imagen · desliza el dibujo hacia los lados

Antes de la primera llamada

  1. El plan incluye la API pública — ver Empezar, paso 1.
  2. El negocio tiene su banco conectado — Conectar bancos—. Sin eso no hay contra qué cruzar: lo que mandes queda pendiente de que una persona lo confirme.
  3. Una llave con el atajo «Registro de pagos»: crear la llave abre el formulario con ese atajo ya marcado.

Empieza con una llave de prueba

Una llave vk_test_ lee tus comprobantes con la misma IA, pero no registra nada ni gasta verificaciones, y con valores mágicos provocas cada veredicto (pendiente, duplicado, no encontrado…) para probar todas las ramas de tu bot. Crear una llave de prueba · Guía del modo de prueba.

La llamada

curl -X POST https://api.verifika.tech/v1/verifications \
  -H "Authorization: Bearer $VERIFIKA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "imageUrl": "https://cdn.tubot.com/comprobantes/7f3a.jpg",
    "expectedAmount": 85000,
    "reporter": { "phone": "+573104567890", "name": "Laura Restrepo", "externalId": "wa_573104567890" },
    "metadata": { "chatId": "wa_573104567890", "pedido": "PED-10293" }
  }'

¿No tienes la imagen en una URL pública? Mándala en base64 —con o sin el prefijo data:—:

{
  "imageBase64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ…",
  "reporter": { "externalId": "wa_573104567890" }
}

Dale tiempo a la llamada

Leer el comprobante con IA toma unos segundos (la lectura tiene un techo de 40 s). Pon a tu cliente HTTP un timeout de al menos 60 segundos, y si se corta, reintenta con la misma Idempotency-Key: nunca vas a registrar el mismo pago dos veces.

Qué manda cada campo

Lo mínimo es la imagen: imageUrl o imageBase64, una de las dos. Todo lo demás es opcional —la IA lee el monto, la referencia, el banco, la fecha y quién pagó—. Si mandas las dos, gana imageUrl.

CampoTipo¿Obligatorio?Qué es
imageUrlstring URLSí, si no mandas imageBase64La imagen del comprobante, en una URL https pública.
imageBase64stringSí, si no mandas imageUrlLa imagen en base64 (con o sin data:…;base64,). Hasta 15 MB ya decodificada.
mimeTypestringNoUna pista. El tipo real lo deciden los bytes: JPG, PNG, WebP, HEIC o PDF.
expectedAmountnumberNoLo que esperabas cobrar. Si el comprobante dice otro valor, el veredicto es AMOUNT_MISMATCH: perfecto para un pedido con precio fijo.
reporterobjetoNoQuién te lo mandó en tu sistema: phone, name, externalId (tu id de chat, de cliente o de pedido). No tiene que ser del equipo del negocio: es el cliente que pagó. Vuelve en la respuesta y en los webhooks.
metadataobjetoNoTus propios datos, tal cual: hasta 20 claves con valores simples, 4 KB. Vuelven en la respuesta y en todos los webhooks de esta verificación.
companyIdstringSolo si tu llave entra a varios negociosEl negocio sobre el que verificas.
reference, occurredAt, customerName, bankLabel, accountLabelNoPistas: solo se usan si la IA no leyó ese dato. El comprobante manda.

¿Y `amount` y `phone`?

No los mandes en este flujo. amount cambia de contrato: con él no se lee la imagen y pasan a ser obligatorios amount y phone (alguien del equipo del negocio). Es la alternativa para quien ya leyó el comprobante por su cuenta; está al final de esta guía. En la referencia verás las tres formas por separado, con lo obligatorio marcado con asterisco.

La respuesta

201 · pendiente del banco
{
  "object": "verification",
  "id": "vrf_3f9a1c0e7b2d4e8fa61c5d90b7e24a13",
  "livemode": true,
  "status": "PENDING",
  "code": "AWAITING_BANK",
  "message": "Recibimos tu comprobante por $85.000. Estamos esperando la confirmación del banco; en unos minutos te avisamos.",
  "mode": "IMAGE",
  "extracted": {
    "isReceipt": true,
    "amount": 85000,
    "reference": "M48213907",
    "bankLabel": "Nequi",
    "accountLabel": "Nequi ··4821",
    "occurredAt": "2026-10-12T19:58:00.000Z",
    "customerName": "Laura Restrepo",
    "confidence": 0.96,
    "note": null
  },
  "income": { "object": "income", "id": "cmuh2k7v40031x8w1d9q2n5rt", "status": "PENDING", "amount": 85000, "…": "…" },
  "duplicate": null,
  "reporter": { "phone": "+573104567890", "name": "Laura Restrepo", "externalId": "wa_573104567890" },
  "metadata": { "chatId": "wa_573104567890", "pedido": "PED-10293" },
  "receipt": { "stored": true, "source": "URL", "contentType": "image/jpeg", "size": 182340, "sha256": "9c1e5f2a…" },
  "nextStep": {
    "type": "AWAIT_BANK",
    "until": "2026-10-12T20:30:06.000Z",
    "webhookEvents": ["verification.verified", "verification.not_found"]
  },
  "warnings": []
}

Qué ramificar

status dice en qué quedó; code dice por qué y es estable: haz el switch sobre él. Y message está escrito para quien pagó —en español, tuteando, sin hablar del negocio ni de planes—: reenvíalo tal cual si tu bot no tiene una frase propia.

HTTPstatuscodeQué pasóQué hace tu bot
201VERIFIEDVERIFIEDEl banco ya había avisado: el pago entró.Entrega el pedido.
201PENDINGAWAITING_BANKRegistrado; falta el aviso del banco.Responde message y espera el webhook. Ver el caso completo.
201PENDINGAWAITING_REVIEWRegistrado, pero el negocio no tiene banco conectado: lo confirma una persona.Responde message. Si alguien lo aprueba, llega verification.verified.
200DUPLICATEDUPLICATEEse comprobante ya se usó (mismo archivo, misma referencia o la misma imagen). duplicate dice cuál.No entregues. Responde message.
200DUPLICATEPOSSIBLE_DUPLICATEMismo monto a la misma hora que otro pago, sin más pruebas. Queda en Pagos sospechosos para que el negocio lo revise.Responde message; no entregues hasta que el negocio confirme.
200REJECTEDNOT_A_RECEIPTLa imagen no es un comprobante (en extracted.note, qué parece).Pide la captura del comprobante.
200REJECTEDUNREADABLEEs un comprobante, pero no se lee el valor.Pide una foto más clara.
200REJECTEDUNSUPPORTED_FORMATUna foto HEIC que no se pudo abrir.Pide una captura de pantalla o un JPG.
200REJECTEDAMOUNT_MISMATCHEl valor del comprobante no es expectedAmount.Responde message: dice los dos valores.

Más tarde, por webhook o por GET, una verificación pendiente puede quedar VERIFIED o NOT_FOUND (NOT_FOUND_IN_BANK: venció la espera y el banco nunca la respaldó).

Ramificar en Node.js
switch (verification.code) {
  case 'VERIFIED':
    await entregarPedido(verification.metadata.pedido);
    break;
  case 'AWAITING_BANK':
  case 'AWAITING_REVIEW':
    // Guarda verification.id: el webhook te va a traer este mismo id y tu metadata.
    await guardarPendiente(verification.id, verification.metadata.chatId);
    break;
  // DUPLICATE, POSSIBLE_DUPLICATE, NOT_A_RECEIPT, UNREADABLE, UNSUPPORTED_FORMAT, AMOUNT_MISMATCH
  default:
    break;
}
await whatsapp.enviar(chatId, verification.message);

Consume una verificación de tu cupo solo cuando queda registrada (201). Un duplicado o un rechazo no gasta nada.

Errores que puede devolver

Los errores llevan details.retryable: si es true, vale la pena reintentar lo mismo (con la misma Idempotency-Key); si es false, reintentar va a fallar igual.

HTTPcodeQué pasóretryable
400INVALID_IMAGE_URL · BLOCKED_IMAGE_URLLa URL no es https, no es pública o apunta a una red interna.No
413IMAGE_TOO_LARGEEl comprobante pesa más de 15 MB.No
415IMAGE_TYPE_NOT_ALLOWEDEl archivo no es JPG, PNG, WebP, HEIC ni PDF (se mira el archivo, no mimeType).No
422IMAGE_REQUIRED · INVALID_IMAGE_BASE64No llegó la imagen, o el base64 no es válido.No
403PLAN_LIMIT_REACHED · ACCOUNT_*El negocio no puede verificar ahora (cupo del mes agotado o cuenta bloqueada). details.customerMessage es la frase para tu cliente, sin contarle por qué.No
502IMAGE_FETCH_FAILEDNo pudimos bajar la imagen de tu URL.Sí
504IMAGE_FETCH_TIMEOUTTu URL tardó más de 8 s.Sí
503AI_UNAVAILABLELa lectura con IA falló. Con retryable: false está apagada: manda los datos (amount, ver abajo).Según details.retryable

El catálogo completo está en Errores.

La imagen: dónde queda y cómo la bajamos

  • Se guarda siempre en nuestro almacenamiento privado y cifrado (AES-256), atada al pago si se registró, o a la verificación si fue duplicado o rechazo. Si tu enlace caduca mañana, la evidencia sigue existiendo. Nunca devolvemos un enlace a ella: receipt solo dice si se guardó, su tipo, su tamaño y su huella.
  • Para bajarla de tu URL: solo https, y comprobamos que cada salto de redirección (máximo 3) vaya a una dirección pública —nadie puede usarnos para tocar una red interna—. Tope de 15 MB, cortado mientras se descarga, y 8 segundos.
  • El tipo lo deciden los bytes, no la extensión ni el Content-Type: un HTML que dice ser image/png se rechaza.
  • Ni la imagen ni la URL completa quedan en nuestros registros: del base64 solo se anota el largo, y de la URL, el dominio.

Qué pasa con reporter

En este contrato el teléfono no se valida contra el equipo del negocio, a propósito: quien te manda la foto es el cliente que pagó, y casi nunca es del equipo. La autoridad la pone tu llave. En el libro del negocio, el pago queda con «Confirmó: Laura Restrepo» (o API · <nombre de tu llave> si no mandas reporter.name).

Consultar después

Para leerlas basta verifications.write (quien verifica puede preguntar en qué quedó) o verifications.read, si la llave solo consulta.

Alternativa: si tú ya leíste el comprobante

Si tu sistema ya extrajo los datos del comprobante (tu propio OCR, un formulario), manda amount. Con amount no se lee la imagen: se usan tus datos, y hace falta el phone de alguien del equipo del negocio (dueño, supervisor o cajero; si no está en Equipo, 404 PHONE_NOT_REGISTERED). La imagen, si la mandas, queda como evidencia.

curl -X POST https://api.verifika.tech/v1/verifications \
  -H "Authorization: Bearer $VERIFIKA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+573001112233",
    "amount": 45000,
    "reference": "M1A2B3C4",
    "occurredAt": "2026-10-12T15:04:05-05:00",
    "customerName": "Juan Pérez",
    "bankLabel": "Nequi",
    "imageUrl": "https://tu-cdn.com/comprobante.jpg"
  }'

La respuesta es la de siempre —outcome (REGISTERED o DUPLICATE), business, income, attachment, y con duplicados duplicate, duplicateOf y message— más verificationId, con el que consultas la verificación y reconoces sus webhooks. Si integraste antes del 12 de octubre de 2026, tu código sigue funcionando igual.

201 · contrato de datos
{
  "outcome": "REGISTERED",
  "business": { "companyId": "cmt7xbfrg0001q3w1itst7uwo", "companyName": "Panadería Delipan" },
  "income": { "id": "cmtj9v2x1000abcw1qz9k2p0l", "status": "PENDING", "amount": 45000, "reference": "M1A2B3C4" },
  "attachment": { "stored": true },
  "verificationId": "vrf_8d2c4e6f0a1b3c5d7e9f1a2b3c4d5e6f",
  "livemode": true
}

Ningún endpoint marca un pago como verificado

Ni con la imagen ni con los datos: solo el cruce contra el aviso real del banco puede confirmarlo. Marcar como verificado algo que solo dijo un bot destruiría el único dato que hace útil a Verifika.

Si manejas muchos negocios

Una llave por negocio, hoy

Hoy cada negocio te da su propia llave, creada desde su panel. Si atiendes cuarenta negocios, guardas cuarenta llaves.

Estamos construyendo el modelo de partner: una sola credencial tuya que actúa sobre los negocios que te hayan dado permiso explícito, con una pantalla de aprobación que ellos firman. Si estás en ese caso, escríbenos a soporte@verifika.tech.

En esta página