POSThttps://api.veriko.mx/v1/validate-ocr

Validate a SPEI transfer (OCR)

Audience
public
Auth
API key
Permission
validations_ocr:create
How-to guide →

Validates a SPEI transfer from its receipt, as an image or a PDF. It extracts the data from the receipt through optical character recognition (OCR) and validates it against Banxico to obtain the transaction's CEP.

The receipt is sent as image or as image_url. When sent as a URL, the target host must serve it over a secure protocol (SSL/HTTPS).

The final verdict is the same as in manual validation:

  • valid: The transaction's CEP was found and validated.
  • not_found: The transaction's CEP was NOT found.
  • cep_unavailable: Banxico recognized the transaction but the CEP is not available for now.
  • returned: the transaction was settled and the beneficiary institution later sent it back. The CEP, when there is one, is still delivered.
  • error: Internal service error.
Parameters
ParameterInTypeRequiredDescription
asyncquerystringoptional

When 1/true/yes, the validation is queued and the server responds 202 immediately with a validation_id; the client polls GET /v1/validations/{id} until the terminal state. Without the flag or with 0, the response is synchronous and returns the final result in the same POST.

Default: 0

Idempotency-Keyheaderstringoptional

Optional client-generated key (Stripe-style) that guarantees the request is processed exactly once within a 24-hour TTL. The scope is (user_id, endpoint, key). Retries with the same key and the same body return the byte-for-byte cached response with the Idempotent-Replayed: true header, without consuming rate-limit quota, without re-firing webhooks, and without creating a new validations row. Same key with a different body → 422 idempotency_key_reused. Same key with an in-flight request → 409 idempotency_key_in_progress. 5xx responses are not cached (retries with the same key are processed for real). Format: 1–255 characters, alphanumeric + _ + -.

e.g. 11111111-2222-3333-4444-555555555555
Parameters
ParameterTypeRequiredDescription
imagestring (byte)Required unless image_url is sent

Receipt encoded in base64, as an image or a PDF. If image_url is also provided, only image_url is considered. Accepted formats: JPEG, PNG, WebP, or a PDF of 1 to 3 pages. Maximum size: 12 MB. Maximum dimensions of an image: 12000px per side. Accepts the data:image/...;base64, or data:application/pdf;base64, prefix.

e.g. iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==
image_urlstring (uri)Required unless image is sent

Public HTTPS URL of the receipt. The file is fetched when the request arrives and follows the same format and size limits as image: JPEG, PNG, WebP, or a PDF of 1 to 3 pages, up to 12 MB. An http URL returns HTTP status 422 (with invalid_url_scheme in the body), and a redirect to http is not followed.

e.g. https://storage.example.com/receipts/comprobante-2025-03.jpg
cuenta_beneficiariastring (pattern)optional

Receiving account for the transfer (useful when it is missing or incomplete in the image). Required for DiMo phone; optional if the image shows the full account or its last digits, which are completed with the saved beneficiaries (they do not replace this field). Accepts CLABE (18 digits), card (16 digits) or DiMo phone (10 digits). It cannot be sent together with cuentas_candidatas: sending both responds with an HTTP status 422 (with cuenta_y_candidatas_excluyentes in the body).

e.g. 012180004412345678
cuentas_candidatasarray<string> (items: 2–10, unique items)optional

Accounts among which the receiving one is, for when the image does not show it and it is not known which. It replaces cuenta_beneficiaria: sending both responds with an HTTP status 422 (with cuenta_y_candidatas_excluyentes in the body). It takes priority over the account read from the image. It accepts from 2 up to the platform's current maximum, 3 by default and never more than 10; the 422 for an excess reports the maximum in meta.max. Each account is a CLABE (18 digits), a card (16) or a DiMo phone (10), with a valid check digit and no repeats. A list that does not comply responds with a 422 with cuentas_candidatas_invalidas, and points out in field_errors the position that failed. Both errors happen before any quota is consumed. It consumes a single quota unit, however many accounts it carries, also with ?async=1. The query goes through the candidates in the order sent and adopts the first one that matches the transfer. The winner is published in full in normalized_data.cuenta_beneficiaria, and candidate_match reports its position. If none matches, the response is an HTTP status 422 with the reason in code (cuenta_unresolvable_after_probes when there is no more precise diagnosis).

