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):

EndpointTipoCosto
/v1/validateCon IA (OCR de imagen/PDF)2 creditos
/v1/validate/manualSin IA (nos mandas los datos)1 credito

Politica de cobro (transparente):

ResultadoCreditoPor que
validSe cobraEl pago existe y coincide.
not_foundSe cobraEl trabajo se hizo: Banxico no encontro la operacion.
failedSe reembolsaFalla 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

CasoQue pasa
Envias receiverAccountIdSe valida contra esa cuenta (debe ser tuya).
Lo omitesSe usa tu cuenta predeterminada.
No tienes ninguna cuenta400 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

EncabezadoValorRequerido
AuthorizationBearer sk_live_...Si
Content-Typemultipart/form-data (con archivo) o application/json (con URL)Si (lo pone tu cliente)

Cuerpo

CampoTipoRequeridoDescripcion
filearchivoVer notaEl comprobante SPEI: imagen (JPG, PNG, WEBP) o PDF. Incluso captura de WhatsApp. Maximo 20 MB. Se manda como multipart/form-data.
fileUrlstring (URL)Ver notaAlternativa 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.
receiverAccountIdstringNoCuenta 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"
}
CampoTipoRequeridoDescripcion
operationDatestringSiFecha de operacion en formato DD-MM-YYYY.
trackingKeystringSiClave de rastreo del comprobante.
senderBankstringSiBanco emisor: nombre (ej. BBVA MEXICO) o su codigo Banxico (ej. 40012).
amountnumberSiMonto de la transferencia en MXN.
receiverAccountIdstringNoCuenta 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
}
CampoTipoDescripcion
idstringIdentificador unico de la validacion (UUID).
statusstringResultado: valid, not_found o failed.
messagestringExplicacion legible del resultado.
modelstringModelo de IA usado en el OCR (segun tu plan).
durationMsnumberTiempo de procesamiento en milisegundos.
creditsobjectcharged (creditos cobrados) y remaining (saldo).
ocrobjectDatos crudos que leyo el OCR del comprobante.
cepobjectCEP completo (ver abajo). null si no hubo CEP.
errorobject{ 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.

CampoTipoDescripcion
availablebooleanSi Banxico entrego el CEP.
operationDate / settlementDatestringFecha de operacion y de abono (YYYY-MM-DD).
captureTimestringHora de abono (HH:MM:SS).
trackingKeystringClave de rastreo.
referenceNumberstringReferencia numerica.
paymentConceptstringConcepto del pago.
amount / taxnumberMonto e IVA.
currencystringMoneda (MXN).
sender / receiverobjectOrdenante y beneficiario: institutionCode, institutionName, accountHolder, accountType, account, taxId.
certificateSerialNumberstringNumero de serie del certificado.
originalStringstringCadena original del CEP.
digitalSealstringSello digital (firma de la institucion receptora).

Valores de status

ValorSignificado
validBanxico confirmo la operacion: monto y cuenta coinciden. Pago real.
not_foundBanxico no encontro una operacion con esos datos. Trata el comprobante como no verificado.
failedNo 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.

CodigoNombreerrorCuando ocurre
200OK—La solicitud se proceso. Revisa status para el resultado.
400Bad Requestmissing_file / validation_errorNo mandaste file ni fileUrl, o algun campo tiene formato invalido.
400Bad Requestinvalid_url / download_failed / file_too_largeEl fileUrl es invalido o no publico, no se pudo descargar, o pesa mas de 20 MB. No se cobra credito.
400Bad Requestno_receiver_accountNo tienes ninguna cuenta receptora configurada. No se cobra credito.
400Bad Requestreceiver_account_not_foundEl receiverAccountId enviado no existe o no es tuyo. No se cobra credito.
401Unauthorizedinvalid_api_keyAPI key ausente, mal formada o revocada.
402Payment Requiredinsufficient_creditsTu cuenta no tiene creditos disponibles.
403Forbiddenaccount_disabledLa cuenta esta desactivada.
404Not Foundnot_foundLa validacion consultada no existe o no es tuya.
429Too Many Requests—Superaste el limite de solicitudes por minuto. Espera lo indicado en Retry-After.
503Service Unavailableservice_unavailableEl servicio de validacion esta temporalmente no disponible. No se cobra credito.
500Internal 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).

PlanModelo
Bienvenida (gratis)gpt-5-mini
Basicogpt-5.6-luna
Premiumgpt-5.6-terra
Business / Personalizadogpt-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:

ACTINVERAFIRMEALBOAZTECABAJIOBANAMEXBANCOPPELBANORTEBANREGIOBBVA MEXICOCITI MEXICOCLIPCOMPARTAMOSCONSUBANCOCUENCAHEY BANCOHSBCINBURSAINVEXKLARMERCADO PAGOMIFELMULTIVA BANCONU / NUBANKSANTANDERSCOTIABANKSPIN BY OXXOSTPUALAVE POR MASVOLKSWAGEN

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).

Node.jsnpm install speicep Disponible Pythonpip install speicep Disponible PHPProximamente Pronto RubyProximamente Pronto JavaProximamente Pronto GoProximamente Pronto .NETProximamente 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 POST con Content-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

EventoCuando se dispara
validation.completedLa validacion termino con veredicto: valid (el pago existe) o not_found (Banxico no lo tiene). Revisa data.status.
validation.failedNo 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

EncabezadoValor
X-Speicep-EventTipo de evento (ej. validation.completed).
X-Speicep-DeliveryID unico de la entrega (usalo para idempotencia).
X-Speicep-TimestampMarca de tiempo en ms usada para firmar.
X-Speicep-Signaturesha256=<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.

MetodoEndpointQue hace
GET/me/webhookVer la URL y el secreto actuales.
PUT/me/webhookDefinir o cambiar la URL: { "url": "https://..." }.
POST/me/webhook/rotate-secretGenerar un secreto nuevo (invalida el anterior).
DELETE/me/webhookDesactivar los webhooks.
GET/me/webhook/deliveriesHistorial 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.