SafetyDataSheetAPI Docs Sign in

#SafetyDataSheetAPI

Turn safety data sheet PDFs into structured, reviewable data. One request returns a fixed 16-section JSON record plus XML and CSV, with a confidence score and warnings.

The API takes a PDF and returns the same record structure every time: sixteen sections following the SDS layout, with fixed field names. Values the document does not contain are null. Nothing is guessed to fill a gap, and the model's uncertainty is exposed through confidence_score and warnings.

  • Base URL https://safetydatasheetapi.com
  • Format multipart upload in, JSON out (XML and CSV included)
  • Languages any SDS language; the detected language is returned as document_language
  • Price $0.25 per document of up to 40 pages, prepaid, failures not billed
Note
Dedicated deployments (own base URL, SLA, data residency, custom schema) are available for enterprise customers. Talk to us.

#Quickstart

Create a key, send a PDF, read the result.

  1. Create an account and an API key. Sign in, open API keys and create one. It starts with sds_live_ and is shown once.
  2. Add credit. The extraction is billed to your prepaid balance. New accounts have none: open Billing in the dashboard and buy a credit pack by card. You can check the balance with GET /api/account.
  3. Send a PDF. Post it as multipart form-data to /extract-sds with your key.
  4. Store the result. Keep request_id, the JSON record, and the warnings together, and route low-confidence documents to review.

Prefer a client library? See the Python and Node.js clients and the Postman collection.

Extract one document
curl -X POST https://safetydatasheetapi.com/extract-sds \
  -H "Authorization: Bearer $SDS_API_KEY" \
  -F "[email protected]" \
  -F "language_hint=en" \
  -F "schema_version=1.0"
import os
import requests

response = requests.post(
    "https://safetydatasheetapi.com/extract-sds",
    headers={"Authorization": f"Bearer {os.environ['SDS_API_KEY']}"},
    files={"file": open("acetone-sds.pdf", "rb")},
    data={"language_hint": "en", "schema_version": "1.0"},
    timeout=120,
)
response.raise_for_status()
result = response.json()

print(result["confidence_score"], result["warnings"])
print(result["outputs"]["json"]["product_identification"]["product_name"])
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("acetone-sds.pdf")], { type: "application/pdf" }), "acetone-sds.pdf");
form.append("language_hint", "en");
form.append("schema_version", "1.0");

