Subalans API
Obtén documentos fiscales (SAT, IMSS, Infonavit), consulta el Buzón Tributario, descarga CFDIs y valida RFCs contra las listas negras del SAT — todo desde una API REST simple, sin manejar navegadores ni portales gubernamentales.
Introducción
Cada servicio vive bajo un mismo dominio y expone un único endpoint POST /api/v1 (más un GET /health). Todas las peticiones se autentican con un Bearer token y envían las credenciales del contribuyente en el cuerpo JSON. Las APIs son stateless: hacen login en cada llamada, no guardan credenciales ni archivos, y no registran datos sensibles.
| Servicio | Endpoint (Base URL) | Devuelve |
|---|---|---|
| Constancia de Situación Fiscal | https://subalans.com | |
| Opinión SAT (32D) | https://subalans.com | |
| Opinión IMSS | https://subalans.com | |
| Opinión Infonavit | https://subalans.com | |
| Buzón Tributario (notificaciones) | https://subalans.com | JSON (metadatos) |
| CFDI (descarga / metadatos) | https://subalans.com | ZIP · XML · PDF |
| Listas negras EFOS (69-B) | https://subalans.com | JSON |
/constancia, /opinion, /opinion-imss, /infonavit) devuelven en 200 el PDF binario directamente, con Content-Type: application/pdf y Content-Disposition — la respuesta no es JSON. Sólo CFDI y Buzón responden JSON; ahí los archivos vienen codificados en Base64 (campos zip_base64, base64). Los errores siempre son JSON.Autenticación
Manda tu token en el header Authorization con el esquema Bearer. Todas las peticiones usan Content-Type: application/json.
| Header | Valor | |
|---|---|---|
Authorization | Bearer sk_live_xxxxxxxxxxxx | requerido |
Content-Type | application/json | requerido |
curl -X POST https://subalans.com/api/v1/constancia \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "rfc": "XAXX010101000", "ciec": "TU_CIEC" }'
Un token ausente devuelve 401 { "error": "Falta el header Authorization: Bearer <token>." }; una llave inválida o revocada devuelve 401 { "error": "API key inválida o revocada." }. Los 401 traen solo error (sin ok).
Permisos y vigencia de las llaves
Desde el dashboard de desarrolladores cada llave puede configurarse con:
- Permisos por servicio — restringe la llave a ciertos servicios (p. ej. sólo EFOS y CFDI). Llamar a un servicio no permitido devuelve 403
code: "key_scope_denied"con la listaallowed_servicesen el cuerpo. - Vigencia — 7 días, 30 días, 90 días, 1 año o sin expiración. Una llave vencida (
expires_at) responde 401 igual que una revocada.
Credenciales del contribuyente
Cada documento se obtiene autenticándose ante el portal del gobierno con las credenciales del contribuyente. Hay dos métodos; cada servicio acepta uno o ambos:
Método CIEC
| Campo | Tipo | Descripción |
|---|---|---|
rfc | string | RFC del contribuyente (12-13 caracteres). |
ciec | string | Contraseña CIEC del SAT. |
Método e.firma (FIEL)
| Campo | Tipo | Descripción |
|---|---|---|
rfc | string | RFC del contribuyente. |
cer_b64 | string (base64) | Archivo .cer (certificado) codificado en Base64. |
key_b64 | string (base64) | Archivo .key (clave privada) codificado en Base64. |
password | string | Contraseña de la clave privada de la e.firma. El campo se llama password; en Infonavit también se acepta el alias efirma_password para distinguirla de la contraseña del portal. |
const fs = require("fs"); const payload = { rfc: "ACE180101AA1", cer_b64: fs.readFileSync("cert.cer").toString("base64"), key_b64: fs.readFileSync("private.key").toString("base64"), password: "TU_PASSWORD_EFIRMA", };
.key no abre con la contraseña, o el .cer no es un certificado válido, responde en milisegundos con 422 y un mensaje claro — sin gastar tiempo en el SAT.Formato de petición y respuesta
Toda petición es un POST con cuerpo JSON al endpoint /api/v1 del servicio. La forma del 200 depende del servicio:
- Endpoints de PDF (
/constancia,/opinion,/opinion-imss,/infonavit): el cuerpo de la respuesta es el PDF binario (Content-Type: application/pdf+Content-Disposition). Guárdalo a disco o súbelo a tu storage tal cual. - CFDI y Buzón: JSON con
"ok": truey los datos; los archivos van en Base64.
HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="constancia.pdf" %PDF-1.7 … (bytes del PDF)
const r = await fetch("https://subalans.com/api/v1/constancia", { method: "POST", headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!r.ok) throw new Error((await r.json()).error); // los errores sí son JSON const pdf = Buffer.from(await r.arrayBuffer()); // el 200 es binario fs.writeFileSync("constancia.pdf", pdf); // o súbelo a tu storage
Manejo de errores
Las respuestas de error siempre son JSON con "ok": false, un error legible (en español, listo para mostrar al usuario) y, cuando aplica, un code semántico para tu lógica. El código HTTP te dice de quién es el problema:
| HTTP | Significado | ¿De quién? | Qué hacer |
|---|---|---|---|
| 200 | Éxito — documento incluido. | — | Procesa la respuesta. |
| 400 | Petición inválida (RFC mal formado, faltan campos). | Tu integración | Corrige el cuerpo. No reintentar. |
| 401 | Token ausente o inválido. | Tu integración | Revisa el header Authorization. |
| 422 | Credencial incorrecta o sin permiso (CIEC/e.firma mala, sin medios de contacto…). | El contribuyente | Pide al usuario corregir la credencial. No reintentar igual. |
| 429 | Rebasaste el límite de solicitudes/min de tu plan. | Tu integración | Espera el retry-after (5 s) y reintenta, o sube de plan. |
| 502 | El portal del gobierno respondió mal o se cayó. | SAT/IMSS/Infonavit | Reintentar con backoff. |
| 504 | El portal del gobierno tardó demasiado (timeout). | SAT/IMSS/Infonavit | Reintentar más tarde. |
Campo code (taxonomía)
code | HTTP | Significado |
|---|---|---|
credencial | 422 · 400 | CIEC o e.firma incorrecta / vencida / revocada. Ojo: en los endpoints de PDF llega como 422; en Buzón y CFDI llega como 400 con el mismo code: "credencial". |
efirma_invalida | 422 | La contraseña no abre el .key, o el .cer no es un certificado válido. |
efirma_requerida | 400 | El documento exige e.firma y no se enviaron cer_b64/key_b64/password. |
sin_medios_contacto | 422 | (IMSS) El RFC no tiene correo/celular registrados en el Buzón IMSS. |
tecnico | 502 | Fallo técnico del portal del gobierno. Reintentable. |
timeout | 504 | El portal no respondió a tiempo. Reintentable. |
plan_gate | 403 | El servicio exige un plan superior (p. ej. Infonavit requiere Pro o superior). El cuerpo incluye min_plan y upgrade_url. |
key_scope_denied | 403 | Tu llave no tiene permiso para este servicio. El cuerpo incluye allowed_services. |
rfc_limit_reached | 403 | Alcanzaste el límite de RFCs de tu plan (ver Padrón de RFCs). |
tecnico | 503 | Infraestructura de límites caída (rate limiter). Reintenta tras el retry-after (2 s). |
rfc_registry_unavailable | 503 | Padrón de RFCs temporalmente no disponible. Reintenta tras el retry-after (5 s). |
expires_at), la API responde 401 igual que una llave revocada. Genera una nueva en el panel.{
"ok": false,
"code": "credencial",
"error": "La CIEC es incorrecta: el RFC o la contraseña CIEC no son válidos en el SAT."
}
error viene siempre en español y describe la causa de forma accionable (p. ej. "Tu e.firma está revocada", "La contraseña del portal Infonavit es incorrecta"). Puedes mostrarlo tal cual.Reintentos y timeouts
Los documentos que requieren navegar portales del gobierno (IMSS, Infonavit, Opinión SAT) tienen latencia variable porque dependen de servicios externos (validación OCSP del SAT, anti-bots, etc.). Diseña tu cliente para tolerarlo:
- Timeout del cliente ≥ 180 s para IMSS e Infonavit (no cortes a los 90 s — matarías corridas válidas).
- Reintenta sólo 5xx (502/504,
code: "tecnico"/"timeout") con backoff exponencial. No reintentes 4xx — son del lado del usuario. - Latencia típica por servicio:
| Servicio | Latencia típica | Notas |
|---|---|---|
| EFOS (listas negras) | ~0.15 s | Consulta en base de datos, instantánea. |
| Constancia / Opinión SAT | 7–20 s | e.firma más rápida que CIEC (sin captcha). |
| Buzón Tributario | 10–25 s | — |
| CFDI (masivo) | ~35 s | Un ZIP por periodo. |
| Opinión Infonavit | 12–90 s | Depende del OCSP del SAT. |
| Opinión IMSS | 18–100 s | Alta variabilidad por el OCSP del SAT. |
Modo asíncrono y webhooks
Los documentos que dependen de portales del gobierno pueden tardar de segundos a minutos. En vez de mantener la conexión abierta, agrega "async": true al cuerpo de cualquier endpoint de documento (/constancia, /opinion, /opinion-imss, /infonavit, /buzon, /cfdi) y la API responde de inmediato con un job_id:
curl -X POST https://subalans.com/api/v1/opinion \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "rfc": "XAXX010101000", "ciec": "TU_CIEC", "async": true }'
{
"ok": true,
"job_id": "job_a1b2c3d4e5f6a7b8c9d0",
"status": "queued",
"poll": "/api/v1/jobs/job_a1b2c3d4e5f6a7b8c9d0"
}
Consultar el resultado (polling)
GET /api/v1/jobs/{job_id} es la fuente de verdad y funciona en todos los planes. Estados: queued → running → done | failed. En done, el campo result trae la misma respuesta que el modo síncrono (en los endpoints de PDF, el documento viene en result.pdf_base64). Los resultados viven 24 horas.
{
"ok": true,
"job_id": "job_a1b2c3d4e5f6a7b8c9d0",
"status": "done",
"result": { "ok": true, "pdf_base64": "JVBERi0…", "sentido": "positiva" }
}
Si el trámite falla, status es failed y error trae el mensaje (mismo texto y semántica que los errores síncronos). Los fallos técnicos del portal se reintentan solos con backoff antes de darse por vencidos; los de credencial fallan a la primera (no tiene caso reintentar una CIEC incorrecta).
Webhooks (planes Pro y Scale)
Configura tu URL en el panel (sección Webhooks) o mándala por petición en webhook_url. Al terminar el job te enviamos un POST firmado:
POST https://tuservidor.com/hooks/subalans X-Subalans-Signature: t=1780000000,v1=5f8a… X-Subalans-Event-Id: evt_job_a1b2c3d4e5f6a7b8c9d0_done { "event": "job.completed", // o "job.failed" "job_id": "job_a1b2c3d4e5f6a7b8c9d0", "type": "ocsat", "status": "done", "result": { … }, "event_id": "evt_job_a1b2c3d4e5f6a7b8c9d0_done" }
Verificar la firma
El header X-Subalans-Signature es t=<epoch>,v1=<hex> donde v1 = HMAC-SHA256(secreto, t + "." + cuerpoCrudo). Rechaza cualquier webhook cuya firma no coincida — sin esto, cualquiera podría inyectarte resultados falsos.
const crypto = require("node:crypto"); function verificar(sigHeader, cuerpoCrudo, secreto) { const [, t, v1] = /t=(\d+),v1=([0-9a-f]+)/.exec(sigHeader) ?? []; if (!t) return false; const esperado = crypto.createHmac("sha256", secreto).update(t + "." + cuerpoCrudo).digest("hex"); return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(v1)); }
- Reintentos: si tu servidor no responde 2xx, reintentamos a 1 s, 10 s, 1 min y 10 min. Después de eso, recupera el resultado con
GET /api/v1/jobs/{id}. - Idempotencia: deduplica con
event_id— un mismo evento puede llegar más de una vez. - Responde rápido: regresa el 200 de inmediato y procesa después; cortamos la conexión a los 10 s.
async: true. El webhook te ahorra el polling; el polling siempre está ahí de respaldo.Modo prueba (sandbox) Disponible
El modo prueba te permite construir tu integración sin credenciales reales del contribuyente y sin tocar los portales del gobierno: con una llave sk_test_…, ciertos RFCs de prueba devuelven respuestas predefinidas — al instante y de forma determinista — para que puedas probar todos los caminos (éxito y cada error) antes de pasar a producción.
Cada RFC de prueba disparará un escenario distinto:
| RFC de prueba (ejemplo) | Respuesta simulada |
|---|---|
DEMO010101AA1 | 200 Documento de prueba (camino feliz). |
DEMO020202BB2 | 422 credencial — credencial incorrecta. |
DEMO030303CC3 | 422 sin_medios_contacto (IMSS). |
DEMO040404DD4 | 502 tecnico — para probar tus reintentos. |
EFOS010101AA1 | 200 (EFOS) Simula un RFC en lista 69-B definitivo. |
| Cualquier otro RFC válido | 200 Éxito — camino feliz, igual que DEMO010101AA1. |
- El PDF de prueba también llega binario (
Content-Type: application/pdf), igual que en producción — tu código de descarga se prueba tal cual. - El sandbox no aplica
plan_gateni el padrón de RFCs: puedes probar todos los servicios y RFCs sin restricciones. El único límite del modo prueba es la tasa: 10 req/min (ver Planes y límites).
sk_test_… en el panel de desarrolladores y úsala igual que la de producción. Las llamadas con llave de prueba no tocan los portales del gobierno ni consumen trámites reales.