Extract your first document
Zero to a parsed invoice in about five minutes, including the part where the answer is wrong.
You will send two documents to the Sift API. The first one will work. The second one will come back wrong, on purpose, because the most important thing to learn about an extraction API is what it does when it is not sure.
Budget about five minutes.
Before you start
You need:
- A sandbox API key. Create one in the dashboard under Settings → API keys.
Sandbox keys start with
sk_test_, and usage on them is not billed. curl, or any HTTP client.uuidgen, or any other way to produce a unique string. EveryPOSTneeds anIdempotency-Key, and every new request needs a new one.
The two sample documents are hosted for this tutorial, so you can pass their URLs directly and skip the upload step for now.
Set your key as an environment variable so it does not end up in your shell history:
export SIFT_KEY="sk_test_your_key_here"
Step 1: Send a clean invoice
Start with a document that is easy: a digitally generated PDF invoice with selectable text and a conventional layout.
curl https://api.sandbox.sift.dev/v1/extractions \
-H "Authorization: Bearer $SIFT_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"document_url": "https://cdn.sift.dev/tutorial/invoice-clean.pdf",
"schema": "invoice"
}'
You get back 202 Accepted and a job, not an answer:
{
"id": "ext_Hq3Vx8Nc2T",
"object": "extraction",
"status": "processing",
"schema": "invoice",
"document_id": "doc_R7fTm2Wq9E",
"metadata": {},
"created_at": "2026-09-01T14:32:07Z"
}
Extraction is asynchronous. A two-page invoice usually finishes in under three seconds; a ninety-page contract does not. The API never blocks on the model, so the first response is always a receipt for the work, never the result.
Run the command twice and you get two extractions, because uuidgen produced a
different key each time. That is correct here. In your own code, generate the
key once per document and send the same key on every retry of that request.
Step 2: Retrieve the result
curl https://api.sandbox.sift.dev/v1/extractions/ext_Hq3Vx8Nc2T \
-H "Authorization: Bearer $SIFT_KEY"
{
"id": "ext_Hq3Vx8Nc2T",
"object": "extraction",
"status": "succeeded",
"schema": "invoice",
"schema_match": 0.98,
"document_id": "doc_R7fTm2Wq9E",
"fields": {
"invoice_number": {
"value": "INV-2026-004182",
"confidence": 0.99,
"page": 1
},
"total": {
"value": { "amount": 4500000, "currency": "INR" },
"confidence": 0.98,
"page": 1
},
"due_date": {
"value": "2026-10-01",
"confidence": 0.97,
"page": 1
}
},
"document": { "quality": { "score": 0.97, "issues": [] } },
"pages": 2,
"metadata": {},
"created_at": "2026-09-01T14:32:07Z",
"completed_at": "2026-09-01T14:32:09Z"
}
Three things to notice before you move on.
Every field carries its own confidence. There is no document-level score, because documents are not uniformly difficult. An invoice can have a crisp printed total and a handwritten due date, and one number averaging the two would hide exactly the field you need to check.
Money comes back as an integer in the currency's minor unit. 4500000 is
₹45,000.00. A float that is very slightly wrong is invisible on one invoice and
material across a month of them.
page is on every field. When a human has to verify a value, the page
number is the difference between a five-second check and a five-minute hunt.
In production you would not poll for this. Register a webhook and let
extraction.succeeded come to you. Polling works, but it is the fallback, not
the pattern.
Step 3: Send a harder one
Now the same call against a photographed receipt: taken on a phone, slightly skewed, with a faded thermal-printed total. It is a new request, so it gets a new key.
curl https://api.sandbox.sift.dev/v1/extractions \
-H "Authorization: Bearer $SIFT_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"document_url": "https://cdn.sift.dev/tutorial/receipt-photo.jpg",
"schema": "receipt"
}'
It returns 202 with a new extraction id, ext_Lb5Jr9Pd4M. Retrieve it the
same way as in step 2:
curl https://api.sandbox.sift.dev/v1/extractions/ext_Lb5Jr9Pd4M \
-H "Authorization: Bearer $SIFT_KEY"
{
"id": "ext_Lb5Jr9Pd4M",
"object": "extraction",
"status": "succeeded",
"schema": "receipt",
"schema_match": 0.95,
"document_id": "doc_Ws4Kn8Fb2Y",
"fields": {
"merchant": {
"value": "BLUE TOKAI COFFEE",
"confidence": 0.94,
"page": 1
},
"total": {
"value": { "amount": 38000, "currency": "INR" },
"confidence": 0.61,
"page": 1,
"alternatives": [
{ "value": { "amount": 88000, "currency": "INR" }, "confidence": 0.31 }
]
},
"purchased_at": {
"value": null,
"confidence": 0.0,
"page": 1
}
},
"document": { "quality": { "score": 0.58, "issues": ["blur", "skew"] } },
"pages": 1,
"metadata": {},
"created_at": "2026-09-01T14:35:09Z",
"completed_at": "2026-09-01T14:35:11Z"
}
The total is wrong. The receipt says ₹880.00. Sift returned ₹380.00 with a
confidence of 0.61, and put the correct answer in alternatives at 0.31. A
faded 8 on thermal paper looks like a 3.
Note what did not happen. status is still succeeded. There was no error,
no 4xx, no exception for your handler to catch. The extraction completed
exactly as designed and produced a wrong answer, and the only signals that
anything is off are numbers in the body.
This is the single most important behavior to internalize, and it is why this tutorial shows it in step 3 instead of in a note at the end. If you write your integration against step 2 and discover step 3 in production, you will have already paid a wrong invoice.
Step 4: Read what it told you
Sift flagged trouble in four places, and none of them was an error.
confidence: 0.61 on the total. Low for any field, and far below what you
want before paying anything. Deciding what to trust
recommends 0.98 for a total, and explains why a single threshold for every
field is the wrong tool.
The alternatives array on the same field, which is the stronger signal.
Its presence means the model found more than one plausible reading and had to
choose. A human with both candidates side by side resolves it in about two
seconds.
purchased_at, where value is null and confidence is exactly 0.0.
That is not low confidence; it is absence. The field was not found. Do not
treat 0.0 as a weak reading of something. There is nothing to read.
document.quality, at 0.58 with blur and skew flagged, against 0.97 and
no issues on the clean invoice. The API told you the capture was poor before
you looked at a single field.
What you should do with this
The instinct after step 3 is to raise a threshold and move on. That is half the answer, and the wrong half to start with.
A merchant name that is wrong is an annoyance. A total that is wrong is money. Those two fields should never share a threshold, and working out what each one costs when it is wrong is most of the design of a production integration.
Deciding what to trust walks through that, including the routing table most teams land on and the two mistakes that cost the most to undo later.
Next
- Deciding what to trust turns confidence scores into a routing decision your finance team will sign off on.
- POST /v1/extractions is the full contract for submission: every parameter, every field, every failure state.
- Register a webhook before you build anything real. Polling works and will quietly become your rate-limit problem.