Facturación API

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.

  1. Recibe tu llave de pruebas (fw_test_…) de parte de FactuWA.com.
  2. Verifica la conexión con GET /ping.
  3. Emite CFDI de prueba con POST /cfdi. No tienen validez fiscal ni consumen timbres.
  4. Cuando todo funcione, solicita tu llave de producción (fw_live_…).

URL base:

https://factuwa.com/api/public/v1
cURL
curl 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

GET/ping— Verifica la llave y el ambiente
GET/saldo— Timbres disponibles del emisor
POST/cfdi— Emite un CFDI 4.0
GET/cfdi/{id}— Consulta estado, UUID y total
GET/cfdi/{id}/pdf— Descarga el PDF
GET/cfdi/{id}/xml— Descarga el XML timbrado
POST/cfdi/{id}/cancelar— Solicita la cancelación ante el SAT

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

JSON · POST /cfdi
{
  "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
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.json
JavaScript (Node 18+)
const 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);
Python
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)
PHP
$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:

JSON
{ "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
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.

JSON
{ "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.

Node.js
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": "…"}}.

HTTPcodeQué hacer
400invalid_bodyRevisa `detalles` con el campo y el motivo
401unauthorizedLlave ausente, inválida o revocada
403live_disabled / client_suspendedProducción no habilitada o cuenta suspendida
404not_foundRuta o CFDI inexistente
409invalid_state / file_unavailableEl CFDI no está en un estado válido para la acción
422cfdi_rejectedLa validación fiscal o el timbrado rechazó el CFDI; ve `cfdi.error`
429rate_limitedLímite por minuto excedido (60 por defecto); reintenta en 60 s
500internal_errorReintenta 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.
Facturar