Ir al contenido

Cómo timbrar un CFDI completo

Conecta una venta de tu sistema con un CFDI de ingreso: guarda al cliente, crea un borrador, agrega sus conceptos y confirma el timbrado. Al terminar tendrás el ID de FactuCat, el UUID fiscal y los archivos de la factura.

El recorrido es: cuenta preparada → cliente → borrador → conceptos → revisión → timbrado → archivos. Cada paso usa datos guardados en el anterior. Ejecuta los bloques en orden y continúa únicamente cuando la solicitud termine correctamente.

Empieza en sandbox (sin tarjeta ni CSD propio)

Sección titulada «Empieza en sandbox (sin tarjeta ni CSD propio)»

Para este recorrido usa el ambiente de pruebas. El emisor es tu cuenta; el receptor es el cliente al que facturas. En sandbox no necesitas CSD propio, plan de pago ni tarjeta para el primer timbrado de prueba.

  1. Regístrate en sandbox.factucat.com.
  2. Crea una llave fc_test_... en Configuración → API keys con customer:read, customer:write, invoice:read, invoice:write e invoice:stamp.
  3. Sandbox precarga el CSD de prueba del SAT EKU9003173C9 (persona moral, régimen 601) cuando la cuenta aún no tiene CSD. Los datos fiscales de ese CSD compartido son de solo lectura en sandbox.

No necesitas enviar el CSD ni dar de alta un emisor en cada solicitud. La API pública actual no tiene endpoints para cargar CSD o configurar emisores. La presentación del PDF también se configura en la webapp.

Cuando el flujo de prueba funcione de punta a punta, pasa a producción con tu propio emisor (sección al final de esta guía).

Necesitas los datos fiscales del receptor, el uso de CFDI que solicita, qué vendiste, cantidad, precio sin impuestos, clasificación SAT, impuestos aplicables y cómo se pagó. No necesitas crear un producto en FactuCat: los conceptos se agregan directamente al borrador.

El ejemplo ilustra un servicio nacional de programación de aplicaciones por $1,000 MXN antes de IVA, pagado totalmente por transferencia al facturar. Supone un emisor persona moral del régimen general y una operación con IVA del 16%, sin retenciones, cuyo receptor solicita G03. Si tu operación es distinta, ajusta los datos fiscales y los impuestos antes de timbrar.

Elige cURL, Node.js, Python o Go en cualquier ejemplo. La selección se mantiene en los demás pasos y guías. Cada bloque es un programa completo: guárdalo y ejecútalo por separado, siempre desde el mismo directorio que los archivos JSON. No pegues varios programas Go en un mismo archivo.

Cliente Requisito Cómo ejecutar cada bloque
cURL cURL y jq sh ejemplo.sh
Node.js Node.js 22+ con fetch nativo node ejemplo.mjs
Python Python 3.10+, biblioteca estándar python3 ejemplo.py
Go Go 1.22+, biblioteca estándar go run ejemplo.go

Para probar el recorrido, prepara tu cuenta en sandbox y guarda su llave fc_test_... en la variable de entorno FACTUCAT_API_KEY de tu servidor. Antes de ejecutar los programas, define la URL de pruebas en la terminal:

Variables compartidas por los cuatro clientes
export FACTUCAT_API_URL='https://sandbox.factucat.com/api/v1'

Los programas guardan las respuestas de cliente y borrador en archivos JSON; los pasos siguientes leen sus IDs automáticamente (UUID ya URL-safe). No hay reintentos automáticos. Si falla una solicitud, revisa el error y la sección de recuperación antes de continuar. En Node.js también puedes usar el SDK de FactuCat.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
curl --fail-with-body --request GET \
--url "$FACTUCAT_API_URL/me" \
--header "X-API-Key: $FACTUCAT_API_KEY"

Un 200 comprueba la conexión y muestra los permisos. En sandbox el CSD de prueba ya está disponible; en producción un 200 no valida por sí solo que tu CSD y datos fiscales estén listos para timbrar.

La URL anterior es sandbox: los comprobantes de prueba no tienen validez fiscal ni generan cargos por timbres. El checklist de producción (CSD propio, plan y tarjeta) va al final de esta guía.

Si tu sistema ya guardó el customerId de este cliente, reutilízalo y pasa al siguiente paso. No crees un cliente por cada venta. Crear y reutilizar clientes explica cómo localizarlo si no tienes su ID.

