Constaia
Use-case guides

Invoices to Excel

Extract number, seller, buyer, lines and VAT from PDF or photographed invoices, validate totals and tax IDs and download Excel per invoice or batch.

Cette page n'est pas encore traduite dans votre langue. Voici la version anglaise.

Typing invoices into a spreadsheet by hand is slow and error-prone. With the invoice type Constaia extracts the header, the lines and the VAT breakdown, checks that the totals add up and that Spanish tax IDs (NIF/CIF) have a correct check character, and gives you an XLSX ready to open.

Options

options.json
{
  "expect": "invoice",
  "export": ["xlsx"],
  "metadata": { "supplier_id": "88" }
}
  • expect: "invoice": if another recognised type arrives (a payment receipt, for example), the verdict is invalid with type_mismatch; if the document isn't recognised, review with type_unknown.
  • export: ["xlsx"]: the response includes a signed URL in exports.xlsx to download the Excel file, valid for 24 hours. You can also ask for csv, json, xml or pdf. See Exports.
  • checks.expected_amount (optional): compares with the invoice total. Useful if you already know the amount.

Fields and validations

FieldExample (test file)
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, VAT 21
tax_base100
vat[] { rate, base, amount }21 % on 100 = 21
withholdingwithholding tax, if any
total, currency121, EUR

Deterministic validations reported in checks[]:

codeWhat it checks
invoice_totalsThat lines, tax base, VAT, withholding and total add up. If not, the message says which amount doesn't match.
nif_check_digitThe check character of each Spanish NIF/CIF (seller and buyer).

If one fails, the verdict becomes invalid with a reason of the same code and severity error. For invoices, invalid because of invoice_totals is usually a misread or a badly made invoice: review it before booking it.

One invoice, one 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(); // reads CONSTAIA_API_KEY

try {
  const analysis = await constaia.analyze(await fromPath("./invoice.pdf"), {
    expect: "invoice",
    export: ["xlsx"],
    language: "en",
    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);

  // Option A: the signed URL in the response (expires in 24 h, no key needed)
  const signed = await fetch(analysis.exports.xlsx);
  await writeFile("./invoice.xlsx", Buffer.from(await signed.arrayBuffer()));

  // Option B: generate it whenever you want with the key (GET /v1/analyses/{id}/export?format=xlsx)
  const res = await constaia.analyses.export(analysis.id, "xlsx");
  await writeFile("./invoice-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 generates the file on the fly, so it works even after the signed URL has expired. The analysis must be finished (otherwise 409 analysis_not_completed) and its results must be kept: with keep_results: false or after deleting it, it returns 404.

What the Excel looks like

One row per document. The first columns are common (id, file, type, label, confidence, verdict, reasons, warnings, date; headers are currently in Spanish), then one column per extracted field and finally one per metadata key (metadata.supplier_id). Compound fields such as seller, lines or vat are written as JSON in their cell.

If you need one row per invoice line, build your own sheet from fields.lines.value in your code.

Many invoices, one Excel

With a batch, options.export applies to the whole batch: when it finishes, the batch's exports.xlsx is a combined Excel with one row per invoice. Batches don't generate per-document exports.

Terminal
curl https://api.constaia.com/v1/batches \
  -H "Authorization: Bearer $CONSTAIA_API_KEY" \
  -F "files[]=@invoices/invoice_001.pdf" \
  -F "files[]=@invoices/invoice_002.pdf" \
  -F "files[]=@invoices/invoice_003.pdf" \
  -F 'options={"expect":"invoice","export":["xlsx"],"metadata":{"month":"2026-09"}}'

The response is 202 with the batch processing. When it finishes you get the batch.completed webhook with exports.xlsx; you can also call GET /v1/batches/{id}. The full flow (webhook, more than 100 invoices, retries) is in Bulk processing with batches.

Mixed inbox: classify first

If invoices arrive mixed with other documents, you can classify first (0.2 credits) and only analyze what is an invoice (1 credit per 2 pages):

inbox.js
import { Constaia } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const constaia = new Constaia();
const file = await fromPath("./attachment.pdf");

const classification = await constaia.classify(file, { expect: "invoice" });
if (classification.document?.type === "invoice") {
  const analysis = await constaia.analyze(file, { expect: "invoice", export: ["xlsx"] });
}

If nearly everything is an invoice, classifying isn't worth it: analyze directly with expect: "invoice" and discard the ones with type_mismatch or type_unknown. More in Which endpoint to use.

Your own fields: extract with JSON Schema

If you need data the invoice type doesn't extract (purchase order, due date, payment IBAN), pass your own JSON Schema in extract. The response contains only the fields in your schema, so also include the standard ones you want to keep.

options.json
{
  "expect": "invoice",
  "export": ["xlsx"],
  "extract": {
    "type": "object",
    "properties": {
      "number": { "type": "string" },
      "issue_date": { "type": "string", "description": "Issue date, YYYY-MM-DD" },
      "due_date": { "type": "string", "description": "Due date, YYYY-MM-DD" },
      "purchase_order": { "type": "string", "description": "Customer purchase order number" },
      "payment_iban": { "type": "string", "description": "IBAN to pay the invoice to" },
      "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" }
    }
  }
}

Deterministic validations only run on fields that exist: if you drop tax_base, vat or total, invoice_totals can't check the amounts. Use description to explain the format you expect.

Test it

With a ck_test_… key, a PDF named invoice.pdf (or containing invoice or factura) returns invoice 20260042 for 2 licences at 50 €, tax base 100, VAT 21 % and total 121 EUR, with invoice_totals and both nif_check_digit passed. More in Test mode.

Next steps

Sur cette page