Referencia de la API
Documentacion de SPEICEP
La API de SPEICEP valida comprobantes de transferencia SPEI combinando lectura por OCR con verificacion en tiempo real contra el CEP de Banxico. Envias el comprobante y recibes un resultado estructurado con el CEP completo — incluidos cadena original y sello digital.
Introduccion
Cada validacion hace tres cosas: (1) el OCR extrae monto, fecha, banco y clave de rastreo del comprobante; (2) se consulta el CEP de Banxico; (3) se devuelve un veredicto y, si el pago existe, el CEP completo. Todas las respuestas de la API estan en ingles.
URL base
https://speicep.com/api
En desarrollo local la API corre en http://localhost:3000.
Autenticacion
Toda solicitud a la API publica requiere tu API key en el encabezado Authorization con el esquema Bearer. Generas y revocas tus llaves desde tu panel.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
- La llave completa se muestra una sola vez al crearla; guardala en un lugar seguro.
- Una llave ausente o invalida devuelve 401
invalid_api_key. - Si tu cuenta esta desactivada, devuelve 403
account_disabled.
Creditos y consumo
El costo depende del tipo de validacion (la IA es lo que cuesta):
| Endpoint | Tipo | Costo |
|---|---|---|
/v1/validate | Con IA (OCR de imagen/PDF) | 2 creditos |
/v1/validate/manual | Sin IA (nos mandas los datos) | 1 credito |
Politica de cobro (transparente):
| Resultado | Credito | Por que |
|---|---|---|
valid | Se cobra | El pago existe y coincide. |
not_found | Se cobra | El trabajo se hizo: Banxico no encontro la operacion. |
failed | Se reembolsa | Falla nuestra (timeout / servicio caido). No pagas por nuestros errores. |
Sin creditos, la API responde 402 insufficient_credits antes de procesar. El saldo restante viene en cada respuesta en credits.remaining.
Rate limiting
Para proteger el servicio hay un limite de 120 solicitudes por minuto por IP. Al excederlo, la API responde 429 Too Many Requests con un encabezado Retry-After (segundos a esperar). Tu cuota real de uso la marcan tus creditos, no este limite.
Cuentas receptoras
Cada validacion comprueba que el pago haya entrado a una cuenta tuya (banco + CLABE). Debes registrar al menos una cuenta receptora antes de validar. Puedes tener varias (distintos bancos/CLABEs) y elegir cual usar en cada peticion con receiverAccountId.
Como se elige la cuenta
| Caso | Que pasa |
|---|---|
Envias receiverAccountId | Se valida contra esa cuenta (debe ser tuya). |
| Lo omites | Se usa tu cuenta predeterminada. |
| No tienes ninguna cuenta | 400 no_receiver_account (no se cobra credito). |
Donde obtienes el receiverAccountId
Desde tu panel: Dashboard → Cuentas receptoras. Cada cuenta muestra su id con un boton Copiar ID; ese valor es el que mandas como receiverAccountId. Ahi mismo agregas cuentas (eligiendo banco y CLABE) y marcas cual es la predeterminada.
Al agregar una cuenta se valida que la CLABE tenga 18 digitos (invalid_clabe) y que el banco exista en el catalogo (invalid_bank).
Endpoints
POST /v1/validate
Valida un comprobante con OCR. Es el modo principal. Puedes mandar el comprobante de dos formas: subiendo el archivo (multipart/form-data), o enviando su URL publica en fileUrl (JSON o form-data) y nosotros lo descargamos.
POST https://speicep.com/api/v1/validate
Encabezados
| Encabezado | Valor | Requerido |
|---|---|---|
Authorization | Bearer sk_live_... | Si |
Content-Type | multipart/form-data (con archivo) o application/json (con URL) | Si (lo pone tu cliente) |
Cuerpo
| Campo | Tipo | Requerido | Descripcion |
|---|---|---|---|
file | archivo | Ver nota | El comprobante SPEI: imagen (JPG, PNG, WEBP) o PDF. Incluso captura de WhatsApp. Maximo 20 MB. Se manda como multipart/form-data. |
fileUrl | string (URL) | Ver nota | Alternativa a file: la URL publica (http/https) del comprobante. La descargamos por ti (imagen o PDF, max 20 MB). No se aceptan direcciones internas/privadas. |
receiverAccountId | string | No | Cuenta receptora contra la que se valida. Si se omite, se usa tu cuenta predeterminada. Ver Cuentas receptoras. |
Manda uno de los dos: file o fileUrl. Si falta cualquiera, la API responde 400 missing_file. Una URL invalida o hacia una direccion no publica responde 400 invalid_url, y si no se puede descargar, download_failed (en ninguno de estos casos se cobra credito). Cuesta lo mismo que con archivo: 2 creditos (es con OCR). Recuerda tener al menos una cuenta receptora configurada.
POST /v1/validate/manual
Valida tecleando los datos, sin subir archivo (sin OCR, no usa modelo de IA).
POST https://speicep.com/api/v1/validate/manual
Content-Type: application/json
{
"operationDate": "30-08-2026",
"trackingKey": "MBAN01002608300000012345",
"senderBank": "BBVA MEXICO",
"amount": 5000.00,
"receiverAccountId": "ef86cd36-59f0-40cd-9c1a-59cd0c58685c"
}
| Campo | Tipo | Requerido | Descripcion |
|---|---|---|---|
operationDate | string | Si | Fecha de operacion en formato DD-MM-YYYY. |
trackingKey | string | Si | Clave de rastreo del comprobante. |
senderBank | string | Si | Banco emisor: nombre (ej. BBVA MEXICO) o su codigo Banxico (ej. 40012). |
amount | number | Si | Monto de la transferencia en MXN. |
receiverAccountId | string | No | Cuenta receptora a usar. Si se omite, se usa tu predeterminada. Ver Cuentas receptoras. |
GET /v1/validations/:id
Recupera una validacion anterior por su id. Devuelve el mismo esquema de respuesta.
GET https://speicep.com/api/v1/validations/ef95734e-f9e7-42d3-b2a6-8e256aa9cb35
Esquema de respuesta
Toda validacion (nueva o consultada) devuelve este objeto. Un 200 significa que la solicitud se proceso — revisa siempre el campo status para el resultado real.
{
"id": "ef95734e-f9e7-42d3-b2a6-8e256aa9cb35",
"object": "validation",
"status": "valid",
"message": "The payment was found in Banxico records. Amount and destination account match.",
"createdAt": "2026-08-30T23:36:46.421Z",
"durationMs": 8213,
"model": "gpt-5.6-terra",
"credits": { "charged": 1, "remaining": 1796 },
"ocr": {
"fecha": "30-08-2026",
"clave_rastreo": "MBAN01002608300000012345",
"banco_emisor": "BBVA MEXICO",
"monto": "5000.00",
"emisor_code": "40012"
},
"cep": {
"available": true,
"operationDate": "2026-08-30",
"settlementDate": "2026-08-30",
"captureTime": "13:09:59",
"trackingKey": "MBAN01002608300000012345",
"referenceNumber": "12345",
"paymentConcept": "Pago de factura A-123",
"amount": 5000.00,
"currency": "MXN",
"tax": 0,
"sender": {
"institutionCode": "40012", "institutionName": "BBVA MEXICO",
"accountHolder": "JUAN PEREZ LOPEZ",
"accountType": "CLABE", "account": "012180001234567890", "taxId": "PELJ850101AB1"
},
"receiver": {
"institutionCode": "40014", "institutionName": "SANTANDER",
"accountHolder": "COMERCIALIZADORA EJEMPLO, S.A. DE C.V.",
"accountType": "CLABE", "account": "014180987654321098", "taxId": "CEE200101AB2"
},
"certificateSerialNumber": "00001000000500000001",
"originalString": "||01|30082026|...||",
"digitalSeal": "HvjHGJ...=="
},
"error": null
}
| Campo | Tipo | Descripcion |
|---|---|---|
id | string | Identificador unico de la validacion (UUID). |
status | string | Resultado: valid, not_found o failed. |
message | string | Explicacion legible del resultado. |
model | string | Modelo de IA usado en el OCR (segun tu plan). |
durationMs | number | Tiempo de procesamiento en milisegundos. |
credits | object | charged (creditos cobrados) y remaining (saldo). |
ocr | object | Datos crudos que leyo el OCR del comprobante. |
cep | object | CEP completo (ver abajo). null si no hubo CEP. |
error | object | { code, message } si status es failed; si no, null. |
Objeto cep
Contiene el Comprobante Electronico de Pago (CEP) de Banxico — la prueba de que el dinero se movio.
| Campo | Tipo | Descripcion |
|---|---|---|
available | boolean | Si Banxico entrego el CEP. |
operationDate / settlementDate | string | Fecha de operacion y de abono (YYYY-MM-DD). |
captureTime | string | Hora de abono (HH:MM:SS). |
trackingKey | string | Clave de rastreo. |
referenceNumber | string | Referencia numerica. |
paymentConcept | string | Concepto del pago. |
amount / tax | number | Monto e IVA. |
currency | string | Moneda (MXN). |
sender / receiver | object | Ordenante y beneficiario: institutionCode, institutionName, accountHolder, accountType, account, taxId. |
certificateSerialNumber | string | Numero de serie del certificado. |
originalString | string | Cadena original del CEP. |
digitalSeal | string | Sello digital (firma de la institucion receptora). |
Valores de status
| Valor | Significado |
|---|---|
| valid | Banxico confirmo la operacion: monto y cuenta coinciden. Pago real. |
| not_found | Banxico no encontro una operacion con esos datos. Trata el comprobante como no verificado. |
| failed | No se pudo completar por una falla nuestra. No se cobro credito. Reintenta. |
Codigos de estado HTTP
Un 200 no significa que el pago sea valido — significa que la solicitud se proceso. Revisa siempre el campo status del body.
| Codigo | Nombre | error | Cuando ocurre |
|---|---|---|---|
| 200 | OK | — | La solicitud se proceso. Revisa status para el resultado. |
| 400 | Bad Request | missing_file / validation_error | No mandaste file ni fileUrl, o algun campo tiene formato invalido. |
| 400 | Bad Request | invalid_url / download_failed / file_too_large | El fileUrl es invalido o no publico, no se pudo descargar, o pesa mas de 20 MB. No se cobra credito. |
| 400 | Bad Request | no_receiver_account | No tienes ninguna cuenta receptora configurada. No se cobra credito. |
| 400 | Bad Request | receiver_account_not_found | El receiverAccountId enviado no existe o no es tuyo. No se cobra credito. |
| 401 | Unauthorized | invalid_api_key | API key ausente, mal formada o revocada. |
| 402 | Payment Required | insufficient_credits | Tu cuenta no tiene creditos disponibles. |
| 403 | Forbidden | account_disabled | La cuenta esta desactivada. |
| 404 | Not Found | not_found | La validacion consultada no existe o no es tuya. |
| 429 | Too Many Requests | — | Superaste el limite de solicitudes por minuto. Espera lo indicado en Retry-After. |
| 503 | Service Unavailable | service_unavailable | El servicio de validacion esta temporalmente no disponible. No se cobra credito. |
| 500 | Internal Server Error | — | Error inesperado del servidor. |
Formato de errores
Los errores devuelven un cuerpo consistente con un codigo estable (error) y un mensaje legible:
{
"error": "insufficient_credits",
"message": "Your account has no credits left. Top up to continue validating payments."
}
Programa contra el campo error (estable), no contra message (puede cambiar).
Ejemplo: falta la cuenta receptora
Si validas sin tener ninguna cuenta receptora configurada (o el receiverAccountId no es tuyo), la API responde 400 y no se cobra credito:
{
"error": "no_receiver_account",
"message": "Configure a receiver account in your profile before validating a payment."
}
Modelos de IA por plan
El modelo de OCR que usa cada validacion depende de tu plan. Solo aplica al modo con archivo (/v1/validate).
| Plan | Modelo |
|---|---|
| Bienvenida (gratis) | gpt-5-mini |
| Basico | gpt-5.6-luna |
| Premium | gpt-5.6-terra |
| Business / Personalizado | gpt-5.6-sol |
Bancos validos
El campo senderBank (o el banco detectado por OCR) acepta el nombre del banco o su codigo Banxico. Algunos de los mas comunes:
Tambien puedes enviar directamente el codigo numerico de Banxico (ej. 40021 para HSBC).
Librerias cliente (SDKs)
Integra SPEICEP en tu lenguaje favorito. Los SDKs oficiales envuelven la API en una sola llamada. Node y Python ya estan disponibles; los demas vienen en camino (mientras tanto, puedes usar la API REST directo desde cualquier lenguaje).
npm install speicep
Disponible
pip install speicep
Disponible
Proximamente
Pronto
Proximamente
Pronto
Proximamente
Pronto
Proximamente
Pronto
Proximamente
Pronto
Ejemplo con el SDK
Node.js — npm install speicep
import { SpeiCep } from "speicep";
const speicep = new SpeiCep("sk_live_TU_LLAVE");
const res = await speicep.validate("comprobante.pdf");
console.log(res.status, res.cep?.trackingKey);
Python — pip install speicep
from speicep import SpeiCep
speicep = SpeiCep("sk_live_TU_LLAVE")
res = speicep.validate("comprobante.pdf")
print(res["status"], res["cep"]["trackingKey"])
Ejemplos
cURL (subiendo el archivo)
El campo receiverAccountId es opcional; omitelo para usar tu cuenta predeterminada.
curl -X POST https://speicep.com/api/v1/validate \
-H "Authorization: Bearer sk_live_TU_LLAVE" \
-F "file=@comprobante.jpg" \
-F "receiverAccountId=ef86cd36-59f0-40cd-9c1a-59cd0c58685c"
cURL (con URL del comprobante)
En vez de subir el archivo, manda su URL publica en fileUrl (JSON):
curl -X POST https://speicep.com/api/v1/validate \
-H "Authorization: Bearer sk_live_TU_LLAVE" \
-H "Content-Type: application/json" \
-d '{"fileUrl": "https://misitio.com/comprobantes/abc.jpg", "receiverAccountId": "ef86cd36-59f0-40cd-9c1a-59cd0c58685c"}'
Node.js
import fs from "node:fs";
const form = new FormData();
form.append("file", new Blob([fs.readFileSync("comprobante.jpg")]), "comprobante.jpg");
form.append("receiverAccountId", "ef86cd36-59f0-40cd-9c1a-59cd0c58685c"); // opcional
const res = await fetch("https://speicep.com/api/v1/validate", {
method: "POST",
headers: { Authorization: "Bearer sk_live_TU_LLAVE" },
body: form,
});
const data = await res.json();
console.log(data.status, data.cep?.trackingKey);
Python
import requests
r = requests.post(
"https://speicep.com/api/v1/validate",
headers={"Authorization": "Bearer sk_live_TU_LLAVE"},
files={"file": open("comprobante.jpg", "rb")},
data={"receiverAccountId": "ef86cd36-59f0-40cd-9c1a-59cd0c58685c"}, # opcional
)
data = r.json()
print(data["status"], data["cep"]["trackingKey"])
Webhooks
Introduccion
Los webhooks avisan a tu servidor cuando una validacion termina, sin que tengas que estar consultando. Cuando configuras una URL, SPEICEP le manda un POST con el resultado en cuanto ocurre el evento. Disponible desde el plan Business.
- Cada envio es un
POSTconContent-Type: application/json. - Va firmado con HMAC-SHA256 para que verifiques que de verdad viene de SPEICEP.
- Si tu servidor no responde 2xx, se reintenta con backoff.
- Responde rapido (menos de 10 s) con un 2xx; si necesitas mas tiempo, procesa en segundo plano.
Eventos
| Evento | Cuando se dispara |
|---|---|
validation.completed | La validacion termino con veredicto: valid (el pago existe) o not_found (Banxico no lo tiene). Revisa data.status. |
validation.failed | No se pudo completar por una falla nuestra (timeout, servicio caido). Ese credito se reembolsa. |
Formato del evento
El cuerpo es un sobre con id, type, createdAt y data. El campo data es el mismo objeto que devuelve GET /v1/validations/:id.
{
"id": "evt_9f2a1c7e-...",
"type": "validation.completed",
"createdAt": "2026-09-04T18:12:34.000Z",
"data": {
"id": "5f0e...",
"object": "validation",
"status": "valid",
"credits": { "charged": 2, "remaining": 1998 },
"cep": { "trackingKey": "...", "amount": 1, "currency": "MXN" }
}
}
Encabezados de cada envio
| Encabezado | Valor |
|---|---|
X-Speicep-Event | Tipo de evento (ej. validation.completed). |
X-Speicep-Delivery | ID unico de la entrega (usalo para idempotencia). |
X-Speicep-Timestamp | Marca de tiempo en ms usada para firmar. |
X-Speicep-Signature | sha256=<hmac> del cuerpo (ver abajo). |
Idempotencia: si recibes el mismo X-Speicep-Delivery dos veces (por un reintento), procesalo una sola vez.
Verificar la firma
La firma es HMAC-SHA256 sobre la cadena <timestamp>.<cuerpo_crudo> con tu secreto (whsec_...). Recalculala y comparala con X-Speicep-Signature. Importante: usa el cuerpo crudo, sin re-serializar el JSON.
Node.js (Express)
import crypto from "node:crypto";
// Usa el cuerpo CRUDO: express.raw({ type: "application/json" })
app.post("/webhooks/speicep", express.raw({ type: "application/json" }), (req, res) => {
const secret = process.env.SPEICEP_WEBHOOK_SECRET; // whsec_...
const ts = req.header("X-Speicep-Timestamp");
const sig = (req.header("X-Speicep-Signature") || "").replace(/^sha256=/, "");
const expected = crypto.createHmac("sha256", secret)
.update(`${ts}.${req.body.toString("utf8")}`).digest("hex");
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(400).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
// ... procesa event.type / event.data ...
res.sendStatus(200);
});
Python (Flask)
import hmac, hashlib, os
from flask import request
@app.post("/webhooks/speicep")
def speicep_webhook():
secret = os.environ["SPEICEP_WEBHOOK_SECRET"] # whsec_...
ts = request.headers.get("X-Speicep-Timestamp", "")
sig = request.headers.get("X-Speicep-Signature", "").replace("sha256=", "")
raw = request.get_data() # cuerpo crudo (bytes)
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
return "bad signature", 400
event = request.get_json()
# ... procesa event["type"] / event["data"] ...
return "", 200
Configuracion
Configuras tu webhook por la API con los endpoints de abajo (el panel visual en el dashboard llegara pronto). El secreto se genera al definir la URL y se muestra una sola vez para que lo guardes. Los webhooks estan disponibles desde el plan Business.
| Metodo | Endpoint | Que hace |
|---|---|---|
| GET | /me/webhook | Ver la URL y el secreto actuales. |
| PUT | /me/webhook | Definir o cambiar la URL: { "url": "https://..." }. |
| POST | /me/webhook/rotate-secret | Generar un secreto nuevo (invalida el anterior). |
| DELETE | /me/webhook | Desactivar los webhooks. |
| GET | /me/webhook/deliveries | Historial de entregas (para depurar). |
Reintentos: si tu servidor no responde 2xx, se reintenta hasta 3 veces mas (a los 30 s, 2 min y 10 min). Puedes revisar cada intento en /me/webhook/deliveries.