Constaia
Guías por caso

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

options.json
{
  "expect": "invoice",
  "export": ["xlsx"],
  "metadata": { "supplier_id": "88" }
}
  • expect: "invoice": si llega otro tipo reconocido (por ejemplo, un justificante de pago), el veredicto es invalid con type_mismatch; si el documento no se reconoce, review con type_unknown.
  • export: ["xlsx"]: la respuesta incluye en exports.xlsx una URL firmada para descargar el Excel, válida 24 horas. También puedes pedir csv, json, xml o pdf. Ver Exportaciones.
  • checks.expected_amount (opcional): compara con el total de la factura. Útil si ya sabes cuánto debe sumar.

Campos y validaciones

CampoEjemplo (fichero de test)
number, issue_date20260042, 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_base100
vat[] { rate, base, amount }21 % sobre 100 = 21
withholdingretención, si la hay
total, currency121, EUR

Validaciones deterministas que aparecen en checks[]:

codeQué comprueba
invoice_totalsQue 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_digitLa 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

invoice-to-xlsx.js
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.

Terminal
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):

inbox.js
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.

options.json
{
  "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

En esta página