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/validate→runDirectPipeline. El cliente proveeclave_rastreo+cuenta_beneficiaria+monto+fecha; no hay extracción.POST /v1/validate-ocr→runOcrPipeline. El cliente sube una imagen (multipart) o un URL HTTPS (image_url); Gemini extrae los campos,NormalizationServicelos 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:
- El controller inserta
status='queued', devuelve202convalidation_id, unETagdébil, y un headerRetry-After. - El despacho viaja por Symfony Messenger al stream
validationsen Redis; el handlerValidationJobHandler::processQueuedtoma la fila y corre el mismo pipeline interno. - El cliente sondea
GET /v1/validations/{id}enviandoIf-None-Matchcon el último ETag visto. Mientras la fila no transicione, el servidor responde304 Not Modified; cuando cambia de status el ETag cambia y el cuerpo trae el resultado. - La cadencia de polling viene de
config/queues.php(polling.initial_seconds+polling.backoff_after_attempts) y se expone enRetry-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 repetida →
422 idempotency_key_reused. - Misma clave todavía procesando →
409 idempotency_key_in_progress.
Ver Idempotencia para el TTL y la ventana de replay.