Para uno nuevo, prepara cliente.json. Estos valores son ficticios y no sirven para timbrar: reemplázalos por los datos reales de tu cliente, tal como están registrados ante el SAT.

cliente.json
{
"legalName": "EMPRESA DE EJEMPLO",
"rfc": "AAA010101AAA",
"postalCode": "06000",
"taxRegime": "601",
"sendInvoices": false,
"invoiceLanguage": "es",
"currency": "MXN"
}

RFC, nombre, régimen y código postal son datos requeridos del receptor en CFDI 4.0. Consulta los requisitos del SAT.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
curl --fail-with-body --request POST \
--url "$FACTUCAT_API_URL/customers" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--header 'Content-Type: application/json' \
--data @'cliente.json' \
--output 'cliente-respuesta.json'

La respuesta 201 devuelve el cliente en data. Guarda data.id junto al ID de ese cliente en tu base de datos. El ejemplo conserva esta respuesta en cliente-respuesta.json. Crear el registro no certifica sus datos ante el SAT ni emite una factura.

En este recorrido tu integración entregará los archivos. Por eso se usa sendInvoices: false y más adelante se desactiva explícitamente el envío al timbrar.

El programa lee data.id de cliente-respuesta.json y lo envía como customerId. Si reutilizas un cliente, consulta y guarda primero su respuesta en ese mismo archivo.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
CUSTOMER_ID="$(jq -er '.data.id' 'cliente-respuesta.json')"
curl --fail-with-body --request POST \
--url "$FACTUCAT_API_URL/invoice-drafts" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--header 'Content-Type: application/json' \
--data "{\"currency\":\"MXN\",\"cfdiUse\":\"G03\",\"exportacion\":\"01\",\"customerId\":\"$CUSTOMER_ID\"}" \
--output 'borrador-respuesta.json'

El 201 devuelve data.id y data.status: "draft". FactuCat copia los datos del cliente al receptor del borrador. Comprueba también data.customerId y data.receiverRfc para confirmar que vinculaste al cliente correcto.

Guarda data.id junto a tu venta antes de continuar. Los ejemplos lo conservan en borrador-respuesta.json y lo reutilizan en cada paso. Será el mismo ID después del timbrado. El borrador todavía no tiene UUID fiscal ni genera un timbre.

Guarda estos datos en comprobante.json para el caso de esta guía:

comprobante.json
{
"cfdiUse": "G03",
"exportacion": "01",
"paymentMethod": "PUE",
"paymentForm": "03",
"currency": "MXN"
}

paymentMethod describe cuándo se paga; paymentForm, cómo. Aquí PUE corresponde al pago total al facturar y 03 a transferencia. G03 es el uso solicitado por el receptor; 01 indica que no aplica exportación. El uso debe ser compatible con su régimen fiscal. Orientación del SAT sobre forma y método de pago.

Si tu venta sigue pendiente de pago, este ejemplo de PUE no corresponde. Un flujo de pago diferido necesita su tratamiento fiscal y complementos de pago; la API pública actual no expone la emisión de esos complementos.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
ID="$(jq -er '.data.id' 'borrador-respuesta.json')"
curl --fail-with-body --request PATCH \
--url "$FACTUCAT_API_URL/invoice-drafts/$ID/meta" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--header 'Content-Type: application/json' \
--data @'comprobante.json'

El 200 devuelve el borrador actualizado. Envía estos campos explícitamente para que los datos de tu venta determinen el comprobante.

Guarda el concepto en concepto.json. Para este servicio ilustrativo se usan 81111504 —servicios de programación de aplicaciones— y E48 —unidad de servicio—. Elige la clasificación que describa tu venta real: estas claves no son una configuración general para cualquier servicio.

concepto.json
{
"productCode": "81111504",
"unitCode": "E48",
"description": "Servicio de programación de aplicaciones",
"quantity": 1,
"unitPrice": 1000,
"vatRate": 0.16,
"isrWithholdingRate": 0
}