const response = await fetch("https://safetydatasheetapi.com/extract-sds", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SDS_API_KEY}` },
  body: form,
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const result = await response.json();

console.log(result.confidence_score, result.warnings);
console.log(result.outputs.json.product_identification.product_name);
Response200 OK
{
  "ok": true,
  "request_id": "9a1f6c2e5b3d4e7f-AMS",
  "output_formats": ["JSON", "XML", "CSV"],
  "confidence_score": 0.94,
  "document_language": "en",
  "is_safety_data_sheet": true,
  "schema_version": "1.0",
  "warnings": [],
  "outputs": {
    "json": {
      "product_identification": { "product_name": "Acetone", "supplier_name": "Example Chemicals Ltd" },
      "hazards_identification": {
        "signal_word": "Danger",
        "h_statements": ["H225 Highly flammable liquid and vapour", "H319 Causes serious eye irritation"]
      },
      "composition": [
        { "substance_name": "Acetone", "cas_number": "67-64-1", "ec_number": "200-662-2", "concentration_percent": "95-100" }
      ],
      "transport_information": { "un_number": "UN1090", "hazard_class": "3", "packing_group": "II" },
      "revision_metadata": { "revision_date": "2024-01-15" }
    },
    "xml": "<?xml version=\"1.0\" ...>",
    "csv": "section,field,value\n..."
  },
  "metadata": { "file_name": "acetone-sds.pdf", "file_size_bytes": 184233, "language_hint": "en",
                "document_language": "en", "schema_version": "1.0" },
  "billing": { "pages": 12, "documents_billed": 1, "cost_usd": 0.25 },
  "processing_ms": 14210
}

#Authentication

Authenticate every request with an API key.

Send the key as a bearer token. The alternative X-API-Key header carries the same key.

Header
Authorization: Bearer sds_live_<id>_<secret>
  • Keys belong to an account. Create, label and revoke them in the dashboard (up to 10 active keys).
  • Only a hash is stored. If a key is lost, revoke it and create another.
  • Never put a key in a URL or in client-side code. Call the API from your server.
  • Invalid or revoked keys return 401; a suspended account returns 403.
Note
Enterprise customers on a dedicated deployment use their own base URL and keys. Every example here works there by changing the base URL.
Check that a key works
curl https://safetydatasheetapi.com/api/account \
  -H "Authorization: Bearer $SDS_API_KEY"

#Extract a document

Synchronous extraction of one PDF. Typical processing time is 10 to 30 seconds depending on length.

POST/extract-sds

Send multipart/form-data. Only PDF files are accepted, up to 10 MB and 400 pages.

NameTypeDescription
filefilerequiredThe SDS or MSDS as a PDF.
language_hintstringoptionalAn ISO language such as en or de. A hint only; the detected language is returned separately as document_language.
schema_versionstringoptionalPins the output schema. Only 1.0 exists today; other values return 400 unsupported_schema_version.

A successful response is 200 with the object described in The response. Failures return a JSON error and are not billed; see Errors.

Tip
Responses can be large. Read outputs.json for application logic and keep request_id for support and audits.
POST /extract-sds
curl -X POST https://safetydatasheetapi.com/extract-sds \
  -H "Authorization: Bearer $SDS_API_KEY" \
  -F "[email protected]" \
  -F "language_hint=en" \
  -F "schema_version=1.0"
import os
import requests

response = requests.post(
    "https://safetydatasheetapi.com/extract-sds",
    headers={"Authorization": f"Bearer {os.environ['SDS_API_KEY']}"},
    files={"file": open("acetone-sds.pdf", "rb")},
    data={"language_hint": "en", "schema_version": "1.0"},
    timeout=120,
)
response.raise_for_status()
result = response.json()

print(result["confidence_score"], result["warnings"])
print(result["outputs"]["json"]["product_identification"]["product_name"])
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("acetone-sds.pdf")], { type: "application/pdf" }), "acetone-sds.pdf");
form.append("language_hint", "en");
form.append("schema_version", "1.0");

const response = await fetch("https://safetydatasheetapi.com/extract-sds", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SDS_API_KEY}` },
  body: form,
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
const result = await response.json();

console.log(result.confidence_score, result.warnings);
console.log(result.outputs.json.product_identification.product_name);

#The response

Every success has the same shape.

FieldTypeDescription
okbooleanAlways true on success.
request_idstringTrace identifier. Quote it when contacting support.
confidence_scorenumber0 to 1. The model's own estimate for the whole document; it is not a calibrated accuracy measure.
warningsstring[]Ambiguities, missing or conflicting values and format problems. Treat a non-empty list as a signal to review.
document_languagestring | nullLanguage the document is written in (sv, pt-BR), or null.
is_safety_data_sheetboolean | nullfalse when the file does not look like a safety data sheet (a warning is added).
schema_versionstringThe schema applied to outputs.json (1.0).
outputs.jsonobjectThe record: sixteen sections with fixed field names. See The 16 sections.
outputs.xml / outputs.csvstringThe same record as XML and CSV. Arrays become repeated elements in XML; CSV cells that start with = + - @ get a leading apostrophe.
metadataobjectFile name, size, language hint, detected language and schema version.
billingobjectpages, documents_billed and cost_usd for this request.
processing_msintegerServer-side processing time.

Unknown or missing values are never invented: a value that is not in the document is null and a list that is not in the document is empty.