e.g. ["012180004412345678","002010077777777771"]
banco_emisorstringoptional

SPEI code of the sending bank (4 or 5 digits, e.g. 40012), to correct the one read from the receipt. It replaces the one read before querying Banxico. A value that is not 4 or 5 digits (a name, for example) is ignored and the one read is kept.

e.g. 40012
banco_receptorstringoptional

SPEI code of the receiving bank (4 or 5 digits, e.g. 40002), to correct the one read from the receipt. It replaces the one read before querying Banxico. A value that is not 4 or 5 digits (a name, for example) is ignored and the one read is kept.

e.g. 40002
retry_policyobjectoptional

Automatic retry policy for this validation. If omitted, the policy configured at PUT /v1/users/me/retry-policy is used. Retries do not consume validation quota.

enabledbooleanoptional

true when the retry cycle is active. With false, the other fields are ignored.

e.g. true
max_retriesinteger (≥ 1)optional

Maximum number of automatic retries. Once reached, the cycle enters the exhausted terminal state. The allowed limit depends on the plan.

e.g. 3
interval_secondsinteger (≥ 300, ≤ 86400)optional

Seconds to wait between each retry (300–86400, i.e., 5 minutes to 24 hours).

e.g. 600
outcomesarray<string> (items: 1–?, unique items)optional

Outcomes that trigger a retry. By default, they are not_found, cep_unavailable, and error; the combination ["not_found", "cep_unavailable"] excludes error.

e.g. ["not_found","cep_unavailable","error"]
client_refstring (pattern, 1–64)optional

Reference of your own that is returned as sent in the validation, in the validation webhooks, and as an exact filter of GET /v1/validations. Text of 1 to 64 characters, without line breaks or emoji; it is trimmed before being stored. A value that does not comply is rejected with an HTTP status 422 (with invalid_client_ref in the body). It must not contain personal data.

e.g. orden-4812
retain_imagebooleanoptional

true when the platform keeps the receipt file after validating it, which is the usual case. With false, the file is deleted as soon as the validation reaches a terminal status in which it is no longer needed; the verdict and the extracted data are kept. With false, the validation publishes image_retained=false and GET /v1/validations/{id}/image responds with an HTTP status 410 (with image_not_retained in the body). It applies the same with ?async=1. The file exists while the validation is being processed. If it came in as a CEP in PDF and has automatic retries active, the deletion waits for that cycle to close, because each retry compares the PDF's seal again. A value that is not a boolean responds with an HTTP status 422 (with invalid_retain_image in the body), before any quota is consumed.

Default: true