unitPrice es el precio antes de impuestos. Las tasas se expresan como fracciones: 0.16 significa 16%. Los impuestos explícitos corresponden al caso descrito; no los reutilices para operaciones con retenciones, exención u otro tratamiento.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
ID="$(jq -er '.data.id' 'borrador-respuesta.json')"
curl --fail-with-body --request POST \
--url "$FACTUCAT_API_URL/invoice-drafts/$ID/items" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--header 'Content-Type: application/json' \
--data @'concepto.json'

La respuesta devuelve el borrador completo, con sus conceptos en data.items y los totales recalculados. Guarda el id de cada concepto si necesitas corregirlo. Para otra partida, envía otra solicitud con sus datos.

No envíes subtotal, taxTotal o total como campos editables: FactuCat los calcula. Los catálogos de productos y unidades no tienen endpoints en esta API; conserva las claves de tus productos en tu propio sistema.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
ID="$(jq -er '.data.id' 'borrador-respuesta.json')"
curl --fail-with-body --request GET \
--url "$FACTUCAT_API_URL/invoice-drafts/$ID" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--output 'revision.json'

Revisa data antes de avanzar:

Parte del borrador Qué confirmar
Receptor receiverRfc, receiverLegalName, receiverPostalCode y receiverTaxRegime corresponden al cliente
Comprobante cfdiUse, paymentMethod, paymentForm, currency y exportacion describen la venta
Conceptos items contiene todas las partidas, con claves SAT, cantidad, precio e impuestos correctos
Totales En este ejemplo: subtotal: 1000, taxTotal: 160, total: 1160

Esta consulta no es una validación previa ante el SAT. Las validaciones de emisión se ejecutan al timbrar.

Si necesitas corregir algo, modifica este mismo borrador:

  • Receptor: PATCH /invoice-drafts/{id}/receiver. Envía los campos receiver* a corregir; cambiar solo customerId en este endpoint no copia automáticamente los datos del nuevo cliente.
  • Comprobante: PATCH /invoice-drafts/{id}/meta.
  • Concepto: PATCH /invoice-drafts/{id}/items/{itemId}; para quitarlo, DELETE sobre esa ruta.

Usa una clave nueva para cada corrección y vuelve a consultar los totales. Los campos admitidos están en la referencia API.

Con el borrador revisado, prepara timbrado.json. Estas opciones dejan la entrega de archivos a cargo de tu integración:

timbrado.json
{
"skipIssuedEmailSend": true,
"sendToCustomerContacts": false,
"sendCustomerEmail": false,
"sendCustomerWhatsApp": false
}

Si el borrador no tiene issueDate y no lo envías al timbrar, el CFDI se emite con la fecha y hora del timbrado. Si el borrador ya tiene issueDate, timbrar sin el campo conserva esa fecha. Para emitirlo con una fecha anterior, envía issueDate (YYYY-MM-DD o datetime ISO) en POST /invoice-drafts, en PATCH /invoice-drafts/{id}/meta o en el cuerpo del timbrado (timbrado.json).

  • El SAT permite que la fecha del CFDI (Fecha) sea de hasta 72 horas antes del timbrado y nunca futura, contadas en la zona horaria del código postal de expedición (LugarExpedicion); lo valida el PAC según la regla 2.7.2.9, fracción I, de la Resolución Miscelánea Fiscal 2026. En la práctica puedes elegir hoy o hasta 3 días de calendario atrás, siempre que al timbrar no hayan pasado más de 72 horas.
  • Una fecha inválida o fuera de rango responde 400 con error.code: "INVALID_ISSUE_DATE".
  • La fecha se valida otra vez al timbrar. La hora del CFDI es la del timbrado, recorrida al día elegido; en el tercer día hacia atrás se ajusta a 71 h 50 min antes del timbrado (reservamos 10 minutos por el reloj del PAC), así que ese día deja de aceptarse en los últimos 10 minutos de cada día.
  • En el SDK es el mismo campo: createDraft, setDraftMeta y stampDraft aceptan issueDate. En el CLI usa --fecha (ver CLI). En el MCP, draft_create, draft_set_meta e invoice_stamp aceptan issue_date, y la previa de invoice_stamp muestra issue_date, fecha y time_zone.

Este paso emite el CFDI y genera consumo en producción. Conserva el ID del borrador antes de enviarlo.

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
ID="$(jq -er '.data.id' 'borrador-respuesta.json')"
curl --fail-with-body --request POST \
--url "$FACTUCAT_API_URL/invoice-drafts/$ID/stamp" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--header 'Content-Type: application/json' \
--data @'timbrado.json' \
--output 'timbrado-respuesta.json'

