Integrar facturación electrónica en México suele implicar XML, certificados, reglas fiscales y la disponibilidad de un PAC. Con Ipsofactura, tu aplicación se conecta a una sola API y la plataforma se encarga del timbrado CFDI 4.0.
En esta guía vas a autenticarte, enviar una factura de ingreso, interpretar la respuesta y dejar tu integración lista para operar de forma segura en producción. Todos los ejemplos funcionan con Node.js 18+ y Python 3.9+.
https://sandbox.api.ipsofactura.com. Los CFDI generados ahí no tienen validez fiscal y no consumen timbres de producción.El flujo completo
Tu backend prepara los datos fiscales, llama a Ipsofactura y recibe el CFDI timbrado. La misma integración funciona en producción cambiando la base URL y la API key.
Antes de empezar
Necesitas lo siguiente:
- Una cuenta de Ipsofactura con acceso al sandbox.
- Una API key de sandbox. Se envía en el header
x-api. - Una empresa emisora y su Certificado de Sello Digital registrados.
- Node.js 18 o superior, o Python 3.9 o superior.
1. Configura el cliente
Guarda tus credenciales en variables de entorno. Los ejemplos usan IPSO_API_KEY y apuntan al sandbox.
const API_KEY = process.env.IPSO_API_KEY;
const BASE_URL = 'https://sandbox.api.ipsofactura.com';
if (!API_KEY) {
throw new Error('Falta IPSO_API_KEY');
}import os
API_KEY = os.environ["IPSO_API_KEY"]
BASE_URL = "https://sandbox.api.ipsofactura.com"2. Construye la factura
Este ejemplo emite un CFDI de ingreso por $1,000 MXN más IVA. Los RFC son datos de prueba del SAT; sustitúyelos por los datos de tu operación cuando pases a producción.
{
"rfc_emisor": "EKU9003173C9",
"tipo_comprobante": "I",
"forma_pago": "03",
"metodo_pago": "PUE",
"moneda": "MXN",
"exportacion": "01",
"subtotal": 1000.00,
"total": 1160.00,
"receptor": {
"rfc": "URE180429TM6",
"nombre": "UNIVERSIDAD ROBOTICA ESPANOLA",
"domicilio_fiscal": "65000",
"regimen_fiscal": "601",
"uso_cfdi": "G03"
},
"conceptos": [{
"clave_prod_serv": "84111506",
"cantidad": 1,
"clave_unidad": "ACT",
"descripcion": "Servicio de desarrollo de software",
"valor_unitario": 1000.00,
"importe": 1000.00,
"objeto_imp": "02",
"impuestos": {
"traslados": [{
"base": 1000.00,
"impuesto": "002",
"tipo_factor": "Tasa",
"tasa_o_cuota": "0.160000",
"importe": 160.00
}]
}
}],
"impuestos": {
"total_impuestos_trasladados": 160.00,
"traslados": [{
"base": 1000.00,
"impuesto": "002",
"tipo_factor": "Tasa",
"tasa_o_cuota": "0.160000",
"importe": 160.00
}]
}
}
601 es el régimen General de Ley Personas Morales, G03 es Gastos en general y 002 identifica IVA. Valida estos valores antes de enviar el request.3. Timbra el CFDI
Envía el objeto a POST /cfdi/timbrar. La cabecera Idempotency-Key evita que un reintento accidental genere una segunda operación.
import { readFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
const factura = JSON.parse(
await readFile('factura.json', 'utf8')
);
const response = await fetch(`${BASE_URL}/cfdi/timbrar`, {
method: 'POST',
headers: {
'x-api': API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID(),
},
body: JSON.stringify(factura),
});
const data = await response.json();
if (!response.ok) {
throw new Error(`${data.code}: ${data.message}`);
}
console.log('UUID:', data.uuid);
console.log('CFDI:', data.id);import json
import uuid
import requests
with open("factura.json", encoding="utf-8") as archivo:
factura = json.load(archivo)
response = requests.post(
f"{BASE_URL}/cfdi/timbrar",
headers={
"x-api": API_KEY,
"Idempotency-Key": str(uuid.uuid4()),
},
json=factura,
timeout=30,
)
data = response.json()
if not response.ok:
raise RuntimeError(f"{data['code']}: {data['message']}")
print("UUID:", data["uuid"])
print("CFDI:", data["id"])Respuesta exitosa
{
"id": "6c5c64d8-f9ce-4c26-8cb7-5e6db3a1ab31",
"uuid": "6128396F-C09B-4EC6-8699-43DA5A244971",
"folio": "1",
"fecha_timbrado": "2026-10-07T18:42:07",
"rfc_pac": "AAA010101AAA",
"pac_provider": "SandboxPacHandler",
"numero_certificado_sat": "30001000000500003416",
"sello_cfdi": "SANDBOX_sello_cfdi_...",
"sello_sat": "..."
}
Guarda tanto id como uuid. El primero identifica el recurso dentro de Ipsofactura; el segundo es el folio fiscal del CFDI ante el SAT.
id puedes descargar el XML y el PDF, consultar el estatus o iniciar una cancelación desde los endpoints correspondientes.Idempotencia: reintentar sin duplicar
Un timeout no significa necesariamente que el timbrado falló: la respuesta pudo perderse después de que el PAC certificara el CFDI. Por eso cada operación debe tener una llave de idempotencia estable.
orden-8472La conexión expira
orden-8472Devuelve el mismo resultado
Genera la llave a partir de un identificador único de tu negocio —por ejemplo, el ID de la factura o de la orden— y reutilízala en todos los intentos de esa misma operación. No generes una llave nueva dentro del ciclo de reintentos.
async function timbrar(factura, facturaId) {
return fetch(`${BASE_URL}/cfdi/timbrar`, {
method: 'POST',
headers: {
'x-api': API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': `factura-${facturaId}`,
},
body: JSON.stringify(factura),
});
}def timbrar(factura, factura_id):
return requests.post(
f"{BASE_URL}/cfdi/timbrar",
headers={
"x-api": API_KEY,
"Idempotency-Key": f"factura-{factura_id}",
},
json=factura,
timeout=30,
)Manejo de errores y reintentos
No todos los errores se resuelven igual. Corrige los errores de datos antes de repetir la llamada y reserva los reintentos automáticos para rate limits y fallas temporales.
| HTTP | Qué significa | Acción | ¿Reintentar? |
|---|---|---|---|
| 400 | Payload o dato fiscal inválido | Corrige el campo indicado | No |
| 401 | API key inválida o revocada | Verifica la credencial y el ambiente | No |
| 402 | Saldo insuficiente | Agrega timbres a la cuenta | No |
| 404 | Empresa o CFDI no encontrado | Valida el identificador | No |
| 409 | Conflicto con un recurso existente | Consulta el recurso creado | No |
| 422 | El SAT o el PAC rechazó el CFDI | Lee el código fiscal y corrige | No |
| 429 | Límite de solicitudes alcanzado | Respeta Retry-After | Sí |
| 500 | Error interno temporal | Reintenta con backoff | Sí |
| 503 | Servicio temporalmente no disponible | Reintenta con la misma llave | Sí |
Qué hace el enrutamiento multi-PAC
Tu integración no necesita conocer la API particular de cada PAC. Ipsofactura valida la solicitud y la envía a una ruta disponible. Si una ruta no puede atenderla antes de confirmar el timbrado, la plataforma puede seleccionar otra sin cambiar el contrato de tu aplicación.
El failover no sustituye la idempotencia de tu lado. Conserva el mismo identificador durante toda la operación para que un timeout ambiguo pueda reconciliarse sin riesgo de duplicidad.
Sandbox y producción
| Sandbox | Producción | |
|---|---|---|
| Base URL | sandbox.api.ipsofactura.com | api.ipsofactura.com |
| Credencial | API key de pruebas | API key de producción |
| RFC y CSD | Datos de prueba del SAT | Datos reales del emisor |
| Validez fiscal | No | Sí |
| Costo | Sin consumo de timbres | $0.10 MXN + IVA por timbre |
Checklist para salir a producción
- La API key está en un gestor de secretos y nunca llega al frontend.
- El CSD del emisor está vigente y corresponde al RFC configurado.
- Los importes, impuestos y catálogos del SAT se validan antes del request.
- Cada factura tiene una llave de idempotencia estable.
- Los errores 400 y 422 se guardan para corrección, no se reintentan en ciclo.
- Los errores temporales usan backoff exponencial y un máximo de intentos.
- Tu sistema conserva el
id, eluuidy el XML timbrado. - Probaste el flujo completo de timbrado, descarga, consulta y cancelación.
Empieza a timbrar hoy
Prueba la integración en sandbox y lleva tu primer CFDI de código a producción con una sola API.
Solicitar acceso a Ipsofactura →