Validate a SPEI transfer (direct)

Goal

Submit the fields of a SPEI transfer to Banxico and receive a readable verdict (valid, not_found, cep_unavailable, returned, or error). When the transfer is confirmed you can download the Electronic Payment Receipt (CEP) certificate as PDF or XML.

Prerequisites

  • An active API key (veriko_…). If you don't have one, generate it from the Veriko console, in the API section, and open the API Key panel.
  • The transfer details:
    • sending date (fecha): the day the transfer was made, not the operation day the CEP prints. See dates and time zone.
    • amount (monto).
    • tracking key (clave_rastreo) or numeric reference (referencia_numerica) — at least one.
    • issuing bank (emisor).
    • receiving bank (receptor) — both accept either the bank code or the bank name; you can find them in the bank list.
    • beneficiary account (cuenta_beneficiaria — CLABE, card, or mobile number), or a list of candidate accounts (cuentas_candidatas) when you do not know which of several received the payment: see step 7.
    • «Pago a Banco» (receptor_participante, optional): 0 for a transfer to an account holder (the default) or 1 when the beneficiary is the receiving institution itself —credit card, loan, or services paid to the institution.

Steps

1. Submit the validation

Send the transfer fields to POST /v1/validate.
Provide clave_rastreo, referencia_numerica, or both — more identifiers increase precision.

curl -X POST 'https://api.veriko.mx/v1/validate' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Content-Type: application/json' \
  -d '{
    "fecha": "2025-03-15",
    "monto": 15000.50,
    "clave_rastreo": "MXBA20250315001234",
    "emisor": "40014",
    "receptor": "40012",
    "cuenta_beneficiaria": "012180004412345678"
  }'

Synchronous response (200) — the CEP query completed immediately:

{
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "type": "validation",
    "attributes": {
      "status": "valid",
      "processing_time_ms": 1320,
      "created_at": "2025-03-15T14:22:10Z",
      "completed_at": "2025-03-15T14:22:11Z"
    },
    "links": {
      "self": "/v1/validations/f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "cep_xml": "/v1/validations/f47ac10b-58cc-4372-a567-0e02b2c3d479/cep?format=xml",
      "cep_pdf": "/v1/validations/f47ac10b-58cc-4372-a567-0e02b2c3d479/cep?format=pdf"
    }
  }
}

Status values:

statusMeaning
validTransfer confirmed in the Banxico CEP.
not_foundNo record exists for a transaction with those details.
cep_unavailableBanxico found the transaction but could not provide the CEP.
returnedThe payment settled and the beneficiary institution later sent it back. The CEP, if one exists, is still delivered.
errorUnexpected system failure.

2. Handle the async mode (status:"async")

When you add ?async=1 to the POST URL, the API responds 202 and the validation is processed in the background:

curl -X POST 'https://api.veriko.mx/v1/validate?async=1' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Content-Type: application/json' \
  -d '{ "fecha": "...", "monto": ..., "clave_rastreo": "...", ... }'

Response 202:

{
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "type": "validation",
    "attributes": { "status": "queued" }
  }
}

To poll the validation status: GET /v1/validations/{id} every 2–5 seconds until status reaches a terminal value (valid, not_found, cep_unavailable, invalid, returned, failed, error):

curl 'https://api.veriko.mx/v1/validations/a1b2c3d4-e5f6-7890-abcd-ef1234567890' \
  -H 'Authorization: Bearer veriko_••••'

3. Download the official receipt (CEP)

When status is valid, the response carries links.cep_pdf and links.cep_xml, and with them you can download the official receipt issued by the Banco de México (Banxico):

# PDF
curl 'https://api.veriko.mx/v1/validations/f47ac10b-.../cep?format=pdf' \
  -H 'Authorization: Bearer veriko_••••' \
  --output cep.pdf

# XML
curl 'https://api.veriko.mx/v1/validations/f47ac10b-.../cep?format=xml' \
  -H 'Authorization: Bearer veriko_••••' \
  --output cep.xml

You can also have the receipt delivered to the Telegram chat linked to the account, without downloading it yourself:

curl -X POST 'https://api.veriko.mx/v1/validations/f47ac10b-.../cep/send-telegram' \
  -H 'Authorization: Bearer veriko_••••'

The response is an immediate 202 with queued: true: it confirms it was queued, not that it was delivered. The document shows up in the chat a few seconds later, and it is the same PDF the download above returns.

Two conditions must hold, and each fails with its own code: the CEP must exist — otherwise 409 cep_not_available — and the account must have Telegram linked — otherwise 409 telegram_not_linked. The second one is visible in telegram_linked on GET /v1/users/me, and is set up in Manage notifications.

4. Enable automatic retries on unsuccessful outcomes

For not_found or cep_unavailable results, you can enable automatic retries so the platform queries Banxico again without additional calls on your part:

curl -X PUT 'https://api.veriko.mx/v1/validations/a1b2c3d4-.../retry-policy' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Content-Type: application/json' \
  -d '{
    "retry_policy": {
      "enabled": true,
      "max_retries": 3,
      "interval_seconds": 600,
      "outcomes": ["not_found", "cep_unavailable"]
    }
  }'