Response200 OK
{
  "ok": true,
  "request_id": "9a1f6c2e5b3d4e7f-AMS",
  "output_formats": ["JSON", "XML", "CSV"],
  "confidence_score": 0.94,
  "document_language": "en",
  "is_safety_data_sheet": true,
  "schema_version": "1.0",
  "warnings": [],
  "outputs": {
    "json": {
      "product_identification": { "product_name": "Acetone", "supplier_name": "Example Chemicals Ltd" },
      "hazards_identification": {
        "signal_word": "Danger",
        "h_statements": ["H225 Highly flammable liquid and vapour", "H319 Causes serious eye irritation"]
      },
      "composition": [
        { "substance_name": "Acetone", "cas_number": "67-64-1", "ec_number": "200-662-2", "concentration_percent": "95-100" }
      ],
      "transport_information": { "un_number": "UN1090", "hazard_class": "3", "packing_group": "II" },
      "revision_metadata": { "revision_date": "2024-01-15" }
    },
    "xml": "<?xml version=\"1.0\" ...>",
    "csv": "section,field,value\n..."
  },
  "metadata": { "file_name": "acetone-sds.pdf", "file_size_bytes": 184233, "language_hint": "en",
                "document_language": "en", "schema_version": "1.0" },
  "billing": { "pages": 12, "documents_billed": 1, "cost_usd": 0.25 },
  "processing_ms": 14210
}

#The 16 sections

outputs.json always contains these keys, in this order.

KeyContains
product_identificationProduct name, identifiers, supplier and emergency contact, recommended uses.
hazards_identificationGHS classification, signal word, hazard (H, EUH) and precautionary (P) statements, pictograms.
compositionSubstances with CAS and EC numbers, concentration, and REACH registration data.
first_aidMeasures by exposure route and notes for physicians.
fire_fightingExtinguishing media, hazards from combustion, firefighter protection.
accidental_releasePersonal precautions, environmental precautions, containment and clean-up.
handling_storageSafe handling, storage conditions, incompatibilities, storage class.
exposure_controls_ppeOccupational exposure limits, engineering controls, protective equipment.
physical_chemical_propertiesAppearance, flash point, boiling point, density, pH, vapour pressure and more.
stability_reactivityReactivity, stability, hazardous reactions and decomposition.
toxicological_informationAcute and chronic effects, routes of exposure, toxicity values.
ecological_informationEcotoxicity, persistence, bioaccumulation, water hazard class.
disposal_considerationsWaste treatment and disposal methods.
transport_informationUN number, proper shipping name, class, packing group, per transport mode.
regulatory_informationRegulations, inventories, SVHC and other regulatory statements.
revision_metadataRevision date, version and issue information.

Inside each section the field names are fixed as well, about 100 fields in all. The machine-readable definition is a JSON Schema at /schema/sds-schema-v1.json, and the rules for changing it are in the schema versioning guide.

Example sectionsExcerpt
{
  "composition": [
    {
      "substance_name": "Acetone",
      "cas_number": "67-64-1",
      "ec_number": "200-662-2",
      "index_number": null,
      "reach_registration_number": null,
      "concentration_percent": "95-100"
    }
  ],
  "transport_information": {
    "un_number": "UN1090",
    "modes": [ { "mode": "ADR", "hazard_class": "3", "packing_group": "II" } ]
  }
}

#Bulk jobs

Submit up to 10 documents at once. The call returns immediately and the work runs in the background.

POST/extract-sds/bulk

NameTypeDescription
filefile (repeat)requiredOne to 10 PDFs, each up to 10 MB and 40 MB in total.
callback_urlstringoptionalAn https:// URL that receives a signed webhook when the job finishes. Local and private addresses are rejected.
language_hintstringoptionalAs for a single document.
schema_versionstringoptionalAs for a single document.
Idempotency-KeyheaderoptionalA unique value per job. Repeating the request with the same key returns the existing job instead of creating a duplicate.

Credit for every file is reserved when the job is accepted. If the balance cannot cover all files the call returns 402 and nothing is processed. Files that fail are released and not billed. Bulk items return the JSON record only (no XML or CSV).

GET/extract-sds/bulk/{jobId}

Returns job progress and per-file results at any time. Use it to poll, or to recover results if a webhook never arrived. Jobs are visible to every key of the same account.

Submit and poll
curl -X POST https://safetydatasheetapi.com/extract-sds/bulk \
  -H "Authorization: Bearer $SDS_API_KEY" \
  -H "Idempotency-Key: backfill-2026-10-25-batch-1" \
  -F "[email protected]" \
  -F "[email protected]" \
  -F "callback_url=https://example.com/hooks/sds"

# later: poll the job
curl https://safetydatasheetapi.com/extract-sds/bulk/$JOB_ID \
  -H "Authorization: Bearer $SDS_API_KEY"
import os, time
import requests

