Guía 1 de 2
Timbrar CFDIs con Savio
En este flujo Savio emite el CFDI por ti. Envías los datos estructurados de la factura, Savio la timbra ante el SAT y te devuelve el PDF y el XML.
Requisito previo
Sube tus certificados CSD
Requerido antes de poder timbrar
No es posible timbrar sin los certificados CSD, y este paso no se puede hacer desde la API.
En la app de Savio, ve a la configuración de tu organización y sube los archivos
.cer y .key emitidos por el SAT, junto con la contraseña de la llave. Luego completa el paso «Sincroniza con el SAT» que aparece después de subirlos — subir los archivos por sí solo no basta, ya que los certificados no se activan hasta que ese segundo paso se completa.Si lo omites,
POST
/invoice igual creará la factura, pero el timbrado fallará y la respuesta incluirá un cfdi_error.Paso 1
Crea un cliente
Toda factura pertenece a un cliente. Créalo con
POST
/customer.curl https://api-sandbox.savio.mx/api/v1/customer \-H "Authorization: $SAVIO_API_KEY" \-H "Content-Type: application/json" \-d '{"name": "Acme Manufacturing","legal_name": "ACME MANUFACTURING SA DE CV","tax_id": "AME010101AAA","tax_system": "601","default_payment_terms": "30","external_customer_id": "crm-4821","contacts": [{ "firstname": "Maria", "email": "maria@acme.mx" }]}'
tax_id(RFC) es obligatorio salvo que envíesvalidate_sat: false. Por defecto Savio valida el RFC y eltax_systemcontra el SAT y rechaza las incongruencias — esto previene fallas de timbrado más adelante.tax_systemes el código de régimen fiscal del SAT, p. ej.601(General de Ley Personas Morales) o612(Personas Físicas con Actividades Empresariales).- Se requiere al menos un contacto con
firstname. - Para clientes extranjeros usa un
address.countryde 3 letras distinto deMEXy el identificador fiscal extranjero en lugar del RFC genérico de público en general.
Paso 2 (opcional)
Define tus productos
Si facturas lo mismo con frecuencia, crea un catálogo de productos con
POST
/product para no repetir los códigos del SAT en cada factura.const product = await savio('/product', {name: 'Consultoria',description_template: 'Servicios de consultoria',product_key: '80101500',unit_key: 'E48',price: 1500,external_product_id: 'sku-consult',})
product_key es la clave de producto o servicio del SAT; unit_key es la clave de unidad.Todo o nada
En una misma factura, o todos los conceptos referencian un
product_id, o ninguno lo hace. Mezclar conceptos del catálogo con conceptos ad-hoc en la misma factura se rechaza.Si no llevas catálogo, omite este paso y describe cada concepto en línea — la siguiente sección muestra exactamente eso.
Paso 3a
Emite una factura PUE (pago inmediato)
Una factura PUE (Pago en una sola exhibición) se paga en el momento en que se emite. Envía
payment_info describiendo cómo se pagó y pon generate_pue: true.curl https://api-sandbox.savio.mx/api/v1/invoice \-H "Authorization: $SAVIO_API_KEY" \-H "Content-Type: application/json" \-d '{"external_customer_id": "crm-4821","external_invoice_id": "erp-inv-1001","currency": "MXN","items": [{"description": "Servicios de consultoria - julio 2026","unit_key": "E48","quantity": 1,"unit_price": 15000}],"payment_info": {"generate_pue": true,"payment_form": "03","use": "G03","date": "2026-07-20"},"email_info": { "send_email": true }}'
El error más común
generate_pue es obligatorio para timbrar. Enviar payment_info sin generate_pue: true registra el pago contra la factura pero no timbra ningún CFDI. Si esperabas un CFDI y no lo recibiste, revisa primero esta bandera.payment_form— forma de pago del SAT, p. ej.01(Efectivo),03(Transferencia),04(Tarjeta de crédito).use— uso del CFDI del SAT elegido por el receptor, p. ej.G03(Gastos en general).unit_priceyquantitydeben ser ambos mayores que cero.email_info.send_emailenvía la factura por correo a los contactos del cliente. Requierepayment_infooppd_info.
La respuesta incluye
invoice_id, cfdi_id y enlaces a los documentos timbrados (cfdi_pdf_url, cfdi_xml_url).Paso 3b
Emite una factura PPD (pago posterior)
Una factura PPD (Pago en parcialidades o diferido) se emite ahora y se paga después. Envía
ppd_info en lugar de payment_info.curl https://api-sandbox.savio.mx/api/v1/invoice \-H "Authorization: $SAVIO_API_KEY" \-H "Content-Type: application/json" \-d '{"external_customer_id": "crm-4821","external_invoice_id": "erp-inv-1002","currency": "MXN","payment_terms_days": 30,"items": [{"description": "Suministro de material","unit_key": "H87","quantity": 200,"unit_price": 85}],"ppd_info": { "use": "G01" }}'
payment_infoyppd_infoson mutuamente excluyentes. Enviar ambos se rechaza.- En una factura PPD la forma de pago siempre se timbra como
99(Por definir), como lo exige el SAT. Cualquierpayment_formque envíes se ignora. - Fija la fecha de vencimiento con
payment_terms_dayso con undate_dueexplícito. Si no envías ninguno, aplican losdefault_payment_termsdel cliente.
Tip
Prueba sin timbrar
Pon
is_preview: true dentro de payment_info o ppd_info para generar un PDF y un XML sin timbrar ante el SAT y sin guardar nada en Savio.const preview = await savio('/invoice', {external_customer_id: 'crm-4821',currency: 'MXN',items: [{ description: 'Prueba', unit_key: 'E48', quantity: 1, unit_price: 100 }],ppd_info: { use: 'G01', is_preview: true },})// preview.cfdi_pdf_url / preview.cfdi_xml_url - no se guardo nada
Paso 4
Registra un pago
Usa
POST
/payment para registrar el dinero recibido. Cada entrada de invoice_payments aplica parte del pago a una factura, así que un mismo pago puede liquidar varias facturas.curl https://api-sandbox.savio.mx/api/v1/payment \-H "Authorization: $SAVIO_API_KEY" \-H "Content-Type: application/json" \-d '{"external_payment_id": "bank-tx-9931","external_customer_id": "crm-4821","amount_paid": 17000,"currency": "MXN","payment_date": "2026-08-15","payment_form": "03","cfdi_type": "PAGO","invoice_payments": [{ "external_invoice_id": "erp-inv-1002", "amount": 17000 }]}'
cfdi_type: 'PAGO' es lo que timbra el complemento de pago. Omítelo (o envía null) para registrar el pago sin emitir un CFDI — lo correcto para facturas PUE, que ya están totalmente timbradas.Paso 5
Aplica una nota de crédito
Usa
POST
/credit con generate_credit_note: true para emitir una nota de crédito (CFDI de egreso) y aplicarla a las facturas.const credit = await savio('/credit', {external_credit_id: 'erp-cn-77',external_customer_id: 'crm-4821',amount_credit: 1000,currency: 'MXN',credit_date: '2026-08-20',payment_form: '03',generate_credit_note: true,invoice_credits: [{ external_invoice_id: 'erp-inv-1002', amount: 1000 }],})
Un crédito siempre debe estar en la misma moneda que las facturas a las que se aplica.
Solución de problemas
Cuando falla el timbrado
El timbrado ocurre dentro de la misma petición que crea la factura, pero ambos pueden tener éxito de forma independiente. Si el timbrado falla, la factura de todas formas se crea y la respuesta incluye un
cfdi_error que describe qué salió mal.const invoice = await savio('/invoice', { /* ... */ })if (invoice.cfdi_error) {// La factura existe en Savio. El CFDI no.// NO reintentes POST /invoice - crearias un duplicado.console.error('Fallo el timbrado:', invoice.cfdi_error, invoice.invoice_id)}
Causas comunes: certificados CSD faltantes, vencidos o nunca sincronizados con el SAT; un
use o payment_form inválido; una combinación de RFC/régimen que el SAT rechaza.Enviar
external_invoice_id te protege aquí: reenviar el mismo devuelve la factura existente en lugar de crear un duplicado.Siguientes pasos
- Cancela un CFDI timbrado con PUT
/cfdi, y consulta su estado de cancelación conGET/cfdi/status. - Suscríbete a los webhooks para recibir notificaciones cuando cambien los pagos, créditos y CFDIs.