e.g. false
Request
curl -X POST 'https://api.veriko.mx/v1/validate-ocr' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Content-Type: application/json' \
  -d '{
    "image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  }'

Python example — coming soon.

JavaScript example — coming soon.

PHP example — coming soon.

Retry policy

POST /v1/validate-ocr

Validation retry policy: activation, limit, interval, and eligible outcomes.

Attempts
—
Interval
5 min – 1 h × 24
Eligible outcomes
not_found, cep_unavailable, error

This operation accepts an optional retry policy.

Response 2xxValidation
FieldTypeDescription
type*string

JSON:API resource type. Always validation.

e.g. validation
id*string (uuid)

Unique identifier of the validation (UUID v4).

e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6
attributes*object

Canonical validation data.

validation_typestring

Validation mode — direct: Manual entry of the fields; ocr: Reading of the receipt image.

e.g. direct
is_playgroundboolean

true when the result comes from a playground simulation. The simulation runs without Banxico calls, quota consumption, persistence, webhooks, or notifications.

e.g. false
statusstring

Lifecycle state — queued: Awaiting processing; processing: In progress; valid: The CEP was found and matches; not_found: Banxico cannot find the operation; cep_unavailable: Banxico recognizes the transaction but the CEP is not available; invalid: Banxico did not return a recognizable verdict; returned: the transfer was settled and later returned; failed: The validation stopped because of a problem in the data sent; error: The validation stopped because of a service failure.

e.g. valid
banxico_statusstring | nullnullable

Verdict reported by Banxico. It can be valid, not_found, cep_unavailable, invalid, returned, or error. It remains pending while no Banxico verdict exists. returned means the transfer settled and the beneficiary institution later sent it back — the CEP, when there is one, is still delivered. A valid is not final: it is the verdict of the moment of the query, and it moves to returned when Banxico reports the return in a follow-up check, which is requested with POST /v1/validations/{id}/recheck or which the service runs by itself during the first 72 hours (the validation.returned webhook is emitted). It becomes error only when the request reached Banxico and the service failed; error_code identifies the cause.

e.g. valid
processing_time_msinteger | nullnullable

Milliseconds spent processing the validation.

e.g. 1320
request_dataobject

Copy of the original request fields. fecha keeps the sending date exactly as sent, even when Banxico found the payment with another one (banxico_result._fecha).

client_refstring

Reference of your own sent in the request (client_ref), returned as sent. It only appears when the request included it. It lets you tie the validation to an order of your own, and GET /v1/validations accepts it as an exact filter. It must not contain personal data.

e.g. orden-4812
created_atstring (date-time)

Validation creation timestamp in ISO 8601 UTC.

e.g. 2025-03-15T14:22:10Z
completed_atstring | nullnullable

Date and time of completion, in ISO 8601 UTC. null while it has not finished.

e.g. 2025-03-15T14:22:11Z
enqueued_atstring | nullnullable

Queue-entry timestamp in ISO 8601 UTC.

e.g. 2026-04-30T10:15:00Z
processing_started_atstring | nullnullable

Processing start timestamp in ISO 8601 UTC.

e.g. 2026-04-30T10:15:00Z
expires_atstring | nullnullable

Expiration timestamp for queued validations in ISO 8601 UTC. After this, the validation moves to error with error_code ttl_expired.

e.g. 2026-04-30T10:15:00Z
etag_versioninteger | nullnullable

Incremental version used with If-None-Match in conditional requests.

e.g. 1
image_pathstring | nullnullable

Relative reference to the receipt, image or PDF, only when validation_type is ocr. It is not published when the file is not kept: see image_retained.

e.g. comprobantes/3fa85f64-5717-4562-b3fc-2c963f66afa6.jpg
image_retainedboolean

true when the platform keeps the receipt file; false when it does not, because the request sent retain_image=false or because the validation was purged. It only appears on validations of type ocr. With false, GET /v1/validations/{id}/image responds with an HTTP status 410 (with image_not_retained or validation_purged in the body).

e.g. true
purged_atstring (date-time)

Date and time, in ISO 8601 UTC, when the validation was purged with POST /v1/validations/{id}/purge/execute. It only appears on a purged validation, which is left as a tombstone. A tombstone keeps id, validation_type, status, banxico_status, the dates, processing_time_ms, error_code, and the amount in normalized_data.monto. It does not keep the file, the CEP, request_data, ocr_result, or banxico_result, and its derived resources respond with an HTTP status 410 (with validation_purged in the body).

e.g. 2026-10-02T09:30:00Z
ocr_resultobject | nullnullable

Fields extracted from the receipt, only when validation_type is ocr. extraction_source is cep_pdf when the receipt was the Banxico CEP as a PDF and its data was read from its text, without OCR. comprobantes_detectados is the number of receipts found in a PDF.

ocr_confidencenumber | nullnullable

OCR score from 0 to 1, only when validation_type is ocr.

e.g. 0.94
normalized_dataobject | nullnullable

Fields normalized after OCR (when validation_type is ocr), or the winning account in cuenta_beneficiaria when a direct validation sent cuentas_candidatas. A tombstone keeps only the amount here.

normalization_warningsarray | nullnullable

Normalization warnings present. They can explain an unexpected not_found result.

duplicate_ofobject

Earlier validation that had already confirmed this same transfer in the same account. It only appears when a valid, non-withdrawn validation with the same tracking key exists; it never points to a validation of another account. It is the structured form of the warning that is also added to normalization_warnings. It does not change the verdict: the validation is queried and charged all the same.

id*string (uuid)

Identifier of the earlier validation.

e.g. 3fa85f64-5717-4562-b3fc-2c963f66afa6
created_at*string (date-time)

Creation date and time of the earlier validation, in ISO 8601 UTC.

e.g. 2026-10-01T15:04:05Z
account_conflictobject

Conflict between the account sent and the one shown in the image. It only appears on ocr validations, when cuenta_beneficiaria was sent and the image shows another full account of the same length, valid and with enough confidence, or a masked account whose visible trailing digits (3 or more) are not the end of the account sent. The account sent prevails and is the one queried at Banxico: the field does not change the verdict. If the payment went to the account in the image, a not_found is the expected outcome. It carries only the last 4 digits of each account, and the conflict is also added to normalization_warnings. With a masked account, read_last4 carries the trailing digits the receipt itself shows (3 or 4), never more.

sent_last4*string

Last 4 digits of the account sent in cuenta_beneficiaria.

e.g. 5678
read_last4*string

Last 4 digits of the account shown in the image (the visible ones, 3 or 4, if it is masked).

e.g. 9012
candidate_matchobject

Which of the cuentas_candidatas sent won. It only appears when the request carried that list and one of the accounts matched the transfer. The full account is in normalized_data.cuenta_beneficiaria; this object says which position of the list it is, so it can be correlated with the integrator's own entity without comparing accounts. It does not change the verdict.

index*integer

Position (from 0) of the winning account in the cuentas_candidatas array, as sent.

e.g. 1
account_last4*string

Last 4 digits of the winning account.

e.g. 7771
is_maskedboolean | nullnullable

Whether the beneficiary account is masked in the receipt, be it a CLABE, a card or a phone number. It is included only when it is true, and only when validation_type is ocr.

banxico_resultobject | nullnullable

Data returned by the Banxico CEP. The typed properties below are the 20 declared by the official SPEI complement schema. _ocr_correction records corrected fields, their original reading (requested), the queried value (used) and confirmation (confirmed_by). status_query indicates a hypothesis confirmed by the payment status query; cep indicates that the receipt was also obtained. A status query alone does not verify a transfer. _cep_upload appears when the uploaded receipt was the CEP as a PDF and Banxico confirmed the payment. authenticated is true when its seal and original string match those of the official CEP; with false, differing_fields lists the fields that differ and normalization_warnings reports it. The verdict is still Banxico's. _payment_status is the official payment status, as Banxico gave it the last time it was asked: code (liquidado, en_proceso, cancelado, rechazado, en_proceso_devolucion, devuelto, or desconocido), label, settled, reversed, and checked_at (ISO 8601 UTC). Its absence means it could not be determined, never that the payment is settled. It is updated on every follow-up check, and with devuelto or en_proceso_devolucion the validation moves to returned. _fecha appears when Banxico did not find the payment with the date sent and did find it with another. used is the date it was found with, requested is the date first queried, and reason is why used was tried. Values of reason: clave_embedded_date: The tracking key embeds another date, at most one SPEI business day from the one sent; cutoff_18h: The key embeds no date and the receipt shows a time close to the 18:00 cutoff, so the next day was tried; dia_operacion: The key embeds the date sent, because the receipt carries the operation date, and an earlier day was tried back to the previous business day. The verdict is still Banxico's, and request_data.fecha keeps the date sent.

operationDatestring

SPEI business day that Banxico assigned to the operation, as reported by the CEP (YYYY-MM-DD), in Mexico central time. It can differ from the sending date: what is sent from 18:00 on, on a Saturday, on a Sunday, or on a non-business day operates on the next business day.

e.g. 2025-03-15
processingTimestring

Operation time as reported by the CEP (HH:MM:SS), in Mexico central time, not UTC.

e.g. 14:22:10
speiKeystring

Operation key within the SPEI system (the CEP's ClaveSPEI). Distinct from the tracking key.

trackingKeystring

SPEI tracking key of the operation — the same one sent in the validation request.

e.g. MBAN01002503151422ABCDEF
digitalSignaturestring

Digital seal of the CEP (sello) — the signature Banxico computes over the operation.

certificateNumberstring

Serial number of the digital certificate Banxico used to seal the CEP.

cadenaCdastring

Original data string of the operation (cadenaCDA): the 47 fields the institution sent to Banxico, pipe-separated, including both dates (operation and capture) and the payment type. Lets the CEP be audited without requesting it again.

senderBankstring

Sending bank of the operation, as identified by the CEP.

e.g. BBVA
senderNamestring

Name of the operation's sender, as reported by the CEP.

senderAccountTypestring

Sender's account type (e.g. CLABE or card), as reported by the CEP.

senderAccountstring

Sender's account number, as reported by the CEP — unmasked in this resource (a private lookup, owned by the validation's account).

senderRfcstring

Sender's RFC (Mexican tax ID), as reported by the CEP.

receiverBankstring

Receiving bank of the operation, as identified by the CEP.

e.g. STP
beneficiaryNamestring

Name of the operation's beneficiary, as reported by the CEP.

beneficiaryAccountTypestring

Beneficiary's account type (e.g. CLABE or card), as reported by the CEP.

beneficiaryAccountstring

Beneficiary's account number, as reported by the CEP. Unmasked in this resource (a private lookup, owned by the validation's account). Outgoing webhooks DO mask it to the last 4 digits in banxico_confirmed.beneficiaryAccount (see the webhooks guide).