headers = {"Authorization": f"Bearer {os.environ['SDS_API_KEY']}"}

job = requests.post(
    "https://safetydatasheetapi.com/extract-sds/bulk",
    headers={**headers, "Idempotency-Key": "backfill-2026-10-25-batch-1"},
    files=[("file", open("acetone-sds.pdf", "rb")), ("file", open("ethanol-sds.pdf", "rb"))],
    timeout=120,
)
job.raise_for_status()
job_id = job.json()["job_id"]

while True:  # or use callback_url and skip polling
    state = requests.get(f"https://safetydatasheetapi.com/extract-sds/bulk/{job_id}", headers=headers, timeout=30).json()
    if state["status"] in ("completed", "failed"):
        break
    time.sleep(5)

for item in state["items"]:
    print(item["file_name"], item["status"], item.get("confidence_score"))
import { readFile } from "node:fs/promises";

const headers = { Authorization: `Bearer ${process.env.SDS_API_KEY}` };
const form = new FormData();
for (const name of ["acetone-sds.pdf", "ethanol-sds.pdf"]) {
  form.append("file", new Blob([await readFile(name)], { type: "application/pdf" }), name);
}

const submit = await fetch("https://safetydatasheetapi.com/extract-sds/bulk", {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "backfill-2026-10-25-batch-1" },
  body: form,
});
const { job_id } = await submit.json();

let state;
do {
  await new Promise((resolve) => setTimeout(resolve, 5000)); // or pass callback_url and skip polling
  state = await (await fetch(`https://safetydatasheetapi.com/extract-sds/bulk/${job_id}`, { headers })).json();
} while (!["completed", "failed"].includes(state.status));

for (const item of state.items) console.log(item.file_name, item.status, item.confidence_score);
Submit response202 Accepted
{
  "ok": true,
  "job_id": "b8b8e3b0-5a11-4d2c-9a57-0f1f6f0c2d77",
  "status": "queued",
  "total_items": 2,
  "status_url": "https://safetydatasheetapi.com/extract-sds/bulk/b8b8e3b0-5a11-4d2c-9a57-0f1f6f0c2d77"
}
Job status200 OK
{
  "ok": true,
  "job_id": "b8b8e3b0-5a11-4d2c-9a57-0f1f6f0c2d77",
  "status": "completed",
  "total_items": 2,
  "completed_items": 2,
  "failed_items": 0,
  "webhook_status": "delivered",
  "webhook_attempts": 1,
  "items": [
    {
      "file_name": "acetone-sds.pdf",
      "status": "success",
      "confidence_score": 0.97,
      "document_language": "en",
      "schema_version": "1.0",
      "warnings": [],
      "data": { "product_identification": { "product_name": "Acetone" }, "...": "16 sections" }
    }
  ]
}

#Webhooks

Get notified when a bulk job finishes instead of polling.

When you pass callback_url, we POST the final job object (the same JSON as the status endpoint) to it. Delivery is attempted a few times immediately; it is not a persistent retry queue, so keep the status endpoint as the fallback.

Every delivery carries X-Webhook-Signature: sha256=<hex>, the HMAC-SHA256 of the raw request body using your webhook signing secret. The secret is specific to your account: show it in the dashboard under API keys ("Webhook signing secret"), keep it private, and never share it. Verify the signature before trusting the payload, and compare in constant time.

For replay protection, each delivery also carries X-Webhook-Id (unique per delivery), X-Webhook-Timestamp (Unix seconds) and X-Webhook-Signature-V2, the HMAC-SHA256 of {id}.{timestamp}.{raw body} with the same secret. Prefer V2: check it, reject timestamps older than five minutes, and ignore an id you have already processed.

Heads up
Verify against the raw bytes you received, not a re-serialized copy of the JSON.
Verify a signature
import hashlib, hmac

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, rawBody, header) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header || "");
  return a.length === b.length && timingSafeEqual(a, b);
}

#Account and balance

Read your balance and recent usage with the same key.

GET/api/account

Returns the prepaid balance, the price, and the last 30 days of usage for the account that owns the key. It is read-only and free. The dashboard shows the same data with history and charts.

This endpoint needs a per-account key. It is not available with a shared enterprise key.

