Facturas a Excel
Extrae número, emisor, receptor, líneas e IVA de facturas en PDF o foto, valida totales y NIF y descarga un Excel por factura o por lote.
Pasar facturas a una hoja de cálculo a mano es lento y da errores. Con el tipo invoice Constaia extrae la cabecera,
las líneas y el desglose de IVA, comprueba que los totales cuadran y que los NIF tienen la letra correcta, y te
devuelve un XLSX listo para abrir.
Las opciones
{
"expect": "invoice",
"export": ["xlsx"],
"metadata": { "supplier_id": "88" }
}expect: "invoice": si llega otro tipo reconocido (por ejemplo, un justificante de pago), el veredicto esinvalidcontype_mismatch; si el documento no se reconoce,reviewcontype_unknown.export: ["xlsx"]: la respuesta incluye enexports.xlsxuna URL firmada para descargar el Excel, válida 24 horas. También puedes pedircsv,json,xmlopdf. Ver Exportaciones.checks.expected_amount(opcional): compara con eltotalde la factura. Útil si ya sabes cuánto debe sumar.
Campos y validaciones
| Campo | Ejemplo (fichero de test) |
|---|---|
number, issue_date | 20260042, 2026-09-01 |
seller { name, tax_id, address } | Add On Sport S.L., B12345674 |
buyer { name, tax_id, address } | Club Deportivo Arco Madrid, G12345674 |
lines[] { description, quantity, unit_price, discount_percent, amount, vat_rate } | Licencia anual, 2 × 50, IVA 21 |
tax_base | 100 |
vat[] { rate, base, amount } | 21 % sobre 100 = 21 |
withholding | retención, si la hay |
total, currency | 121, EUR |
Validaciones deterministas que aparecen en checks[]:
code | Qué comprueba |
|---|---|
invoice_totals | Que las líneas, la base, el IVA, la retención y el total cuadran. Si no, el mensaje dice qué importe no coincide. |
nif_check_digit | La letra o dígito de control de cada NIF/CIF (emisor y receptor). |
Si una falla, el veredicto pasa a invalid con un motivo del mismo code y severidad error. En facturas, invalid
por invoice_totals suele ser un error de lectura o una factura mal hecha: revísala antes de contabilizarla.
Una factura, un Excel
import { writeFile } from "node:fs/promises";
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia(); // lee CONSTAIA_API_KEY
try {
const analysis = await constaia.analyze(await fromPath("./invoice.pdf"), {
expect: "invoice",
export: ["xlsx"],
metadata: { supplier_id: "88" },
});
console.log(analysis.verdict?.status, analysis.fields.total?.value, analysis.fields.currency?.value);
for (const check of analysis.checks) console.log(check.code, check.passed, check.message);
// Opción A: la URL firmada de la respuesta (caduca en 24 h, no necesita clave)
const signed = await fetch(analysis.exports.xlsx);
await writeFile("./factura.xlsx", Buffer.from(await signed.arrayBuffer()));
// Opción B: generarlo cuando quieras con la clave (GET /v1/analyses/{id}/export?format=xlsx)
const res = await constaia.analyses.export(analysis.id, "xlsx");
await writeFile("./factura-2.xlsx", Buffer.from(await res.arrayBuffer()));
} catch (err) {
if (err instanceof ConstaiaError) console.error(err.code, err.message, err.requestId);
else throw err;
}GET /v1/analyses/{id}/export genera el fichero en el momento, así que funciona aunque la URL firmada haya caducado.
Necesita que el análisis haya terminado (si no, 409 analysis_not_completed) y que sus resultados se conserven: con
keep_results: false o tras borrarlo devuelve 404.
Cómo es el Excel
Una fila por documento. Las primeras columnas son comunes (id, fichero, tipo, etiqueta, confianza, veredicto, motivos,
avisos, fecha), después una columna por cada campo extraído y al final una por cada clave de metadata
(metadata.supplier_id). Los campos compuestos, como seller, lines o vat, van como JSON dentro de su celda.
Si necesitas una fila por línea de factura, construye tu hoja a partir de fields.lines.value en tu código.
Muchas facturas, un solo Excel
Con un lote, options.export se aplica al lote entero: cuando termina, exports.xlsx del lote
es un Excel combinado con una fila por factura. En los lotes no se generan exportaciones por documento.
curl https://api.constaia.com/v1/batches \
-H "Authorization: Bearer $CONSTAIA_API_KEY" \
-F "files[]=@facturas/invoice_001.pdf" \
-F "files[]=@facturas/invoice_002.pdf" \
-F "files[]=@facturas/invoice_003.pdf" \
-F 'options={"expect":"invoice","export":["xlsx"],"metadata":{"month":"2026-09"}}'La respuesta es 202 con el lote en processing. Cuando termina te llega el webhook batch.completed con
exports.xlsx; también puedes consultar GET /v1/batches/{id}. El flujo completo (webhook, más de 100 facturas,
reintentos) está en Procesamiento masivo con lotes.
Buzón mixto: clasificar primero
Si te llegan facturas mezcladas con otros documentos, puedes clasificar antes (0,2 créditos) y analizar solo lo que sea factura (1 crédito cada 2 páginas):
import { Constaia } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";
const constaia = new Constaia();
const file = await fromPath("./adjunto.pdf");
const classification = await constaia.classify(file, { expect: "invoice" });
if (classification.document?.type === "invoice") {
const analysis = await constaia.analyze(file, { expect: "invoice", export: ["xlsx"] });
}Si casi todo lo que llega son facturas, clasificar no compensa: analiza directamente con expect: "invoice" y descarta
las que den type_mismatch o type_unknown. Más en Qué endpoint usar.
Campos propios: extract con JSON Schema
Si necesitas datos que el tipo invoice no extrae (número de pedido, fecha de vencimiento, IBAN de pago), pasa tu propio
JSON Schema en extract. La respuesta trae solo los campos de tu esquema, así que incluye también los estándar que
quieras conservar.
{
"expect": "invoice",
"export": ["xlsx"],
"extract": {
"type": "object",
"properties": {
"number": { "type": "string" },
"issue_date": { "type": "string", "description": "Fecha de emisión, YYYY-MM-DD" },
"due_date": { "type": "string", "description": "Fecha de vencimiento, YYYY-MM-DD" },
"purchase_order": { "type": "string", "description": "Número de pedido del cliente" },
"payment_iban": { "type": "string", "description": "IBAN donde pagar la factura" },
"tax_base": { "type": "number" },
"vat": { "type": "array", "items": { "type": "object", "properties": {
"rate": { "type": "number" }, "base": { "type": "number" }, "amount": { "type": "number" } } } },
"total": { "type": "number" },
"currency": { "type": "string" }
}
}
}Las validaciones deterministas solo se ejecutan sobre los campos que existan: si quitas tax_base, vat o total,
invoice_totals no podrá comprobar los importes. Usa description para explicar el formato que esperas.
Probarlo
Con una clave ck_test_…, un PDF llamado invoice.pdf (o que contenga invoice o factura) devuelve la factura
20260042 de 2 licencias a 50 €, base 100, IVA 21 % y total 121 EUR, con invoice_totals y los dos nif_check_digit
superados. Más en Modo test.
Siguientes pasos
Conciliar justificantes de transferencia
Comprueba que cada justificante de transferencia trae el importe, el IBAN de tu cuenta y la referencia, uno a uno o en lotes con export a Excel.
Procesamiento masivo con lotes
Procesa cientos o miles de documentos con lotes de hasta 100, webhooks firmados, idempotencia, reintentos, límites de peticiones y export combinado.