Confirma data.status: "issued" y data.satUuid presente en la respuesta exitosa. Guarda data.id, data.satUuid y data.issuedAt: id se usa en las rutas de FactuCat y satUuid identifica el CFDI ante el SAT.

La factura emitida se consulta en GET /invoices/{id}. El endpoint de borradores deja de devolverla una vez emitida; un 404 allí no significa que se perdió.

Descarga primero el XML:

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
ID="$(jq -er '.data.id' 'borrador-respuesta.json')"
curl --fail-with-body --request GET \
--url "$FACTUCAT_API_URL/invoices/$ID/xml" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--output 'xml-respuesta.json'

Luego, su representación en PDF:

Ventana de terminal
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
ID="$(jq -er '.data.id' 'borrador-respuesta.json')"
curl --fail-with-body --request GET \
--url "$FACTUCAT_API_URL/invoices/$ID/pdf" \
--header "X-API-Key: $FACTUCAT_API_KEY" \
--output 'pdf-respuesta.json'

Ambas respuestas son JSON con data.fileName, data.mimeType y data.base64. Decodifica los dos archivos de respuesta desde el mismo directorio. La opción cURL usa jq y la utilidad base64:

Ventana de terminal
for extension in xml pdf; do
jq -er '.data.base64' "$extension-respuesta.json" | base64 --decode > "factura.$extension"
done

Pon ambos archivos a disposición del cliente desde tu sistema. El XML timbrado es el comprobante fiscal; el PDF es su representación. Si falla una descarga, repite ese GET, sin volver a timbrar. Más sobre archivos y cancelaciones.

El checklist de emisor propio aplica después de validar el flujo en sandbox. En producción la cuenta corresponde a un solo RFC emisor y la preparación se hace en la webapp:

Debes tener guardado Dónde se configura Para qué se usa
RFC, nombre fiscal, régimen y código postal del emisor Configuración → Datos fiscales Identificar al negocio que emite
CSD vigente: .cer, .key y contraseña de la llave Configuración → Datos fiscales Sellar los CFDI de ese mismo RFC
Plan FactuCat API o Miau Pro habilitado Configuración → Suscripción Acceder a la API; el plan API requiere tarjeta registrada
API key fc_live_... de tu integración Configuración → API keys Autenticar las solicitudes hacia producción

Usa https://factucat.com/api/v1 y una llave fc_live_.... El timbrado en producción emite CFDI con validez fiscal y genera consumo. Consulta Ambientes y pruebas.

Referencia Cuándo guardarla Para qué te sirve
Tu ID del cliente ↔ customerId Al crear o localizar al cliente Reutilizarlo en otras ventas
Tu ID de venta ↔ id del documento Al crear el borrador Continuar el mismo flujo tras un reinicio
Clave de idempotencia, ruta y body de cada mutación Antes de cada solicitud Reintentar exactamente la misma operación
satUuid, status e issuedAt Al confirmar la emisión Registrar el resultado fiscal
XML y PDF Después del timbrado Entregarlos y conservarlos según tu operación
meta.requestId o error.requestId En cada respuesta Identificar una solicitud al investigar una falla

Los archivos locales permiten seguir este tutorial. En tu integración, persiste estas referencias en tu base de datos; una variable de terminal no sobrevive a un reinicio.

Situación Cómo continuar
401 o 403 Revisa llave, ambiente y permisos; en producción, también el acceso del plan
Cliente con RFC ya registrado Localiza el registro y reutiliza su ID
Datos inválidos o CSD pendiente Corrige la configuración o el mismo borrador; usa una clave nueva para la solicitud corregida
Timeout o conflicto al timbrar Consulta GET /invoices/{id} con el ID guardado; no crees otro borrador
Documento issued con satUuid Guarda la confirmación y continúa con los archivos
Timbrado pendiente o resultado desconocido Conserva documento y evidencia; espera su conciliación antes de decidir un nuevo intento

Idempotency-Key es opcional. Si la usas, reutilízala solo para la misma ruta, método y body. Un error de red no demuestra que no se timbró, y ver todavía un estado de borrador tampoco garantiza que el PAC haya rechazado la operación. Idempotencia y recuperación.