GET /api/account
curl https://safetydatasheetapi.com/api/account \
  -H "Authorization: Bearer $SDS_API_KEY"
import os
import requests

account = requests.get(
    "https://safetydatasheetapi.com/api/account",
    headers={"Authorization": f"Bearer {os.environ['SDS_API_KEY']}"},
    timeout=30,
).json()
print(account["balance_usd"], account["usage"]["documents_billed"])
const account = await (
  await fetch("https://safetydatasheetapi.com/api/account", {
    headers: { Authorization: `Bearer ${process.env.SDS_API_KEY}` },
  })
).json();
console.log(account.balance_usd, account.usage.documents_billed);
Response200 OK
{
  "ok": true,
  "balance_usd": 12.5,
  "pricing": { "price_per_document_usd": 0.25, "pages_per_document": 40, "max_pages_per_request": 400 },
  "usage": { "period_days": 30, "requests": 42, "succeeded": 41, "documents_billed": 44, "pages": 610, "spend_usd": 11 },
  "daily": [ { "date": "2026-10-24", "documents": 3, "pages": 41, "spend_usd": 0.75 } ]
}

#Pricing and billing

One price. Prepaid. You pay for results, not for failures.

$0.25per document of up to 40 pages
  • A longer document bills as one extra document for each further 40 pages, rounded up (45 pages = 2 documents). The limit is 400 pages.
  • Credit is reserved before the extraction runs and released automatically if it fails. Provider errors, unreadable output and early stops are not billed.
  • A low-confidence result is billed: the work was done. Use warnings and the score to decide what to review.
  • When the balance cannot cover a request you get 402 insufficient_credit and nothing is processed.
  • Every response to a per-account key includes a billing object; usage is also listed in the dashboard.

Buying credit. Open Billing in the dashboard and choose a pack: $25 (100 documents), $100 or $500, priced in USD, GBP or EUR. Payment is by card on Stripe's page; prices exclude VAT, which is added where it applies, and businesses can enter a VAT number so it appears on the invoice. Every payment gets an invoice. Credit is valid for 12 months from purchase; whatever is unused then expires, and the dashboard warns you beforehand. Unused credit is refundable within 30 days and failed extractions are never charged: see the refund policy. Need a purchase order or a larger amount? Contact us.

Not enough credit402 Payment Required
{
  "ok": false,
  "error": { "code": "insufficient_credit", "message": "Not enough credit for this document. Add credit to continue." },
  "balance_usd": 0.1,
  "required_usd": 0.25,
  "pages": 12,
  "documents_billed": 1,
  "request_id": "9a1f6c2e5b3d4e7f-AMS"
}

#Rate limits

Per-key limits protect everyone's latency. Contracted plans can have different limits.

ScopeDefault limit
POST /extract-sds per API key60 requests per minute
POST /extract-sds/bulk per API key10 job submissions per minute
Shared enterprise key100 requests per 24 hours per client IP
  • Successful extraction responses include X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds).
  • When limited you get 429 rate_limited with a Retry-After header (seconds) and retry_after_epoch_seconds in the body.
  • Limits are enforced on a best-effort basis. Do not treat them as an exact quota boundary; the prepaid balance is the real spending control.
  • For large backfills use bulk jobs rather than many parallel single requests.
Rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{"ok":false,"error":{"code":"rate_limited","message":"Upload limit reached for this time window."},"retry_after_epoch_seconds":1792764000}

#Errors

Errors are JSON with a stable error.code. Branch on the code, not the message.

StatusCodeMeaningWhat to do
400missing_file invalid_content_type invalid_multipart_payload The request is not multipart/form-data with a file part.Fix the request; do not retry unchanged.
400unsupported_file_type The upload is not a PDF.Send the original PDF.
400unsupported_schema_version schema_version is not a supported version (only 1.0).Send 1.0 or omit the field.
401unauthorized api_key_revoked The key is missing, invalid or revoked.Check or replace the key in the dashboard.
402insufficient_credit The balance does not cover the document (for a bulk job: all documents). Nothing was processed or billed.Add credit. Do not retry automatically.
403account_suspended ip_restricted The account is suspended, or the caller IP is blocked by the endpoint policy.Contact support.
413file_too_large job_too_large A file exceeds 10 MB, or a bulk job exceeds 40 MB.Split the job or compress the PDF.
413too_many_pages The PDF has more than 400 pages. Not billed.Split the document.
429rate_limited The request limit for the window was reached.Wait for the Retry-After seconds, then retry.
500 / 502provider_error The extraction model failed, returned unreadable output, or stopped early. Not billed.Retry with exponential backoff.
500output_processing_failed Extraction ran but formatting the response failed. Not billed.Retry; it is safe to repeat.
503auth_unavailable billing_unavailable extract_api_not_configured A dependency is temporarily unavailable.Retry after a short delay.

