Data Extraction API
The extract endpoint pulls fields you name out of unstructured text — invoices, emails, resumes, support tickets, contracts — and returns them as typed JSON. There is no schema to train and no template to maintain: describe the fields in the request and the endpoint fills in the ones the document actually states.
Nearly every value is grounded: the model has to quote the span of the text a value came from, and
the server verifies that quote really occurs in the document. Anything it cannot point at is reported
missing instead of guessed. The one exception is boolean fields, whose true/false answers are
judgments about the text rather than spans in it and so are not span-verified — see Tips.
Values are also coerced to the type you declare, so total comes back as 1299.0 rather than
"$1,299.00", and dates come back as YYYY-MM-DD.
Where the sentiment and tone endpoints answer "what kind of text is this?", extract answers "what is in it?". For a value that is nowhere stated in the document and has to be inferred or condensed, use summarize instead — see Tips.
Sample Code
- cURL
- JavaScript
- Python
- Python (Sapling client)
curl -X POST https://api.sapling.ai/api/v1/extract \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.", "fields": ["invoice_number", {"name": "total", "type": "number", "description": "Amount due", "required": true}, {"name": "due_date", "type": "date"}, "purchase_order"], "context": "A vendor invoice"}'
import axios from 'axios';
async function run(text) {
try {
const response = await axios.post(
'https://api.sapling.ai/api/v1/extract',
{
key: '<api-key>',
text,
fields: [
'invoice_number',
{name: 'total', type: 'number', description: 'Amount due', required: true},
{name: 'due_date', type: 'date'},
'purchase_order',
],
context: 'A vendor invoice',
},
);
const {status, data} = response;
console.log({status});
console.log(JSON.stringify(data, null, 4));
} catch (err) {
const msg = err.response?.data?.msg || err.message;
console.log({err: msg});
}
}
run('Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.');
import requests
from pprint import pprint
response = requests.post(
"https://api.sapling.ai/api/v1/extract",
json={
"key": "<api-key>",
"text": "Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.",
"fields": [
"invoice_number",
{"name": "total", "type": "number", "description": "Amount due", "required": True},
{"name": "due_date", "type": "date"},
"purchase_order",
],
"context": "A vendor invoice",
}
)
if 200 <= response.status_code < 300:
pprint(response.json())
else:
print('Error: ', response.status_code, response.text)
from sapling import SaplingClient
api_key = '<api-key>'
client = SaplingClient(api_key=api_key)
text = 'Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.'
fields = [
'invoice_number',
{'name': 'total', 'type': 'number', 'description': 'Amount due', 'required': True},
{'name': 'due_date', 'type': 'date'},
'purchase_order',
]
result = client.extract(text, fields, context='A vendor invoice')
# `data` is a plain {name: value} map with an entry for every requested field
print(result['data']['invoice_number'], result['data']['total'])
# `fields` keeps the request order and carries the quoted evidence span
for entry in result['fields']:
if entry['found']:
print(f"{entry['name']}: {entry['value']} <- {entry['evidence']}")
print('Not stated in the text:', result['missing']) # ['purchase_order']
Sample Response
{
"data": {
"invoice_number": "INV-1042",
"total": 1299.0,
"due_date": "2026-03-05",
"purchase_order": null
},
"fields": [
{"name": "invoice_number", "type": "string", "value": "INV-1042",
"evidence": "Invoice INV-1042", "found": true},
{"name": "total", "type": "number", "value": 1299.0,
"evidence": "total $1,299.00", "found": true},
{"name": "due_date", "type": "date", "value": "2026-03-05",
"evidence": "due March 5, 2026", "found": true},
{"name": "purchase_order", "type": "string", "value": null,
"evidence": "", "found": false}
],
"missing": ["purchase_order"]
}
A document that yields nothing is still a successful 200: data is all null, every found is
false, and missing lists all of your field names.
Batch Requests
To run the same extraction over many short documents — a folder of receipts, a queue of form
submissions — in one request, send texts (a list of 1–10 strings) instead of text. The same
fields and context apply to every item, and the combined length of all items may be up to 10,000
characters (the same cap as a single text). Exactly one of text and texts must be provided.
curl -X POST https://api.sapling.ai/api/v1/extract \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "texts": ["Invoice INV-1042 for Acme Corp.", "Invoice INV-2077 for Globex."], "fields": ["invoice_number", "customer_name"]}'
The batch response is {"results": [...]} with one entry per input, in input order; each entry has
exactly the single-response shape above (data, fields, missing):
{
"results": [
{
"data": {"invoice_number": "INV-1042", "customer_name": "Acme Corp"},
"fields": [
{"name": "invoice_number", "type": "string", "value": "INV-1042",
"evidence": "Invoice INV-1042", "found": true},
{"name": "customer_name", "type": "string", "value": "Acme Corp",
"evidence": "Acme Corp", "found": true}
],
"missing": []
},
{
"data": {"invoice_number": "INV-2077", "customer_name": "Globex"},
"fields": [
{"name": "invoice_number", "type": "string", "value": "INV-2077",
"evidence": "Invoice INV-2077", "found": true},
{"name": "customer_name", "type": "string", "value": "Globex",
"evidence": "Globex", "found": true}
],
"missing": []
}
]
}
Items are billed individually (a batch of N costs the same as N single requests), and each item is
cached the same way as a single request — batches and single calls share the cache. If any item fails
to extract, the whole request returns a 502 and nothing is billed; the items that did succeed are
already cached, so a retry only re-runs the failures.
Request Parameters
POST to https://api.sapling.ai/api/v1/extract
key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.
text: String
The document to extract from, up to 10,000 characters (measured on the raw input, before any HTML is
stripped). Plain text or HTML — tags are stripped server-side. Text that is empty after stripping
returns a 400. The text is treated as untrusted data: instructions inside it are ignored and only
its content is extracted from. Provide exactly one of text and texts.
texts: List[String]
Batch form (see Batch Requests): 1–10 documents to extract from in one request, each
treated exactly like text above, with a combined length of up to 10,000 characters. An item that is
empty after stripping returns a 400 naming its index. The response becomes {"results": [...]}, one
entry per item in input order.
fields: List[String | Object]
The fields to look for: between 1 and 20 entries. Each entry is either a string (the field name) or an
object with the keys below; any other key returns a 400. Names are at most 50 characters, non-empty,
and case-insensitively unique across the list.
nameString — the field name, echoed back as the key indata.typeString, optional, defaults tostring— one ofstring,number,integer,boolean,dateorlist. Unknown types return a400.descriptionString, optional — up to 200 characters saying what this field means; the cheapest way to fix a field that is being read wrongly.requiredBoolean, optional, defaults to false — a hint that the field is expected to be present. It never forces a value: a required field that the text does not state is still reported inmissingrather than guessed.
context: String, optional
Up to 500 characters describing what the document is and how to read ambiguous fields, for example
"A vendor invoice; total means the amount due including tax". Guidance only — it can never add a
field outside fields.
Response Parameters
data: Object
A convenience {field name: value} map. Every requested field is present, with null where the
document did not state it, so you can index it without checking for missing keys.
fields: List[Object]
One entry per requested field, in the order you requested them. Each has:
name: the field name as submitted.type: the type the value was coerced to (stringwhen you did not declare one).value: the extracted value, ornullwhen not found.evidence: the verbatim span oftextthe value was taken from, or""when not found. Useful for highlighting the source in your own UI and for spot-checking results.found: whether a value was extracted.
missing: List[String]
The names of the fields the document did not yield, in request order. Empty when everything was found.
Types
type | Value in the response |
|---|---|
string | The extracted text. |
number | A float. Currency symbols, thousands separators and trailing units are stripped ("$1,299.00" → 1299.0). |
integer | A whole number; a value with a fractional part is reported missing rather than rounded. |
boolean | true or false. |
date | Always YYYY-MM-DD. "March 5, 2026", "5 March 2026" and ISO timestamps all normalize; ambiguous numeric forms like 03/04/2026 are deliberately not guessed. |
list | A list of strings, deduplicated, at most 20 items. |
A value that will not convert to the declared type is reported missing rather than returned in the
wrong type, so you never have to type-check data yourself.
Errors
| Status | Meaning |
|---|---|
400 | Validation error — missing or oversized text, text empty after stripping, both or neither of text/texts provided, an invalid or over-length texts batch, context over 500 characters, or invalid fields (empty, more than 20, a duplicate or oversized name, an unknown type, or an unrecognized key inside a field object). The body is {"msg": "..."}. |
401 / 403 | Missing or invalid API key. |
429 | Rate limit exceeded or key over capacity. Each call draws at least 100 tokens from the burst bucket. |
502 | {"msg": "Unexpected error extracting from text."} — the extraction model failed; safe to retry. Not billed and not cached. |
Tips
- Describe the ambiguous fields. A bare name works for
invoice_number, but a one-linedescriptionis what separatestotal(amount due, including tax) fromsubtotal, orstart_datefromsigned_date. This is usually the first thing to reach for when a field comes back wrong. - Declare types.
{"name": "amount", "type": "number"}gets you a float you can sum, and"type": "date"gets you an ISO date you can sort — no parsing on your side, and values that do not convert are reported missing rather than returned in a shape your code did not expect. - Use
listfor repeated items. Line items, attendees, skills, ticket tags — onelistfield beatsitem_1,item_2,item_3, and each item is grounded in the text individually. - Extraction is not inference. Values must be stated in the document; the grounding check discards
anything the model cannot quote.
booleanfields are the exception (a true/false answer such as{"name": "is_urgent", "type": "boolean"}is a judgment about the text rather than a span in it). For a value that has to be inferred or condensed rather than quoted, use summarize. - Treat
missingas normal. Documents vary, and an all-nullresult is a successful response, not an error. Branch onfound(or onmissing) rather than assuming every field will be filled, and userequiredto mark the fields whose absence should raise a flag in your own workflow. - Show the evidence. When a person reviews extractions, rendering
evidencenext to each value makes verification a glance instead of a re-read of the whole document. - Results are cached on Sapling's side for about three days keyed on the text, fields and context, so re-running an identical request is fast — but the same document against a different field set is separate work.