API de CEP · Documentación
Integra la validación de transferencias SPEI y la descarga del
Comprobante Electrónico de Pago (CEP) oficial de Banco de México en tu
propio software — en JSON, PDF, XML o ZIP. API REST, sin SDKs: funciona con
curl, fetch o cualquier cliente HTTP.
Pensada para bancos, fintechs, ERPs, conciliadores y tesorerías que necesitan obtener los CEP de sus operaciones automáticamente, sin operar a mano el formulario web de Banxico. Tú envías los datos de la transferencia; nosotros consultamos a Banxico y te devolvemos el comprobante.
Primeros pasos
De cero a validar un CEP en tres pasos. Usa los datos de tu comprobante SPEI (fecha, clave de rastreo, montos y cuentas).
Todos los datos de ejemplo de esta página son ficticios
(tienen la forma correcta, pero no corresponden a ninguna transferencia real): con
ellos Banxico responde 404 not_found. Sustitúyelos por los de tu
operación.
1 · Obtén los códigos de banco
Lista las instituciones para conocer el code del emisor y receptor.
curl $BASE/api/banks
# [{ "code": "40012", "name": "BBVA MEXICO" }, { "code": "40014", "name": "SANTANDER" }, ...]2 · Valida la transferencia
Envía los datos del pago y recibe el CEP en JSON.
curl -X POST $BASE/api/cep/validate \
-H "Content-Type: application/json" \
-d '{
"fecha": "2026-09-25",
"clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014",
"receptor": "40012",
"cuenta": "012180015555555550",
"monto": "1500.00"
}'3 · Descarga el comprobante
Con el mismo cuerpo JSON, baja el PDF oficial (o XML /
ZIP). Esta es la vía recomendada para integraciones servidor-a-servidor.
curl -X POST "$BASE/api/cep/download?formato=PDF" \
-H "Content-Type: application/json" \
-d '{
"fecha": "2026-09-25",
"clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014",
"receptor": "40012",
"cuenta": "012180015555555550",
"monto": "1500.00"
}' \
-o cep.pdfBase URL
Todas las rutas son relativas a tu host. En estos ejemplos:
http://localhost:8000Autenticación
Los endpoints de CEP aceptan una API key que identifica a tu
integración. Envíala siempre que la tengas; cuando el servicio la requiere, es
obligatoria. Los endpoints /api/health, /api/ready y
/api/banks son siempre públicos. El campo auth de
/api/health indica si el servidor exige una llave.
Puedes enviar la llave de tres formas (en orden de preferencia):
| Método | Ejemplo |
|---|---|
Header X-API-Key | X-API-Key: TU_API_KEY |
| Bearer token | Authorization: Bearer TU_API_KEY |
| Query param (para descargas) | ?api_key=TU_API_KEY |
401 unauthorized. Los límites de uso son
por IP, no por llave: una llave válida no amplía el límite.
¿Necesitas una API key para tu software? Solicítala en ceppro.mx.
Health
Verifica que el servicio está disponible.
curl $BASE/api/health
const r = await fetch("$BASE/api/health");
console.log(await r.json());
import requests
requests.get("$BASE/api/health").json()
Respuesta 200
{ "status": "ok", "version": "0.1.0", "auth": false }auth es true cuando el servidor exige una API key en los
endpoints de CEP.
GET /api/ready (readiness) responde 200 cuando
los servicios de apoyo funcionan y 503 cuando Redis está configurado pero
no responde. Úsalo para balanceadores y monitoreo; para saber si el proceso vive basta
/api/health.
Listar bancos
Devuelve el catálogo de instituciones participantes en SPEI. Usa los
code para los campos emisor y receptor.
La respuesta se cachea (1 h) en el navegador.
curl $BASE/api/banks
const banks = await (await fetch("$BASE/api/banks")).json();
import requests
banks = requests.get("$BASE/api/banks").json()
Respuesta 200
[
{ "code": "40002", "name": "BANAMEX" },
{ "code": "40012", "name": "BBVA MEXICO" },
{ "code": "40014", "name": "SANTANDER" }
]Validar CEP
Valida una transferencia contra Banxico y devuelve los datos estructurados del comprobante en JSON. Ver parámetros para el detalle de campos.
curl -X POST $BASE/api/cep/validate \
-H "Content-Type: application/json" \
-H "X-API-Key: TU_API_KEY" \
-d '{
"fecha": "2026-09-25",
"clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014",
"receptor": "40012",
"cuenta": "012180015555555550",
"monto": "1500.00"
}'
const res = await fetch("$BASE/api/cep/validate", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": "TU_API_KEY" },
body: JSON.stringify({
fecha: "2026-09-25",
clave_rastreo: "EJEMPLO2026092500001",
emisor: "40014", receptor: "40012",
cuenta: "012180015555555550", monto: "1500.00"
})
});
const cep = await res.json();
import requests
cep = requests.post("$BASE/api/cep/validate",
headers={"X-API-Key": "TU_API_KEY"},
json={
"fecha": "2026-09-25",
"clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014", "receptor": "40012",
"cuenta": "012180015555555550", "monto": "1500.00",
}).json()
Respuesta 200
{
"clave_rastreo": "EJEMPLO2026092500001",
"fecha_operacion": "20260925",
"hora_operacion": "12:00:00",
"monto": "1500.00",
"iva": "0.00",
"concepto": "PAGO DE EJEMPLO",
"emisor": "SANTANDER",
"receptor": "BBVA MEXICO",
"ordenante_nombre": "JUAN PEREZ EJEMPLO",
"ordenante_cuenta": "014180000000000013",
"ordenante_rfc": "XAXX010101000",
"beneficiario_nombre": "EMPRESA DE EJEMPLO SA DE CV",
"beneficiario_cuenta": "012180015555555550",
"beneficiario_rfc": "XAXX010101000",
"sello": "U0VMTE8gREUgRUpFTVBMTw==..."
}Un CEP encontrado es el comprobante de que el pago se acreditó, así que la respuesta
no incluye el estado del pago. Cuando Banxico muestra el estado en lugar del CEP
(p. ej. Devuelto, o Liquidado antes de emitir el CEP), la respuesta es
un 409 con ese estado.
Tiempo de respuesta: una consulta nueva tarda unos segundos (Banxico se
consulta a un ritmo controlado, compartido por todos los usuarios). En cuanto la
validación responde, el servidor descarga el PDF de esa misma transferencia en
segundo plano, así que una descarga inmediatamente posterior
suele salir de la caché. Usa un timeout de cliente de al menos
60 s: el servidor deja de reintentar a los 40 s y, si la
cola hacia Banxico está llena, responde 503 busy de inmediato. Si
llegan varias solicitudes de la misma transferencia a la vez, Banxico se consulta
una sola vez y todas reciben el mismo resultado.
Descargar CEP
Valida y devuelve el comprobante oficial como archivo binario. Elige el
formato con el parámetro de query formato =
PDF · XML · ZIP (por defecto PDF).
Hay dos formas, con los mismos datos que validar:
POST(recomendado para integraciones) — cuerpo JSON, igual quevalidate. Sin codificar nada en la URL.GET— todos los datos en la query string. Útil para enlaces de descarga directos que el navegador puede abrir.
# POST (recomendado): mismo cuerpo JSON que validate
curl -X POST "$BASE/api/cep/download?formato=PDF" \
-H "Content-Type: application/json" \
-H "X-API-Key: TU_API_KEY" \
-d '{
"fecha": "2026-09-25",
"clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014",
"receptor": "40012",
"cuenta": "012180015555555550",
"monto": "1500.00"
}' \
-o cep.pdf
# GET: enlace directo (descargable en el navegador)
curl -G $BASE/api/cep/download \
--data-urlencode "fecha=2026-09-25" \
--data-urlencode "clave_rastreo=EJEMPLO2026092500001" \
--data-urlencode "emisor=40014" \
--data-urlencode "receptor=40012" \
--data-urlencode "cuenta=012180015555555550" \
--data-urlencode "monto=1500.00" \
--data-urlencode "formato=PDF" \
--data-urlencode "api_key=TU_API_KEY" \
-o cep.pdf
const body = {
fecha: "2026-09-25", clave_rastreo: "EJEMPLO2026092500001",
emisor: "40014", receptor: "40012",
cuenta: "012180015555555550", monto: "1500.00"
};
const res = await fetch("$BASE/api/cep/download?formato=PDF", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": "TU_API_KEY" },
body: JSON.stringify(body)
});
const blob = await res.blob();
import requests
r = requests.post("$BASE/api/cep/download",
params={"formato": "PDF"},
headers={"X-API-Key": "TU_API_KEY"},
json={
"fecha": "2026-09-25", "clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014", "receptor": "40012",
"cuenta": "012180015555555550", "monto": "1500.00",
})
open("cep.pdf", "wb").write(r.content)
Respuesta: el archivo binario con
Content-Type application/pdf · application/xml ·
application/zip y Content-Disposition: attachment. El
ZIP de Banxico contiene el PDF y el XML del mismo CEP. Los errores son los
mismos que en validar (ver códigos de
error).
Varios CEP en un ZIP
Recibe hasta 10 transferencias —cada una con los mismos campos que
validar— y devuelve un solo archivo ZIP con el
CEP de cada una en el formato indicado por query (PDF por
defecto, o XML / ZIP). Útil para integraciones que quieren un
solo archivo con varios CEP.
curl -X POST "$BASE/api/cep/download-zip?formato=PDF" \
-H "Content-Type: application/json" \
-H "X-API-Key: TU_API_KEY" \
-d '{ "items": [ {
"fecha": "2026-09-25",
"clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014",
"receptor": "40012",
"cuenta": "012180015555555550",
"monto": "1500.00"
} ] }' \
-o ceps.zip
import requests
items = [{
"fecha": "2026-09-25", "clave_rastreo": "EJEMPLO2026092500001",
"emisor": "40014", "receptor": "40012",
"cuenta": "012180015555555550", "monto": "1500.00",
}]
r = requests.post("$BASE/api/cep/download-zip", params={"formato": "PDF"},
headers={"X-API-Key": "TU_API_KEY"}, json={"items": items}, timeout=600)
open("ceps.zip", "wb").write(r.content)
- Respuesta
200:application/zip(ceps.zip). Las transferencias que fallen se omiten, y el encabezadoX-CEP-Omitidosdice cuántas (no hace falta comparar el contenido). Si a mitad del ZIP la cola hacia Banxico se llena o aparece un captcha, las restantes no se intentan y también cuentan enX-CEP-Omitidos. - Si ninguna se pudo generar, el código refleja la causa:
404 not_found(ninguna encontrada),409(cep_pending/cep_status),502 banxico_unavailableo503(captcha_enforced/busy; reintenta). Una causa transitoria tiene prioridad sobre «no encontrada». 422con más de 10 transferencias o datos inválidos.503 busysi el servidor ya está procesando el máximo de lotes o la cola hacia Banxico está llena; reintenta tras elRetry-After.
Cada transferencia se consulta a Banxico al ritmo compartido, así que un ZIP de 10 CEP nuevos puede tardar minutos: usa un timeout amplio. Comparte el límite por IP de los lotes (6/min por defecto).
Lote por archivos
Sube hasta 10 archivos (PDF o imagen, máx. 8 MB cada uno). El
servicio extrae los campos de cada archivo (texto de PDF o OCR de imagen) y valida cada
comprobante contra Banxico. Se envía como multipart/form-data con el campo
repetido files.
not_found para ese resultado.
curl -X POST $BASE/api/cep/batch \
-H "X-API-Key: TU_API_KEY" \
-F "files=@comprobante1.pdf" \
-F "files=@captura2.png"
const fd = new FormData();
fd.append("files", file1);
fd.append("files", file2);
const res = await fetch("$BASE/api/cep/batch", {
method: "POST", headers: { "X-API-Key": "TU_API_KEY" }, body: fd
});
const { items } = await res.json();
import requests
files = [
("files", open("comprobante1.pdf", "rb")),
("files", open("captura2.png", "rb")),
]
r = requests.post("$BASE/api/cep/batch",
headers={"X-API-Key": "TU_API_KEY"}, files=files).json()
Respuesta 200
{
"items": [
{
"filename": "comprobante1.pdf",
"source_index": 0,
"status": "valid", // valid | pending | no_cep | needs_input | not_found | extract_failed | error
"request": { "fecha": "2026-09-25", "clave_rastreo": "...", "...": "..." },
"details": { "beneficiario_nombre": "...", "monto": "1500.00" }
},
{ "filename": "captura2.png", "source_index": 1, "status": "not_found", "message": "..." }
]
}Cómo leer items
- Uno o más resultados por archivo, en orden de subida. Un PDF de
dispersión con un comprobante por página produce un resultado por página, con
filename"archivo.pdf (p. N)". Usasource_index(índice base 0 del archivo subido) para asociar cada resultado a su archivo; dos archivos pueden llamarse igual. - Ninguna página se descarta en silencio: una página con su propia clave de rastreo
pero sin todos los datos aparece como
needs_input(vermissing_fields); una página sin clave propia se toma como continuación de la anterior. - Hasta 20 comprobantes consultados por lote. Cuenta cada resultado
que podría consultar a Banxico (los completos y los
needs_input). Los que exceden el límite llegan comoerrorcon el mensaje «Se alcanzó el límite de 20 comprobantes por lote», sin consultar a Banxico: envíalos en otro lote. Por esoitemspuede tener más resultados que archivos. pendingyno_ceptraenestado(como los409de validar). Unerrores reintentable (p. ej. Banxico saturado o no disponible).
Descarga los archivos de cada
valid con /api/cep/download usando los campos de
request (normalmente ya están en caché). Un lote de muchos comprobantes
nuevos puede tardar minutos; si quieres mostrar progreso usa
la versión en streaming. Respuestas de error:
401, 422 (sin archivos o más de 10),
429 y 503 busy.
Lote en streaming
Igual que /api/cep/batch (mismos archivos, límites y errores),
pero la respuesta es application/x-ndjson: un objeto JSON por
línea, enviado en cuanto cada comprobante termina, en orden de subida. Cada
línea tiene la misma forma que un elemento de items, con
source_index. Sirve para mostrar progreso en vivo.
curl -N -X POST $BASE/api/cep/batch/stream \
-H "X-API-Key: TU_API_KEY" \
-F "files=@comprobante1.pdf" \
-F "files=@captura2.png"
import json, requests
files = [("files", open("comprobante1.pdf", "rb"))]
with requests.post("$BASE/api/cep/batch/stream", files=files, stream=True,
headers={"X-API-Key": "TU_API_KEY"}) as r:
for line in r.iter_lines():
if line:
item = json.loads(line)
print(item["source_index"], item["status"])
Los errores previos al inicio (401, 422,
429, 503 busy) llegan como JSON normal, no como NDJSON.
Una vez iniciado el stream siempre responde 200; si un archivo falla, su
línea trae status: "error".
Parámetros
Todos son obligatorios. Para validate y POST download van en
el cuerpo JSON; para GET download, como query string. formato
siempre va como parámetro de query.
| Campo | Tipo | Descripción | |
|---|---|---|---|
fecha | string | REQ | Fecha de operación, formato YYYY-MM-DD. |
clave_rastreo | string | REQ | Clave de rastreo o número de referencia (≤ 40 caracteres). Se detecta automáticamente: solo dígitos y ≤ 7 → referencia (si Banxico no la encuentra así, se reintenta una vez como clave de rastreo; si ese reintento no alcanza turno en la cola, responde 503 busy y, al repetir la solicitud en los siguientes minutos, se busca directamente como clave de rastreo); en otro caso → clave de rastreo. |
emisor | string | REQ | Código del banco emisor (ver /api/banks). |
receptor | string | REQ | Código del banco receptor. |
cuenta | string | REQ | Cuenta beneficiaria: CLABE (18 dígitos), tarjeta (16) o teléfono (10). Se eliminan espacios y guiones; el resto debe tener 18 dígitos (CLABE), 16 (tarjeta) o 10 (celular); si no, 422. El dígito verificador de la CLABE no se valida: Banxico decide si la transferencia existe. |
monto | string | REQ | Monto en pesos, mayor que 0 y con hasta 2 decimales, p. ej. 1500.00. |
formato | string | OPC | Solo download y download-zip: PDF (default), XML o ZIP. |
Un campo que solo contiene espacios cuenta como faltante (422).
Códigos de error
Los errores devuelven JSON con error (código legible por máquina) y
detail (mensaje en español). La excepción es el 422, que
tiene dos formas y tu cliente debe aceptar ambas:
- Validación de esquema (cuerpo JSON inválido, o un parámetro de
query con tipo o longitud incorrectos): el formato estándar de FastAPI, sin
error:{"detail": [{"type", "loc", "msg", …}]}. - Validaciones propias (p. ej. monto o datos inválidos en
GET /api/cep/download, lote con más de 10 archivos):{"error": "invalid_request", "detail": "…"}.
| HTTP | error | Significado |
|---|---|---|
401 | unauthorized | API key faltante o inválida (cuando el servidor tiene auth activada). |
404 | not_found | Banxico no encontró una transferencia con esos datos (en download-zip: no se encontró ninguna; otras causas usan su propio código). |
409 | cep_pending | Banxico identificó el pago, pero el CEP aún no está disponible (reintenta más tarde). Incluye estado (p. ej. Liquidado, En proceso) cuando Banxico lo muestra. |
409 | cep_status | Banxico encontró la operación, pero está en un estado sin CEP descargable: Devuelto, Retornado, Cancelado, Rechazado, No liquidado o En proceso de devolución (ver estado). |
422 | — / invalid_request | Parámetros inválidos o faltantes (ver las dos formas arriba), p. ej. una cuenta que no tiene 18, 16 ni 10 dígitos. |
413 | payload_too_large | Cuerpo mayor a 1 MB (90 MB en /api/cep/batch*). Si lo corta el proxy, la respuesta puede no ser JSON. |
429 | rate_limited | Demasiadas solicitudes desde tu IP (por defecto 30/min por endpoint en consultas que no están en caché y en bancos; 120/min en total en consultas, incluidas las de caché; 6/min compartidos entre lotes y ZIP); reintenta con backoff. |
502 | banxico_unavailable | Banxico no respondió, lo rechazó temporalmente (403/429) o devolvió un error transitorio, incluso después de los reintentos. |
503 | captcha_enforced | Banxico exige captcha (rate-limiting); reintenta más tarde. |
503 | busy | Servicio saturado: la cola compartida hacia Banxico está llena, o el servidor ya atiende el máximo de consultas o lotes simultáneos. No se consultó a Banxico (salvo cuando el detail indica que la búsqueda por número de referencia no encontró la transferencia y la búsqueda por clave de rastreo quedó en espera: al reintentar se busca directamente por clave de rastreo); incluye Retry-After: 10 — respétalo. Aplica a todos los endpoints de CEP. |
Ejemplo 404
{ "error": "not_found", "detail": "Banxico no encontró una transferencia que coincida con los datos proporcionados." }Ejemplo 409 con estado
Cuando Banxico muestra el estado del pago en lugar del CEP, la respuesta lo incluye
tal como aparece en Banxico, junto con sus fechas (fecha_recepcion y
fecha_procesamiento) cuando Banxico las muestra.
{
"error": "cep_status",
"estado": "Devuelto",
"fecha_recepcion": "25/09/2026 12:00:00",
"fecha_procesamiento": "25/09/2026 12:00:03",
"detail": "Estado del pago en Banxico: Devuelto. La institución del beneficiario devolvió el pago; …"
}Ejemplo 503 busy
{ "error": "busy", "detail": "Banxico está muy solicitado en este momento; intenta de nuevo en unos segundos." }Límites de uso e idempotencia
Banxico aplica rate-limiting por IP a su servicio de CEP. La API
lo mitiga con una cola de consultas a ritmo controlado (compartida por todos los
usuarios), reintentos con backoff y caché de comprobantes (los CEP son inmutables,
así que repetir la misma consulta dentro de las 24 h siguientes es instantáneo,
no vuelve a golpear a Banxico y no cuenta para el límite de 30 por minuto). Cuando la cola está llena, la API responde
503 busy de inmediato en lugar de hacerte esperar.
| Límite | Valor por defecto |
|---|---|
Consultas que pueden llegar a Banxico (no están en caché), por IP y por endpoint (validate tiene su propio límite; download GET y POST comparten uno; /api/banks tiene otro, donde cuenta toda solicitud) | 30 por minuto |
Todas las consultas (validate y download), incluidas las que salen de la caché, por IP | 120 por minuto |
Lotes y ZIP (batch, batch/stream, download-zip), por IP | 6 por minuto |
| Archivos por lote · comprobantes consultados por lote | 10 · 20 |
Transferencias por download-zip | 10 |
| Tamaño por archivo (lote) | 8 MB |
| Una consulta individual (con reintentos) no inicia nuevas peticiones a Banxico después de | ~40 s (más el tiempo de respuesta de Banxico) |
Recomendaciones para integrar
- Trata cada consulta como idempotente: validar o descargar la misma transferencia siempre devuelve el mismo CEP. Puedes reintentar sin efectos secundarios.
- Reintenta con backoff exponencial ante
429 rate_limited,502 banxico_unavailable,503 captcha_enforcedy503 busy(p. ej. 2 s, 4 s, 8 s); enbusyrespeta elRetry-After(10 s). - Usa un timeout de al menos 60 s en consultas individuales: una consulta nueva puede esperar su turno en la cola hacia Banxico. Para lotes y ZIP, usa uno de varios minutos (o el streaming).
- Los endpoints de lote y ZIP (
/api/cep/batch,/api/cep/batch/stream,/api/cep/download-zip) tienen un límite por IP más estricto que las consultas individuales, porque cada uno consulta hasta 20 CEP (lote) o 10 (ZIP). Procesa los lotes de forma secuencial, no en paralelo. 409 cep_pendingsignifica que el pago existe pero el CEP aún no está listo (el campoestadodice en qué punto va, p. ej. Liquidado o En proceso): reintenta más tarde (minutos/horas), no de inmediato.409 cep_statussignifica que la operación existe pero está en un estado sin CEP descargable (p. ej. Devuelto): no reintentes, revisa el campoestadoy eldetailque Banxico reporta.- Evita ráfagas concurrentes desde una sola IP; encola y procesa a un ritmo moderado para no disparar el captcha de Banxico.