Saltar al contenido principal

Reintentos

La entrega es al menos una vez. Planificá para duplicados, no para exactamente-una-vez.

Cómo se produce una entrega

Cuando se alcanza un veredicto — por el pipeline o por un revisor humano en el panel — el evento se escribe en un outbox de entregas dentro de la misma transacción de base de datos que el veredicto mismo. Un servicio aparte, veridia-webhooks, lee ese outbox y hace las llamadas HTTP.

Esa estructura es la que te da la garantía que importa: no puede existir un veredicto sin su evento, ni un evento para un veredicto que se revirtió. También es el motivo por el que la request no llega desde adentro del proceso que calculó el veredicto — un despachador puede reintentar y hacer backoff; un manejador de request esperando a tu endpoint, no.

El esquema

Seis intentos. Uno inmediato, después cinco reintentos:

IntentoEspera previaTranscurrido desde el primer intento
10 s
21 s1 s
35 s6 s
430 s36 s
52 min156 s
610 min756 s

Ventana total: 756 segundos, unos 12,6 minutos. Ese es el tiempo que tu endpoint puede estar caído antes de que un evento deje de reintentarse por su cuenta. Dimensioná tus ventanas de mantenimiento contra esos 12,6 minutos, no contra la cantidad cruda de intentos.

Un intento se reintenta ante un 5xx, un timeout (10 s en total, 5 s para conectar), una falla de conexión, o un 408/429. Cualquier otro 4xx es un fallo permanente y corta el esquema de inmediato — un 401 o un 404 no se va a convertir en un 200 al cuarto intento, y reintentar solo demora que descubras la mala configuración.

Cada intento se firma de nuevo

El despachador recalcula el HMAC en cada intento, así que el t de Veridia-Signature es la hora de ese intento, no la del primero.

Esto responde la pregunta que el esquema de arriba naturalmente dispara: si el último reintento puede caer 12,6 minutos después de creado el evento, y la tolerancia de replay recomendada es de 300 segundos, ¿los reintentos tardíos se rechazan como replays?

No. El sexto intento llega con un timestamp de segundos de antigüedad. Dejá la tolerancia en 300 segundos. Ampliarla para cubrir la ventana de reintentos no te aporta nada y triplica el intervalo en el que una request capturada puede ser reproducida en tu contra.

Los duplicados son normales

El duplicado común no es una falla. Es un handler que procesó el evento correctamente y después tardó demasiado en responder — el trabajo está hecho, la confirmación se pasó del timeout, y el despachador, sin manera de distinguir eso de un endpoint muerto, lo intenta de nuevo.

Deduplicá por id:

const eventId = request.headers.get('Veridia-Event-Id'); // === payload.id
if (await alreadyProcessed(eventId)) return ok();

id es el valor evt_*. Es estable a lo largo de los seis intentos de un evento y distinto entre eventos — incluido entre dos eventos de la misma verificación, que es exactamente el caso que una clave basada en verificationId resuelve mal. Ver Idempotencia.

Registrá el id dentro de la misma transacción que aplica el efecto. Marcarlo como procesado antes del trabajo arriesga perder el evento; marcarlo después arriesga hacer el trabajo dos veces.

Después del último intento

La entrega se marca como failed y se detiene. No se descarta: queda en el outbox con su estado, el último código de estado HTTP y el último error, visible en el panel bajo Webhooks.

Un operador puede volver a encolar una entrega fallida desde ahí. Volver a encolar resetea el contador de intentos a cero, así que el evento recibe de nuevo la ventana completa de seis intentos. Solo se pueden volver a encolar entregas en estado failed — un evento que todavía está recorriendo su esquema no puede ser devuelto al inicio por un clic impaciente.

Un evento re-encolado lleva el mismo id. Si tu handler efectivamente lo procesó antes de fallar en confirmar, tu chequeo de deduplicación lo va a reconocer y descartar. Ese es el comportamiento buscado.

El panel también muestra el outbox en sí: una fila por evento con su estado actual, que es la vista que querés cuando la pregunta es "¿este veredicto llegó a mi cliente, y si no, por qué?".

La protección de URL

La URL del endpoint se valida dos veces, y ambos chequeos existen porque el cuerpo de un webhook lleva fieldsExtracted — nombre completo, número de documento, fecha de nacimiento.

Al guardar, el panel rechaza cualquier URL que no empiece con https://. No hay modo HTTP, no hay excepción para localhost, no hay relajación en modo test. Los datos de identidad no cruzan la red en texto plano.

Al enviar, la dirección resuelta se chequea contra una protección SSRF, que rechaza:

  • rangos privados (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  • loopback (127.0.0.0/8, ::1)
  • link-local, incluidos endpoints de metadata de nube (169.254.0.0/16)
  • NAT de operador (100.64.0.0/10)

La resolución se vuelve a chequear en cada envío y no una sola vez al guardar. Un hostname que resolvía a una dirección pública cuando lo configuraste puede resolver a 169.254.169.254 más tarde — DNS rebinding — y una protección que solo corriera al configurar no se daría cuenta.

Si te estás preguntando cómo desarrollar contra esto: usás un túnel. Ver pruebas locales.

Recuperar eventos que perdiste

Si tu endpoint estuvo caído más de 12,6 minutos y no volviste a encolar a tiempo, los veredictos siguen existiendo. Reconciliá con la API:

curl https://api.xxuxe.online/v1/verify/vf_AG07CDWRRFQV4T05ZXG2 \
-H "Authorization: Bearer qv_sec_YOUR_SECRET_KEY"

Esto requiere una clave secreta (qv_sec_ en live, qv_sect_ en test). Una clave publicable devuelve 401 secret_key_required.

Leer un veredicto de esta forma devuelve status y verdict como campos separados. status: "completed" significa que el pipeline terminó — no significa que la persona haya pasado. Ramificá sobre verdict.

Para un job de reconciliación necesitás la lista de IDs de verificación que te faltan, lo que implica registrar cada verificationId en el momento de /v1/verify/init, antes de que exista ningún webhook. Si hoy no estás guardando ese mapeo, ese es el hueco a cerrar primero: sin él no hay forma de enumerar qué te perdiste.

Qué sigue