Documentación de la API
Versión 1. Todo lo necesario para emitir CFDI 4.0 desde tu plataforma, primero en pruebas y después en producción.
Inicio rápido
La API de FactuWA.com permite emitir CFDI 4.0 desde tu plataforma (tienda, ERP, app o portal). Usa JSON sobre HTTPS.
- Recibe tu llave de pruebas (
fw_test_…) de parte de FactuWA.com. - Verifica la conexión con
GET /ping. - Emite CFDI de prueba con
POST /cfdi. No tienen validez fiscal ni consumen timbres. - Cuando todo funcione, solicita tu llave de producción (
fw_live_…).
URL base:
https://factuwa.com/api/public/v1curl https://factuwa.com/api/public/v1/ping \
-H "Authorization: Bearer fw_test_TU_LLAVE"Autenticación y ambientes
Envía tu llave en el encabezado Authorization: Bearer <llave>. La llave determina el ambiente:
Pruebas · fw_test_
Valida con las mismas reglas que producción y simula el timbrado. UUID y PDF de prueba marcados "SIN VALIDEZ FISCAL". Sin costo.
Producción · fw_live_
Timbrado real ante el SAT con el sello digital (CSD) del emisor. Cada CFDI consume 1 timbre; si falla, se devuelve.
Nunca expongas tu llave en el navegador o en una app móvil. Llama a la API sólo desde tu servidor. Si una llave se filtra, pide su revocación de inmediato.
Endpoints
/ping— Verifica la llave y el ambiente/saldo— Timbres disponibles del emisor/cfdi— Emite un CFDI 4.0/cfdi/{id}— Consulta estado, UUID y total/cfdi/{id}/pdf— Descarga el PDF/cfdi/{id}/xml— Descarga el XML timbrado/cfdi/{id}/cancelar— Solicita la cancelación ante el SATEmitir un CFDI
Antes de timbrar validamos RFC, régimen, uso de CFDI, forma/método de pago, claves SAT e impuestos. Si algo no cumple, recibes 422 con el motivo y no se consume timbre.
{
"tipo": "I",
"receptor": {
"rfc": "XAXX010101000",
"nombre": "PUBLICO EN GENERAL",
"regimen": "616",
"cp": "06000",
"email": "cliente@ejemplo.com",
"whatsapp": "5215512345678"
},
"uso_cfdi": "S01",
"forma_pago": "03",
"metodo_pago": "PUE",
"tasa_iva": 0.16,
"conceptos": [
{ "descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 1000,
"clave_prod_serv": "81112100", "clave_unidad": "E48", "unidad": "Servicio" }
],
"enviar": ["email"],
"referencia": "PEDIDO-1001"
}curl -X POST https://factuwa.com/api/public/v1/cfdi \
-H "Authorization: Bearer fw_test_TU_LLAVE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: PEDIDO-1001" \
-d @factura.jsonconst res = await fetch("https://factuwa.com/api/public/v1/cfdi", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FACTUWA_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": pedido.id,
},
body: JSON.stringify(factura),
});
const { cfdi, error } = await res.json();
if (!res.ok) throw new Error(error?.message ?? cfdi?.error);
console.log(cfdi.uuid);import os, requests
r = requests.post("https://factuwa.com/api/public/v1/cfdi",
headers={"Authorization": f"Bearer {os.environ['FACTUWA_API_KEY']}",
"Idempotency-Key": "PEDIDO-1001"},
json=factura, timeout=60)
data = r.json()
print(r.status_code, data["cfdi"]["uuid"] if r.ok else data)$ch = curl_init("https://factuwa.com/api/public/v1/cfdi");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("FACTUWA_API_KEY"),
"Content-Type: application/json", "Idempotency-Key: PEDIDO-1001"],
CURLOPT_POSTFIELDS => json_encode($factura),
]);
$res = json_decode(curl_exec($ch), true);Respuesta 201:
{ "cfdi": { "id": "b5c1…", "ambiente": "pruebas", "estado": "timbrada",
"uuid": "5F1C…", "serie": "TEST", "folio": "482913", "total": 1160,
"receptor": { "rfc": "XAXX010101000", "nombre": "PUBLICO EN GENERAL", … },
"error": null, "referencia": "PEDIDO-1001", "creado": "…" } }Campos: tasa_iva 0, 0.08 o 0.16 · ret_iva y ret_isr como tasa (p. ej. 0.106667 y 0.10) · enviar manda PDF/XML al receptor por correo y/o WhatsApp (sólo producción).
Idempotencia
Envía siempre Idempotency-Key (p. ej. el id de tu pedido). Si repites la petición con la misma llave, devolvemos el mismo CFDI con el encabezado Idempotent-Replayed: true y nunca se timbra dos veces. Es seguro reintentar ante cortes de red o errores 500.
PDF, XML y cancelación
curl -o factura.pdf https://factuwa.com/api/public/v1/cfdi/{id}/pdf -H "Authorization: Bearer …"
curl -o factura.xml https://factuwa.com/api/public/v1/cfdi/{id}/xml -H "Authorization: Bearer …"
curl -X POST https://factuwa.com/api/public/v1/cfdi/{id}/cancelar -H "Authorization: Bearer …" \
-H "Content-Type: application/json" -d '{"motivo":"02"}'Motivos SAT: 01 con relación (requiere sustituto = UUID nuevo), 02 sin relación, 03 no se llevó a cabo, 04 nominativa en factura global. Si el SAT requiere aceptación del receptor, el estado queda cancelacion_pendiente.
Webhooks
Configuramos una URL https en tu cuenta y te enviamos POST con JSON cuando ocurre un evento: cfdi.timbrado, cfdi.error, cfdi.cancelado.
{ "id": "evt_…", "evento": "cfdi.timbrado", "ambiente": "produccion",
"creado": "2026-10-13T18:00:00Z", "data": { "id": "…", "uuid": "…", "estado": "timbrada", … } }Cada envío trae X-FactuWA-Signature: t=<unix>,v1=<hex>: HMAC-SHA256 de `${t}.${cuerpo}` con tu secreto whsec_…. Verifícalo y rechaza firmas con más de 5 minutos.
import crypto from "node:crypto";
function verificar(secret, cuerpoCrudo, header) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const esperado = crypto.createHmac("sha256", secret).update(`${t}.${cuerpoCrudo}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(v1));
}Responde 2xx en menos de 10 s. Si fallas, reintentamos con espera creciente hasta 6 veces. Usa id para ignorar duplicados.
Errores y límites
Todos los errores tienen la forma {"error": {"code": "…", "message": "…"}}.
| HTTP | code | Qué hacer |
|---|---|---|
| 400 | invalid_body | Revisa `detalles` con el campo y el motivo |
| 401 | unauthorized | Llave ausente, inválida o revocada |
| 403 | live_disabled / client_suspended | Producción no habilitada o cuenta suspendida |
| 404 | not_found | Ruta o CFDI inexistente |
| 409 | invalid_state / file_unavailable | El CFDI no está en un estado válido para la acción |
| 422 | cfdi_rejected | La validación fiscal o el timbrado rechazó el CFDI; ve `cfdi.error` |
| 429 | rate_limited | Límite por minuto excedido (60 por defecto); reintenta en 60 s |
| 500 | internal_error | Reintenta con la misma Idempotency-Key |
Costo, plazo y garantías
Facturación API es un servicio cotizable. El precio, el plazo de integración y las garantías se definen para cada cliente según su volumen, tipo de integración y nivel de soporte, y quedan por escrito en tu cotización formal (folio COT-…), que recibes por correo.
- Precios en MXN con IVA incluido; cada cobro se factura al momento.
- El ambiente de pruebas no tiene costo ni consume timbres.
- En producción, cada CFDI timbrado consume 1 timbre; si el timbrado falla, el timbre se devuelve.
- El plazo de integración corre a partir de que aceptas la cotización y recibes tu llave de pruebas.
¿Aún no tienes cotización? Solicítala en factuwa.com/facturacion-api.
Pase a producción
Cuando tus pruebas estén listas, pide el pase a producción a tu contacto de FactuWA.com. Validamos que tu emisor tenga su sello digital (CSD) vigente, datos fiscales completos y timbres disponibles, y te entregamos tu llave fw_live_. Sólo cambias la llave: la URL, los campos y los webhooks son los mismos, y desde ese momento los CFDI se timbran ante el SAT con validez fiscal.
- Emitiste en pruebas al menos un CFDI de cada tipo que usarás (ingreso, egreso, con retenciones).
- Manejas 422 mostrando el motivo a tu usuario.
- Envías Idempotency-Key en cada emisión.
- Verificas la firma de los webhooks.
- La llave está guardada sólo en tu servidor (variable de entorno).
- El emisor tiene su CSD vigente y timbres disponibles.