beneficiaryRfcstring

Beneficiary's RFC (Mexican tax ID), as reported by the CEP.

amountnumber (float)

Operation amount Banxico confirmed via the CEP, in pesos.

e.g. 1500
ivanumber (float)

VAT (IVA) of the operation reported by the CEP, in pesos.

paymentConceptstring

Payment concept of the operation, as reported by the CEP.

error_messagestring | nullnullable

Human-readable error message when terminal.

e.g. Banxico no respondió tras tres intentos.
error_codeValidationErrorCode | null

Stable code of the terminal error, when there is one. After not_found, diagnosis may indicate not_found_identity (date, criterion or banks), not_found_account (account), not_found_amount (amount), not_found_account_and_amount (both) or not_found_account_or_amount (payment found, field not isolated). A diagnosis does not change the verdict or prove payment. Without a conclusive response, the field remains null.

batch_idinteger | nullnullable

Bulk import job identifier, when applicable.

e.g. 318
batch_positioninteger | nullnullable

Position within the job, starting at 1.

e.g. 12
retry_stateobject

Full retry cycle state. Always present; if retries are not active, enabled is false and policy fields are null. Bulk import rows always have enabled equal to false.

enabledboolean

true when the validation has retries configured, even if its cycle has already ended (terminal_state).

e.g. true
max_retriesinteger | nullnullable

