A validation takes a SPEI receipt (canonical fields or an image) and asks Banxico whether it issued a CEP. There are two pipelines and two modes, but the destination is the same: a row in validations with a terminal status and, when applicable, the CEP XML persisted for download.
Direct vs OCR
POST /v1/validate→runDirectPipeline. The client providesclave_rastreo+cuenta_beneficiaria+monto+fecha; no extraction step.POST /v1/validate-ocr→runOcrPipeline. The client uploads an image (multipart) or an HTTPS URL (image_url); Gemini extracts the fields,NormalizationServicenormalizes and resolves masking (see Beneficiaries), and the Banxico call rejoins the same path as the direct flow.
Both pipelines bottom out in BanxicoService::validate POSTing against the Banxico CEP form. The only operational difference is that OCR may attach warnings[] and may consume a separate quota when the plan distinguishes OCR.
Sync vs async
By default both endpoints are synchronous: the HTTP request stays open until Banxico replies (median 1.5-3 s; the recommended client timeout is 30 s). To offload, add ?async=1:
- The controller inserts
status='queued', returns202withvalidation_id, a weakETag, and aRetry-Afterheader. - Dispatch travels via Symfony Messenger to the Redis
validationsstream; the handlerValidationJobHandler::processQueuedclaims the row and runs the same internal pipeline. - The client polls
GET /v1/validations/{id}sendingIf-None-Matchwith the last ETag it saw. Until the row transitions, the server replies304 Not Modified; when status changes the ETag changes and the body carries the result. - Polling cadence is driven by
config/queues.php(polling.initial_seconds+polling.backoff_after_attempts) and is mirrored inRetry-After+meta.next_poll_after_seconds.
Full detail in Async validations.
The six terminal states
ValidationService::processQueued closes each row in one of six values (canonical list at src/Services/ValidationService.php:771):
valid— Banxico returned the CEP XML. The only verdict that counts as "verified" (see CEP semantics and Finance).not_found— Banxico did not find a CEP for the submitted fields.cep_unavailable— Banxico replied but the CEP is not available yet (typically not settled).invalid— Banxico answered with an unexpected shape (malformed payload or any future string).failed— application-level exception caught (ValidationException).error— transport-level exception (timeout/5xx).
Retry-eligible outcomes
A terminal row may enter a retry cycle when its outcome is one of not_found, cep_unavailable, or error (RetryPolicy::ALLOWED_OUTCOMES in src/Validations/RetryPolicy.php:22). Policy is configured via PUT /v1/validations/{id}/retry-policy — set enabled, max_retries, interval_seconds, and the eligible outcomes. Hard caps come from config/queues.php and from plan limits; the cycle, scheduling, and cancellation behavior live in Retry policy.
Idempotency-Key
The Idempotency-Key header is optional and is checked by IdempotencyMiddleware. The key is scoped by (user_id, endpoint, key):
- New key → INSERT
in_flight, request is processed, response is persisted for replays. - Same body, same key → cached 200 (deterministic replay).
- Different body, same key →
422 idempotency_key_reused. - Same key still processing →
409 idempotency_key_in_progress.
See Idempotency for the TTL and replay window.