Docs / Getting started
Quickstart
Send a document, read what the agent extracted, and handle the one field it was not sure about. About ten minutes.
Before you start
You need a Tessera workspace and a workflow with a schema. The examples use a workflow called ap-invoices. Every request goes to https://api.tessera.example/v1 over HTTPS.
Template note. This host and every identifier on this page are fictional. Replace them with your own API when you adapt the template.
1. Create an API key
In the workspace, open Settings, API keys and create a key with the documents:write scope. Keys are shown once. Store it in an environment variable:
export TESSERA_KEY="tsk_live_..." 2. Send a document
Post the file as multipart form data and name the workflow that should read it. The Idempotency-Key header makes a retried request safe: the same key returns the same document instead of creating a duplicate.
curl https://api.tessera.example/v1/documents \
-H "Authorization: Bearer $TESSERA_KEY" \
-H "Idempotency-Key: inv-20418" \
-F workflow=ap-invoices \
-F [email protected] The same request from Node 20 or later:
import { readFile } from 'node:fs/promises';
const form = new FormData();
form.set('workflow', 'ap-invoices');
form.set('file', new Blob([await readFile('inv-20418.pdf')]), 'inv-20418.pdf');
const res = await fetch('https://api.tessera.example/v1/documents', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.TESSERA_KEY}`, 'Idempotency-Key': 'inv-20418' },
body: form,
});
const doc = await res.json();
console.log(doc.id, doc.status); The API answers immediately with 202 Accepted and the document id. Reading takes a few seconds.
{
"id": "doc_8f31c04a",
"workflow": "ap-invoices",
"status": "processing",
"received_at": "2026-10-19T14:02:11Z"
} 3. Read the response
Fetch the document with GET /v1/documents/{id}, or wait for the document.routed webhook. Every field has a value and a confidence. Fields under your threshold carry held: true and a reason you can show to a person.
{
"id": "doc_8f31c04a",
"status": "needs_review",
"fields": {
"supplier": { "value": "Halvorsen Supply Co.", "confidence": 0.99 },
"invoice_no": { "value": "INV-20418", "confidence": 0.99 },
"total": { "value": 12480.00, "currency": "USD", "confidence": 0.99 },
"bank_account": { "value": "...4417", "confidence": 0.62, "held": true,
"reason": "The 15th digit is smudged on the page" }
},
"route": null
} Until the held field is confirmed, route stays null: nothing is posted anywhere on an unconfirmed value.
4. Handle review
Register a webhook endpoint in Settings, Webhooks and subscribe to document.needs_review. Tessera sends this payload as soon as a field is held, so you can notify the right person in your own tools. The built-in review lane works without any webhook.
{
"type": "document.needs_review",
"created": "2026-10-19T14:02:15Z",
"data": {
"document": "doc_8f31c04a",
"fields": ["bank_account"],
"review_url": "https://app.tessera.example/review/doc_8f31c04a",
"due_at": "2026-10-19T16:02:15Z"
}
} 5. Verify webhooks
Each delivery is signed with your endpoint secret. Verify the signature on the raw request body before you trust the payload, and reject anything older than five minutes.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
// Header looks like: t=1760882535,v1=5c1d...e9
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return ok && fresh;
} Endpoints at a glance
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/documents | Submit one document |
| POST | /v1/batches | Submit up to 500 documents |
| GET | /v1/documents/{id} | Fields, scores and route |
| POST | /v1/documents/{id}/review | Resolve a held field by API |
| GET | /v1/audit | Query the audit log |