You can also include retry_policy directly in the body to configure retries in a single call. Check the validation's retry history with GET /v1/validations/{id}/retry-attempts.

For more information about retries, see Retry policies

5. Avoid duplicate submissions

Use the Idempotency-Key header to de-duplicate network retries. Any request with the same key and identical body within 24 hours returns the cached response:

curl -X POST 'https://api.veriko.mx/v1/validate' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Idempotency-Key: mi-sistema-txn-id-12345' \
  -H 'Content-Type: application/json' \
  -d '{ ... }'

For more information about idempotency, see Idempotency

6. Tie the validation to your order

To tie each validation to an order of your own, send client_ref in the body of POST /v1/validate or POST /v1/validate-ocr, also with ?async=1. It is text of 1 to 64 characters, without line breaks or emoji, and it must not contain personal data (names, email addresses, or accounts).

curl -X POST 'https://api.veriko.mx/v1/validate' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Content-Type: application/json' \
  -d '{ "client_ref": "orden-4812", ... }'

The reference comes back as sent in the response, in GET /v1/validations/{id}, in the list, and in the validation webhook payload (data.attributes.client_ref). To find the validation of an order, GET /v1/validations?client_ref=orden-4812 returns only those carrying that exact reference. A value that does not meet the rules answers 422 invalid_client_ref without consuming quota.

7. Try several accounts in a single validation

When you know the payment went to one of several accounts but not which (for example, a school with more than one account), send cuentas_candidatas instead of cuenta_beneficiaria. It is a single validation and a single quota unit, however many accounts it carries.

curl -X POST 'https://api.veriko.mx/v1/validate' \
  -H 'Authorization: Bearer veriko_••••' \
  -H 'Content-Type: application/json' \
  -d '{
    "fecha": "2025-03-15",
    "monto": 15000.50,
    "clave_rastreo": "MXBA20250315001234",
    "emisor": "40014",
    "cuentas_candidatas": [
      "012180004412345678",
      "002010077777777771"
    ]
  }'
ruledetail
Count2 to 3 accounts. When exceeded, the 422 reports the current maximum in meta.max.
FormatEach account is a CLABE (18 digits with a valid check digit), a card (16, with a valid Luhn) or a DiMo phone (10). They are sent as text and without repeats.
With cuenta_beneficiariaThey are mutually exclusive. Sending both answers 422 cuenta_y_candidatas_excluyentes.
Invalid list422 cuentas_candidatas_invalidas, with the position that failed in field_errors.

Both 422 happen before any quota is consumed. They also apply to POST /v1/validate-ocr and with ?async=1.

The query goes through the accounts in the order sent and adopts the first one that matches the transfer. The response carries the winner in full in normalized_data.cuenta_beneficiaria and its position in candidate_match: index (from 0) and account_last4.

{
  "data": {
    "attributes": {
      "status": "valid",
      "normalized_data": { "cuenta_beneficiaria": "002010077777777771" },
      "candidate_match": { "index": 1, "account_last4": "7771" }
    }
  }
}

In the validation webhook payload candidate_match also travels (data.attributes.candidate_match), without the full account. If no account matches, the response is a 422 with the reason in code (cuenta_unresolvable_after_probes when there is no more precise diagnosis), and the quota unit is consumed all the same: the Banxico queries were already made. The exception is the rejection of the request before querying (preflight_failed), which consumes no quota.

8. Query a valid again

A valid is the verdict of the moment of the query: the CEP proves that the transfer settled, and the receiving institution can send the money back afterwards. To confirm it is still there before closing a payment, POST /v1/validations/{id}/recheck queries Banxico again only for the payment status, with the data the validation already stored:

curl -X POST 'https://api.veriko.mx/v1/validations/f47ac10b-58cc-4372-a567-0e02b2c3d479/recheck'   -H 'Authorization: Bearer veriko_••••'

The response is the same validation, and meta.recheck says when it was queried (checked_at), whether it changed (changed), and the verdict it had before (previous_status):

{
  "data": {
    "attributes": {
      "status": "returned",
      "banxico_status": "returned",
      "banxico_result": { "_payment_status": { "code": "devuelto", "reversed": true } }
    }
  },
  "meta": {
    "recheck": { "checked_at": "2026-10-02T09:15:44Z", "changed": true, "previous_status": "valid" }
  }
}
  • The operation consumes no quota and does not count as a new validation, and the CEP is kept.
  • It only responds for a validation of the last 72 hours, with one query every 10 minutes per validation: past the period it is 422 recheck_window_expired, and a query repeated too soon is 429 recheck_rate_limited with Retry-After.
  • A validation in returned responds with its current state without querying Banxico, and any other status is 422 recheck_not_eligible.
  • If Banxico does not return a readable status, the response is 503 recheck_unavailable and it can be retried right away.

Without being asked, the service also rechecks the valid validations of the last 72 hours, and reports it with the validation.returned webhook (webhooks architecture).

What's next

  • Save accounts where you receive transfers and speed up future validations: Create a beneficiary.
  • Validate from a receipt image instead of manual fields: see Validate via OCR.
  • Browse your full validation history: GET /v1/validations (filter by status, date range, validation type).