Some errors add fields beside error, for example balance_usd on a 402 or request_id on server errors. Retry only 429, 500, 502 and 503, with exponential backoff and jitter.

Error shape
{
  "ok": false,
  "error": {
    "code": "unsupported_file_type",
    "message": "Only SDS/MSDS PDF files are supported."
  }
}

#AI agents and MCP

The same API is available to AI assistants through a remote MCP server.

Endpoint https://safetydatasheetapi.com/mcp (Streamable HTTP, stateless). Discovery works without a key; tool calls need an API key and bill to the same balance as REST calls.

ToolWhat it does
extract_sdsExtract one PDF from a public file_url or file_base64.
submit_bulk_extractionSubmit up to 10 PDFs by URL as one job.
get_bulk_jobRead a job's status and results. Read-only.
get_accountRead the balance and 30-day usage. Read-only.

The server is listed in the official MCP registry as com.safetydatasheetapi/sds-extraction and described in mcp-manifest.yml. Give agents a spending allowance by funding a dedicated account; a 402 tells the agent to stop.

Note
Assistants cannot create accounts. A person must create the key and keep the balance topped up.
Claude Code
claude mcp add --transport http safetydatasheetapi https://safetydatasheetapi.com/mcp \
  --header "Authorization: Bearer $SDS_API_KEY"
Generic MCP client config
{
  "mcpServers": {
    "safetydatasheetapi": {
      "url": "https://safetydatasheetapi.com/mcp",
      "headers": { "Authorization": "Bearer sds_live_..." }
    }
  }
}

#Review routing

An extraction can succeed and still deserve a human look.

  • Store the request_id with every result, even clean ones.
  • Route on warnings. Send any document with a non-empty warnings list to a QA or regulatory queue.
  • Set a confidence floor that fits your risk, for example auto-accept above 0.9 and review below. The score is the model's own estimate, so tune the threshold on your own documents.
  • Check is_safety_data_sheet to catch wrong files early.
  • Validate what matters. CAS numbers have a checksum, and UN numbers are four digits; cheap checks catch most transcription errors.
  • Pin schema_version so a future schema cannot change your mappings silently.

Extraction supports compliance work; it does not replace the responsible person's review of the source document.

#FAQ

What does the SDS extraction API return?

Each successful request returns a request ID, a confidence score, warnings, the detected document language, a fixed 16-section JSON record (schema version 1.0) and matching XML and CSV renderings built from the same extraction run.

How do I authenticate requests?

Send your API key as a bearer token (Authorization: Bearer sds_live_...) or in an X-API-Key header. Create and revoke keys in the dashboard.

How is the API billed?

Each document costs $0.25 for up to 40 pages; longer documents bill as one extra document per additional 40 pages. Credit is prepaid, bought by card in the dashboard (USD, GBP or EUR; VAT added where it applies), and valid for 12 months. Failed extractions are not billed.

When should I use bulk extraction and webhooks?

Use bulk jobs for backlogs, nightly refreshes and queued ingestion where a synchronous request per file would create unnecessary waiting. A webhook removes the need to poll.

How do low-confidence results surface?

Through confidence_score and warnings. The score is the model's own estimate, not a calibrated accuracy figure, so use it to route documents to review rather than as a guarantee.

Does the API support scanned and multilingual SDS PDFs?

The model reads the PDF directly and returns the detected document_language. Scan quality and unusual layouts affect confidence, which the warnings and score reflect.

How do schema versions protect integrations?

The response shape is fixed (version 1.0). Send schema_version to pin it; unsupported values are rejected instead of answered with a different shape. See the schema versioning guide.