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:

shell
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
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:

node
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.

202 response
{
  "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.

200 response
{
  "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.

document.needs_review
{
  "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.

node
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

MethodPathPurpose
POST/v1/documentsSubmit one document
POST/v1/batchesSubmit up to 500 documents
GET/v1/documents/{id}Fields, scores and route
POST/v1/documents/{id}/reviewResolve a held field by API
GET/v1/auditQuery the audit log

Next steps