Skip to content

Invoice Desk (Node.js)

Updated

What it does#

Drop invoices, extract headers and line items, check that the numbers add up, flag anything that does not, correct it with a note, export CSV or JSON.

Source: invoice-desk in the examples repository.

Invoice Desk with one validated invoice and two flagged for review
Invoice Desk with one validated invoice and two flagged for review

Run it#

Requires Node 20+. No database.

cd invoice-desk
npm ci
npm start

Open http://127.0.0.1:3101 and choose Explore sample data. Three synthetic invoices load: one validates, one has a line whose quantity times price does not equal its amount, and one has no tax amount at all.

The SDK calls#

The whole backend is one file, server.js, with three routes. Only two of them touch DocXtract.

Connect lists the templates the key can use. It costs no credits.

import { DocXtract } from '@docxtract/sdk';

const dx = new DocXtract({ apiKey, maxRetries: 2 });
const models = await dx.models();

Extract writes the upload to a private temp folder, calls the SDK, and removes the folder whatever happens.

folder = await mkdtemp(path.join(tmpdir(), 'invoice-desk-'));
const filename = path.join(folder, 'document' + ext);
await writeFile(filename, bytes, { mode: 0o600 });

const options = { document_type: settings.documentType || 'invoice', store_db: false };
if (settings.model?.trim()) options.model = settings.model.trim();

try {
  const result = await dx.extract(filename, options);
  return { raw: result.data, meta: result.meta, complete: result.complete };
} finally {
  if (folder) await rm(folder, { recursive: true, force: true });
}

extract() handles multi-page PDFs itself, so the app has no chunking code. See Multi-page documents for what it does underneath.

The third route, analyse, runs the validation rules against extracted JSON. It makes no API call, which is why corrections in the UI are free.

The validation rules#

The interesting part of the app is not the extraction. It is what happens after.

  • Each line: quantity × unit price − line discount = line amount.
  • Line amounts sum to the subtotal.
  • Subtotal + tax − document discount + shipping + rounding = total.
  • Missing invoice number, vendor, currency, line items or required totals send the invoice to review. A missing tax is unknown, not zero. An explicit zero is accepted.
  • Header tax can be read directly or summed from cgst, sgst, igst and vat when they are present. A percentage string is not a tax amount.
  • The same vendor and invoice number appearing twice in the workspace is flagged.
  • The default tolerance is 0.01 currency units, configurable from 0 to 100.

The sample data is chosen to show why line-level checks matter: one invoice has a subtotal and total that agree while a single line is off by 50.00.

Acme review showing a 50.00 line difference and correction note
Acme review showing a 50.00 line difference and correction note

Field mapping#

The default mapping expects invoice_number, vendor.name, currency, line_items, subtotal, tax and total, with common aliases such as items, qty, unit_price and grand_total. If your template uses other keys, map them in Settings. Paths are relative to the SDK's data object.

{"number":"Header.InvoiceNumber","vendor":"Header.SupplierName","items":"LineItems","quantity":"Qty","rate":"UnitPrice","total":"Totals.GrandTotal"}

Where things are#

FilePurpose
server.jsHTTP server, the three routes, SDK calls, temp-file cleanup
includes/invoices.jsNormalisation and arithmetic checks
includes/config.jsPort, upload limit, defaults
includes/*.phpHTML-only view fragments. Node concatenates them, so no PHP runtime is needed
tests/invoices.test.jsRules, rounding, strict number parsing, mappings, cleanup
npm test
Tip
Watch the request guard at the top of server.js. It refuses anything not from localhost, and checks a custom header and the Origin. That is enough for a local tool. A hosted app needs real authentication in front of the extract route, because every call spends your credits.