Guía 2 de 2

Importar facturas desde XML

Usa este flujo cuando tus CFDIs ya se timbraron en otro lado — tu ERP, otro PAC o tu contador — y quieres que Savio los registre y los cobre.
Envía un XML de CFDI ya timbrado a POST /invoice/from-cfdi. Sin cliente fijado, Savio lo busca por el RFC del receptor y crea el cliente si es nuevo; con uno fijado, el RFC del receptor debe coincidir. La factura registrada luego acepta pagos y créditos importados desde sus propios XML.
Requisitos previos

Nada que configurar

No necesitas certificados CSD para este flujo, porque Savio nunca timbra nada; solo lee documentos que tú ya emitiste.
Paso 1

Importa una factura

Envía el XML del CFDI a
POST
/invoice/from-cfdi
.
# jq arma el cuerpo JSON para escapar el XML correctamente como string
jq -Rs '{ xml_text: ., external_invoice_id: "erp-inv-1001" }' cfdi-1001.xml \
| curl https://api-sandbox.savio.mx/api/v1/invoice/from-cfdi \
-H "Authorization: $SAVIO_API_KEY" \
-H "Content-Type: application/json" \
-d @-
  • El XML va en el cuerpo JSON como un string en xml_text. No es una carga de archivo multipart.
  • El límite de tamaño del cuerpo para este endpoint es de 10 MB.
  • Savio lee todo del XML: emisor, receptor, conceptos, impuestos, totales, moneda y la forma/método de pago. No vuelves a capturar nada de eso.
  • La fecha de vencimiento sale de los términos de pago del cliente. Anúlala con due_date.
  • Envía status: 'unready' para importar la factura sin que entre a cobranza todavía.

Cómo se manejan los clientes

Por defecto, los clientes se crean por ti. Si omites customer_id y external_customer_id, Savio busca el RFC del receptor entre tus clientes existentes y, si no encuentra coincidencia, crea el cliente, la dirección y el contacto a partir del nodo del receptor en el XML.
Para asociar la factura a un cliente que ya conoces, fíjalo de forma explícita:
await savio('/invoice/from-cfdi', {
xml_text,
external_invoice_id: 'erp-inv-1001',
external_customer_id: 'crm-4821',
})
Cuando fijas un cliente, el RFC de ese cliente debe coincidir con el RFC del receptor en el XML, o la petición se rechaza con RFC_MISMATCH. Un cliente fijado ya debe tener un contacto — Savio no creará uno en este caso.
Lo que envíasResultado
Ni customer_id ni external_customer_idSe busca por RFC; se crea desde el XML si es nuevo
external_customer_id (o customer_id)Se usa tal cual; el RFC del receptor debe coincidir

Importar varias veces de forma segura

Las importaciones son idempotentes por dos vías independientes, así que los reintentos y las cargas históricas traslapadas son seguros:
  • external_invoice_id — reenviar uno que ya usaste devuelve la factura existente en lugar de crear una segunda.
  • UUID del CFDI — Savio también deduplica por el sat_uuid del XML. Si ese UUID ya se importó, obtienes INVOICE_ALREADY_EXISTS junto con existing_invoice_id.
En una carga histórica
Trata INVOICE_ALREADY_EXISTS como éxito, no como error.
Paso 2

Importa pagos

Los complementos de pago se importan con
POST
/payment/from-cfdi
, con la misma forma.
const xml_text = await fs.readFile('./complemento-9931.xml', 'utf8')
const payment = await savio('/payment/from-cfdi', {
xml_text,
external_payment_id: 'bank-tx-9931',
})
// -> { payment_id, cfdi_id, cfdi_xml_url }
Savio lee del XML a qué facturas aplica el complemento y cuánto va a cada una, y luego lo aplica a las facturas correspondientes. Importa las facturas antes que sus complementos para que las referencias resuelvan.
Paso 3

Importa notas de crédito

Las notas de crédito (CFDIs de egreso) se importan con
POST
/credit/from-cfdi
.
const xml_text = await fs.readFile('./nota-credito-77.xml', 'utf8')
const credit = await savio('/credit/from-cfdi', {
xml_text,
external_credit_id: 'erp-cn-77',
})

Todo junto: una carga histórica típica

El orden importa — primero las facturas, luego los documentos que las referencian:
for (const file of invoiceXmls) {
await savio('/invoice/from-cfdi', {
xml_text: await fs.readFile(file, 'utf8'),
external_invoice_id: idFrom(file),
})
}
for (const file of paymentXmls) {
await savio('/payment/from-cfdi', {
xml_text: await fs.readFile(file, 'utf8'),
external_payment_id: idFrom(file),
})
}
Como ambos endpoints deduplican, puedes volver a ejecutar esto de forma segura tras una falla.

Siguientes pasos

  • Concilia los pagos importados con los movimientos bancarios usando
    GET
    /bank-transaction
    .
  • Suscríbete a los webhooks para mantener tu sistema sincronizado.