 and a mock receipt photo → field cards (invoice_number, total, tax…). Designed.)
Fields to extract
| Field | Type / factory | Notes |
|---|---|---|
| invoice_number / order_number | text / textField() | Document number, can carry a span |
| issue date, due_date | date / dateField() | Calendar date |
| purchase_time | datetime / dateTimeField() | Date and time |
| total, subtotal, tax, tip, unit_price | number / numberField({ decimals: 2 }) | decimals is 0–6 |
| quantity | number / numberField({ integer: true }) | Whole number |
| contacts / line item names | list / listField({ items: 'text' }) | items is required: text, number or date |
| days_to_pay | estimate / estimateField({ min, max }) | Returns median, p10, p90 and more |
Text invoice example (INV-2291)
This is the invoice example from the Clairic SDK docs. Install the SDK:
bashnpm install @system-one-ai/core@0.8.0 @system-one-ai/adapter-system-one@0.7.0 @system-one-ai/transport-fetch@0.8.0
Save as extract-invoice.mts and set SYSTEM_ONE_API_KEY in .env:
tsimport {
createSystemOne, defineFields, textField, numberField, dateField, listField, estimateField,
} from '@system-one-ai/core';
import { systemOneAdapter } from '@system-one-ai/adapter-system-one';
import { createFetchTransport } from '@system-one-ai/transport-fetch';
const apiKey = process.env.SYSTEM_ONE_API_KEY;
if (!apiKey) throw new Error('Set SYSTEM_ONE_API_KEY.');
const client = createSystemOne({
adapter: systemOneAdapter,
transport: createFetchTransport(),
apiKey,
timeoutMs: 30_000,
maxRetries: 0,
});
const fields = defineFields({
invoice_number: textField({ instructions: 'The invoice id printed on the document.' }),
due_date: dateField({ instructions: 'Issue date plus the net payment days.' }),
total: numberField({ decimals: 2, instructions: 'The total amount due.' }),
contacts: listField({ items: 'text' }),
days_to_pay: estimateField({ min: 0, max: 120, step: 1 }),
});
const result = await client.extract({
model: 'clairic',
state: [
'INVOICE INV-2291',
'From: Northwind Supply',
'To: Contoso Ltd',
'Issued: 2026-09-02, net 30',
'Total due: 1080.00 including shipping',
'Contact: Dana Ruiz, Priya Shah',
].join('\n'),
fields,
referenceDate: '2026-10-19',
options: { includeQuantiles: false },
});
const invoice: string | null = result.answers.invoice_number.value;
console.log({
model: result.model,
invoice,
dueDate: result.answers.due_date.value,
total: result.answers.total.value,
contacts: result.answers.contacts.value,
daysToPay: result.answers.days_to_pay.median,
truncated: result.truncated,
usage: result.usage,
});
Run it with node --env-file=.env extract-invoice.mts. Every run creates a new billable request.
contacts is a list, so its value is an array of names. days_to_pay is an estimate field, so read its median; p10 and p90 come back too.

Skewed, blurry receipt photos
In an example from the official blog, Clairic reads a receipt photo taken at an angle with hard-to-read text and pulls the order number, purchase time, quantity, unit price, subtotal, tax and tip. The receipt shows no discount, so the discount field returns null.
Here's the photo version, saved as extract-receipt.mts. You can send up to 4 images per request:
tsimport { readFile } from 'node:fs/promises';
import {
createSystemOne, defineFields, textField, numberField, dateTimeField,
type ImageInput,
} from '@system-one-ai/core';
import { systemOneAdapter } from '@system-one-ai/adapter-system-one';
import { createFetchTransport } from '@system-one-ai/transport-fetch';
const apiKey = process.env.SYSTEM_ONE_API_KEY;
const imagePath = process.argv[2];
if (!apiKey || !imagePath) throw new Error('Set SYSTEM_ONE_API_KEY and supply a JPEG path.');
const images: readonly ImageInput[] = [{
mediaType: 'image/jpeg',
base64: (await readFile(imagePath)).toString('base64'),
}];
const client = createSystemOne({
adapter: systemOneAdapter,
transport: createFetchTransport(),
apiKey,
timeoutMs: 30_000,
maxRetries: 0,
});
const fields = defineFields({
order_number: textField({ instructions: 'The order or receipt number.' }),
purchase_time: dateTimeField({ instructions: 'Date and time of purchase.' }),
quantity: numberField({ integer: true }),
unit_price: numberField({ decimals: 2 }),
subtotal: numberField({ decimals: 2 }),
tax: numberField({ decimals: 2 }),
tip: numberField({ decimals: 2 }),
discount: numberField({ decimals: 2, instructions: 'Discount amount, only if the receipt shows one.' }),
});
const result = await client.extract({ model: 'clairic', state: '', images, fields });
for (const [name, answer] of Object.entries(result.answers)) {
console.log(name, answer.value, answer.confidence, answer.pNull);
}
When the receipt has no discount, discount comes back null. Before production, test with your own photos and set thresholds on confidence and pNull.

referenceDate and due dates
referenceDate (reference_date over HTTP) sets the year for dates written without one, such as a receipt that only prints the day and month.
Due dates can be computed. In the invoice example, the due_date field's instructions say "Issue date plus the net payment days.", so Clairic works out the due date from 2026-09-02 and net 30.
Missing fields return null
When pNull is above the null threshold (0.5 by default), the value is null, and guess may still hold something. Adjust the threshold for the whole request with options.nullThreshold, or per field.
The invoice extraction demo shows this: when the total due says "to be confirmed", the amount comes back null and the invoice number is still extracted.
Pricing in one line
Clairic is billed once per request based on input size, not per field; see the Clairic model page. New accounts get 100 free credits on sign-up.
Try the demo
Try the same invoice task in the invoice extraction demo (sign-in required). To call the API directly, this is the HTTP body for POST https://system-one.dev/v1/systemone:
json{
"model": "clairic",
"state": "INVOICE INV-2291\nIssued: 2026-09-02, net 30\nTotal due: 1080.00",
"reference_date": "2026-10-19",
"fields": {
"invoice_number": { "type": "text" },
"total": { "type": "number", "decimals": 2 }
}
}
See the API docs for authentication, idempotency and headers. Retrying the same operation with the same key doesn't create a second charge while the replay record remains.

Related reading
Last verified: 2026-10-22