Una validación toma un comprobante SPEI (datos canónicos o imagen) y le pregunta a Banxico si emitió un CEP. Hay dos pipelines y dos modos, pero el destino final es el mismo: una fila en validations con status terminal y, cuando aplica, el XML del CEP persistido para descarga.

Directo vs OCR

  • POST /v1/validaterunDirectPipeline. El cliente provee clave_rastreo + cuenta_beneficiaria + monto + fecha; no hay extracción.
  • POST /v1/validate-ocrrunOcrPipeline. El cliente sube una imagen (multipart) o un URL HTTPS (image_url); Gemini extrae los campos, NormalizationService los normaliza y resuelve enmascaramiento (ver Beneficiarios), y la llamada a Banxico vuelve al mismo path que el directo.

Ambos pipelines terminan en BanxicoService::validate POSTeando contra el formulario CEP de Banxico. La única diferencia operativa es que el OCR puede agregar warnings[] y consumir cuota separada cuando el plan distingue OCR.

Sync vs async

Por default ambos endpoints son síncronos: el HTTP queda abierto hasta que Banxico responde (la mediana es 1.5-3 s; el timeout de cliente recomendado es 30 s). Para offload, agregá ?async=1:

  1. El controller inserta status='queued', devuelve 202 con validation_id, un ETag débil, y un header Retry-After.
  2. El despacho viaja por Symfony Messenger al stream validations en Redis; el handler ValidationJobHandler::processQueued toma la fila y corre el mismo pipeline interno.
  3. El cliente sondea GET /v1/validations/{id} enviando If-None-Match con el último ETag visto. Mientras la fila no transicione, el servidor responde 304 Not Modified; cuando cambia de status el ETag cambia y el cuerpo trae el resultado.
  4. La cadencia de polling viene de config/queues.php (polling.initial_seconds + polling.backoff_after_attempts) y se expone en Retry-After + meta.next_poll_after_seconds.

Detalle completo en Validaciones async.

Los seis estados terminales

ValidationService::processQueued cierra cada fila en uno de seis valores (lista canónica en src/Services/ValidationService.php:771):

  • valid — Banxico devolvió el XML del CEP. Es el único veredicto que cuenta como "verificado" (ver Semántica del CEP y Finanzas).
  • not_found — Banxico no encontró el CEP con esos datos.
  • cep_unavailable — Banxico respondió pero el CEP no está disponible (típicamente todavía no liquidó).
  • invalid — Banxico contestó con una forma inesperada (datos mal formados o cualquier string futuro).
  • failed — excepción de aplicación capturada (ValidationException).
  • error — excepción de transporte (timeout/5xx).

Retry-eligible outcomes

Una fila terminal puede entrar a un ciclo de reintentos cuando su outcome es uno de not_found, cep_unavailable o error (RetryPolicy::ALLOWED_OUTCOMES en src/Validations/RetryPolicy.php:22). La política se setea con PUT /v1/validations/{id}/retry-policy — define enabled, max_retries, interval_seconds y los outcomes específicos. Los caps duros vienen de config/queues.php y de los límites del plan; el ciclo, el manejo de horarios y el comportamiento de cancelación están en Política de reintentos.

Idempotency-Key

El header Idempotency-Key es opcional y se valida en IdempotencyMiddleware. La clave se escopa por (user_id, endpoint, key):

  • Nueva clave → INSERT in_flight, se procesa la request, se persiste la respuesta para replays.
  • Mismo body con clave repetida → 200 cacheado (replay determinístico).
  • Body distinto con clave repetida422 idempotency_key_reused.
  • Misma clave todavía procesando409 idempotency_key_in_progress.

Ver Idempotencia para el TTL y la ventana de replay.