Configured maximum number of retries; null when enabled is false.

e.g. 3
interval_secondsinteger | nullnullable

Interval between retries in seconds, from 300 to 86400; null when enabled is false.

e.g. 600
outcomesarray | nullnullable

Outcomes that trigger a retry: not_found, cep_unavailable, and error; null when enabled is false.

e.g. ["not_found","cep_unavailable","error"]
attempts_completedinteger

Number of retries completed so far.

e.g. 1
next_attempt_atTimestampUTC | null

Date of the next retry; null when none is scheduled.

resolved_atTimestampUTC | null

Date when a retry produced valid; null until that occurs.

exhausted_atTimestampUTC | null

Date when attempts were exhausted; null until that occurs.

cancelled_atTimestampUTC | null

Cycle cancellation date; null until that occurs.

terminal_statestring | nullnullable

Cycle state: pending, resolved, exhausted, or cancelled. It is null when the validation has no retry cycle.

e.g. pending
linksobject

Related links (JSON:API links).

selfstring

Canonical link to the validation details.

e.g. /v1/validations/3fa85f64-5717-4562-b3fc-2c963f66afa6
cep_xmlstring | nullnullable

URL of the CEP in XML format. It only appears when banxico_status is valid, or is returned with a receipt, and the validation was not purged; otherwise the links object is omitted.

e.g. /v1/validations/a1b2c3d4-e5f6-4890-abcd-ef0123456789/cep?format=xml
cep_pdfstring | nullnullable

URL of the CEP in PDF format. It only appears when banxico_status is valid, or is returned with a receipt, and the validation was not purged; otherwise the links object is omitted.

