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.
- Tu cliente → Tu bot: Manda la foto del comprobante
- Tu bot → Verifika: La imagen, tal cual (POST /v1/verifications)
- Verifika: Lee con IA y busca duplicados
- Verifika: Cruza con el aviso del banco
- Verifika → Tu bot: Veredicto + frase para el cliente (status · code · message)
- Tu bot → Tu cliente: Le reenvía el message
Antes de la primera llamada
- El plan incluye la API pública — ver Empezar, paso 1.
- 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.
- 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.
| Campo | Tipo | ¿Obligatorio? | Qué es |
|---|---|---|---|
imageUrl | string URL | Sí, si no mandas imageBase64 | La imagen del comprobante, en una URL https pública. |
imageBase64 | string | Sí, si no mandas imageUrl | La imagen en base64 (con o sin data:…;base64,). Hasta 15 MB ya decodificada. |
mimeType | string | No | Una pista. El tipo real lo deciden los bytes: JPG, PNG, WebP, HEIC o PDF. |
expectedAmount | number | No | Lo que esperabas cobrar. Si el comprobante dice otro valor, el veredicto es AMOUNT_MISMATCH: perfecto para un pedido con precio fijo. |
reporter | objeto | No | Quié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. |
metadata | objeto | No | Tus 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. |
companyId | string | Solo si tu llave entra a varios negocios | El negocio sobre el que verificas. |
reference, occurredAt, customerName, bankLabel, accountLabel | No | Pistas: 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
{
"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.
| HTTP | status | code | Qué pasó | Qué hace tu bot |
|---|---|---|---|---|
201 | VERIFIED | VERIFIED | El banco ya había avisado: el pago entró. | Entrega el pedido. |
201 | PENDING | AWAITING_BANK | Registrado; falta el aviso del banco. | Responde message y espera el webhook. Ver el caso completo. |
201 | PENDING | AWAITING_REVIEW | Registrado, pero el negocio no tiene banco conectado: lo confirma una persona. | Responde message. Si alguien lo aprueba, llega verification.verified. |
200 | DUPLICATE | DUPLICATE | Ese comprobante ya se usó (mismo archivo, misma referencia o la misma imagen). duplicate dice cuál. | No entregues. Responde message. |
200 | DUPLICATE | POSSIBLE_DUPLICATE | Mismo 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. |
200 | REJECTED | NOT_A_RECEIPT | La imagen no es un comprobante (en extracted.note, qué parece). | Pide la captura del comprobante. |
200 | REJECTED | UNREADABLE | Es un comprobante, pero no se lee el valor. | Pide una foto más clara. |
200 | REJECTED | UNSUPPORTED_FORMAT | Una foto HEIC que no se pudo abrir. | Pide una captura de pantalla o un JPG. |
200 | REJECTED | AMOUNT_MISMATCH | El 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ó).
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.
| HTTP | code | Qué pasó | retryable |
|---|---|---|---|
400 | INVALID_IMAGE_URL · BLOCKED_IMAGE_URL | La URL no es https, no es pública o apunta a una red interna. | No |
413 | IMAGE_TOO_LARGE | El comprobante pesa más de 15 MB. | No |
415 | IMAGE_TYPE_NOT_ALLOWED | El archivo no es JPG, PNG, WebP, HEIC ni PDF (se mira el archivo, no mimeType). | No |
422 | IMAGE_REQUIRED · INVALID_IMAGE_BASE64 | No llegó la imagen, o el base64 no es válido. | No |
403 | PLAN_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 |
502 | IMAGE_FETCH_FAILED | No pudimos bajar la imagen de tu URL. | Sí |
504 | IMAGE_FETCH_TIMEOUT | Tu URL tardó más de 8 s. | Sí |
503 | AI_UNAVAILABLE | La 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:
receiptsolo 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 serimage/pngse 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
GET /v1/verifications/{id}devuelve la verificación con su estado de ahora y el ingreso como está hoy. Es la red de seguridad si se te perdió un webhook.GET /v1/verifications?externalId=wa_573104567890trae todas las de un mismo chat o cliente. También filtra porstatus,code,fromyto.
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.
{
"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.