VerifikaDocs
Guías

Webhooks

Recibe cada pago verificado en tu propia URL, firmado y con reintentos.

En vez de preguntar cada minuto si entró un pago, nos das una URL y te avisamos cuando pasa.

1. Crea el endpoint

Desde el panel (Integraciones → API pública y webhooks → Webhooks) o por API:

curl -X POST https://api.verifika.tech/v1/webhook-endpoints \
  -H "Authorization: Bearer $VERIFIKA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://tusistema.com/verifika",
    "description": "Producción",
    "events": ["income.verified", "suspicious.created"]
  }'
Respuesta
{
  "object": "webhookEndpoint",
  "id": "cmtnoypsk00076kw1lggjtcjx",
  "url": "https://tusistema.com/verifika",
  "events": ["income.verified", "suspicious.created"],
  "status": "ENABLED",
  "secret": "whsec_f0CLZeUj..."
}

Qué manda cada campo

CampoTipo¿Obligatorio?Qué significa
urlstring URLA dónde mandamos los eventos. Tiene que ser https y resolver a una dirección pública: por http los datos de dinero de un negocio viajarían en claro.
eventsstring[]Qué quieres recibir. Claves del catálogo (GET /v1/webhook-events). Al menos una — un webhook sin eventos no recibiría nada, y esa es una trampa silenciosa.
descriptionstringNoPara ti: «producción», «staging», «el ERP del contador». Sale en la lista del panel para que sepas cuál es cuál.

Guarda el secreto

Con él verificas que cada entrega vino de nosotros. A diferencia de una API key, este se puede volver a consultar desde el panel: lo necesitamos nosotros para firmar, así que ya está guardado de forma reversible y ocultarlo no protegería nada.

La URL tiene que ser https y resolver a una dirección pública. Se valida al guardarla y otra vez en cada entrega — un dominio que hoy apunta a un servidor público puede apuntar mañana a una red interna, y no vamos a ser nosotros quienes toquen esa puerta.

2. Los eventos

Consulta el catálogo con GET /v1/webhook-events: devuelve todos, con allowed según tu plan.

EventoCuándo
income.createdSe registró un pago, venga de donde venga
income.verifiedEl banco confirmó el pago. El que casi todos quieren
income.status_changedCambió de estado. Trae previousStatus
income.updated · income.deletedCambió o se borró un pago declarado
expense.created · expense.updated · expense.deletedMovimientos de egreso
suspicious.createdComprobante repetido, o que el banco nunca respaldó
suspicious.resolvedAlguien lo resolvió desde el panel
connection.status_changedSe cayó (o volvió) un canal del banco
team.member_added · team.member_removedCambios de equipo
plan.limit_reachedSe agotó un límite del plan
webhook.testLo dispara el botón «Probar»

income.verified e income.status_changed llegan los dos

Cuando un pago pasa a verificado emitimos ambos. Suscribe solo income.verified si eso es lo que te importa: te ahorras filtrar dentro de tu handler y no te llega ruido por cada duplicado y cada no-encontrado del negocio.

3. La entrega

POST https://tusistema.com/verifika
Content-Type: application/json
Verifika-Event-Id: evt_cms9qvef700013jw1qawn586g
Verifika-Event-Type: income.verified
Verifika-Delivery-Id: dlv_cms9qvef700013jw1qawn586h
Verifika-Attempt: 1
Verifika-Signature: t=1789012345,v1=5257a869e7...
{
  "id": "evt_cms9qvef700013jw1qawn586g",
  "type": "income.verified",
  "created": "2026-09-04T15:04:05.000Z",
  "apiVersion": "v1",
  "companyId": "cmt7xbfrg0001q3w1itst7uwo",
  "data": {
    "object": {
      "object": "income",
      "id": "cmtmf1k8m006faqw1lh27ozcr",
      "amount": 45000,
      "currency": "COP",
      "status": "VERIFIED",
      "previousStatus": "PENDING",
      "reference": "M1A2B3C4",
      "verifiedAt": "2026-09-04T15:04:05.000Z"
    }
  }
}

data.object es exactamente el mismo objeto que devuelve el GET de ese recurso. No hay dos formas del mismo dato que aprender.

4. Verifica la firma

HMAC-SHA256 sobre ${timestamp}.${cuerpo} con tu secreto.

Sobre el cuerpo CRUDO

Calcula el HMAC sobre los bytes tal como llegaron, antes de parsear el JSON. Si lo haces sobre el objeto re-serializado, un espacio de diferencia rompe la firma. En Express: express.raw({ type: 'application/json' }) en esa ruta.

import crypto from 'node:crypto';

export function verificarFirma(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) return false;

  // Rechaza eventos viejos: sin esto, quien capture una entrega puede
  // reenviártela mañana y tu sistema la aceptaría como buena.
  const edad = Math.abs(Date.now() / 1000 - Number(t));
  if (!Number.isFinite(edad) || edad > toleranciaSeg) return false;

  const esperada = crypto.createHmac('sha256', secreto).update(`${t}.${cuerpoCrudo}`).digest('hex');
  const buf = Buffer.from(esperada, 'hex');

  // Puede venir más de una v1 durante una rotación: basta con que una cuadre.
  return partes
    .filter((p) => p.startsWith('v1='))
    .some((p) => {
      const got = Buffer.from(p.slice(3), 'hex');
      // Comparación en tiempo constante: un `===` filtra información por lo que tarda.
      return got.length === buf.length && crypto.timingSafeEqual(got, buf);
    });
}

5. Reintentos

Éxito es cualquier 2xx en menos de 5 segundos. Si no, reintentamos con espera creciente:

1 min → 5 min → 30 min → 2 h → 6 h → 12 h → 24 h

Ocho intentos en unas 48 horas. Después la entrega queda FAILED y puedes reenviarla desde el panel o con POST /v1/webhook-deliveries/{id}/retry.

Si tu endpoint acumula 20 fallos seguidos, lo desactivamos (status: DISABLED_AUTO) y se lo avisamos al dueño de la cuenta. Se reactiva con PATCH { "enabled": true }.

Responde rápido y procesa después

Encola el trabajo y devuelve 200 de inmediato. Si procesas dentro del handler y tardas 6 segundos, para nosotros es un fallo y vas a recibir el mismo evento otra vez.

6. Dos reglas que evitan el 90 % de los bugs

El orden no está garantizado

Puedes recibir income.verified antes que income.created. Ordena por el campo created, nunca por el orden de llegada.

Puedes recibir el mismo evento dos veces

Un reintento nuestro tras un timeout de tu lado entrega algo que ya procesaste. Guarda los id de evento que ya viste y descarta los repetidos.

7. Rotar el secreto

curl -X POST https://api.verifika.tech/v1/webhook-endpoints/{id}/rotate-secret \
  -H "Authorization: Bearer vk_live_..."

Durante 24 horas firmamos cada entrega con los dos secretos (verás dos v1= en la cabecera), así despliegas el nuevo cuando quieras sin perder un solo evento.

8. Depurar

GET /v1/webhook-deliveries te dice qué mandamos, qué respondió tu servidor y en qué intento:

{
  "object": "webhookDelivery",
  "status": "PENDING",
  "attempt": 1,
  "responseStatus": 405,
  "responseSnippet": "<!doctype html><html lang=\"en\"><head><title>...",
  "durationMs": 563,
  "error": "El endpoint respondió 405.",
  "nextRetryAt": "2026-09-05T01:17:58.472Z"
}

Ese responseSnippet es tu propia respuesta: casi siempre dice exactamente qué salió mal sin que tengas que ir a mirar tus logs.

En esta página