e.g. /v1/validations/a1b2c3d4-e5f6-4890-abcd-ef0123456789/cep?format=pdf
Response status codesPOST /v1/validate-ocr
StatusClassDescriptionBody
2002xxResolved validation with the data extracted from the receipt and its verdict.Validation
2022xxValidation enqueued. Poll GET /v1/validations/{id} until one of the terminal statuses (valid, not_found, cep_unavailable, invalid, returned, failed, error). meta.next_poll_after_seconds hints the recommended first-poll interval. ValidationQueued
4004xxThe Idempotency-Key header value does not meet the allowed format (alphanumeric + _ + -, 1–255 characters). ErrorResponse
4014xxAuthentication is required or the provided credentials are invalid.ErrorResponse
4094xxA request with the same Idempotency-Key is still being processed. The client must retry after the number of seconds indicated by the Retry-After header. In-flight rows older than idempotency.in_flight_timeout_seconds (300 by default) are automatically treated as zombies and cleaned up on the next attempt. ErrorResponse
4134xxThe request body exceeds the maximum accepted size (body_too_large).ErrorResponse
4224xxThe image or request data is invalid. Possible codes: image_or_image_url_required, invalid_image, invalid_image_format, image_too_large, invalid_url, invalid_url_scheme, url_ssrf_blocked, invalid_clabe_checksum, image_too_small, image_mime_mismatch, image_dimensions_too_large, image_decompression_bomb, image_polyglot_detected, image_url_unreachable, image_url_too_large, image_url_too_many_redirects. With a PDF: pdf_invalid_structure, pdf_encrypted, pdf_active_content, pdf_hidden_content, pdf_modified_after_issue, pdf_too_many_pages, pdf_unsupported_image, pdf_text_layer_too_large, and pdf_multiple_receipts. With a retain_image that is not a boolean: invalid_retain_image. With cuentas_candidatas: cuenta_y_candidatas_excluyentes, cuentas_candidatas_invalidas and, when no candidate matches, cuenta_unresolvable_after_probes. In asynchronous mode, a generic image failure is reported as image_invalid. Reusing Idempotency-Key with a different body produces idempotency_key_reused.ErrorResponse
4294xxRate limit exceededErrorResponse
5035xxOCR is unavailable, the Banxico request could not be completed (banxico_rate_limit_exhausted), or the asynchronous validation could not be started (dispatch_failed).ErrorResponse
Response headers200
HeaderTypeDescription
Idempotent-ReplayedstringPresent only when the client sent Idempotency-Key. false for fresh responses; true when replayed from cache.
Errors from POST /v1/validate-ocr
StatusCodeExample
400invalid_idempotency_key

Invalid Idempotency-Key format. Allowed: A-Z a-z 0-9 _ - (1-255 chars).

Envelope
meta.request_id
3c4d5e6f7a8bb8a7f6e5d4c3
401unauthorized

Invalid or missing authentication credentials.

Envelope
meta.request_id
c4d5e6f7a8b99b8a7f6e5d4c
409idempotency_key_in_progress

A request with this Idempotency-Key is still being processed.

Envelope
meta.request_id
2b3c4d5e6f7aa7f6e5d4c3b2
Response headers
  • Retry-After: integer — Seconds to wait before retrying
413body_too_large

The request body is too large.

Envelope
meta.request_id
1a2b3c4d5e6ff6e5d4c3b2a1
429rate_limit_exceeded

Rate limit exceeded. Try again in 45 seconds.

Envelope
meta.request_id
f7a8b9c0d1e22e1d0c9b8a7f
Response headers
  • Retry-After: integer — Seconds to wait before retrying. Matches the endpoint's rate-limit window (typically 60s for list endpoints, 1-5s for in-flight idempotent operations).
  • X-RateLimit-Limit: integer — Configured request cap for this bucket. It is emitted on every 429 and, on the authenticated API, also on successful responses, where it is the per-key cap of the account plan.
  • X-RateLimit-Remaining: integer — Requests remaining in the current window. Always 0 at the moment of the 429; on a successful response of the authenticated API it is what is left of the per-key cap.
  • X-RateLimit-Reset: integer — Absolute Unix epoch (seconds) when the window resets. Emitted on every 429, alongside Retry-After, and on successful responses of the authenticated API. Per-endpoint overrides exist (e.g. `rate_limited_login`).