Skip to main content
This walkthrough uploads a PDF, parses it to markdown, and then locates a value inside it. You need an API key (contact your Nace representative) and curl.
Export your key once so the examples below work as-is:
export NDI_API_KEY="ndi_sk_..."
export NDI_BASE="https://ndi-api.nace.ai/api/v1"

Step 1 — Upload a document

Stage the file once and get back an opaque file_id you can reuse across every method:
curl -s "$NDI_BASE/upload" \
  -H "X-API-Key: $NDI_API_KEY" \
  -F "file=@./annual-report.pdf"
{
  "file_id": "7f4d2a10-3b7e-4c25-9f61-8f0f4f0a1b2c",
  "file_name": "annual-report.pdf",
  "expires_at": "2026-07-18T21:04:11Z"
}
Reference this file in any job as ndi://file/<file_id>. Handles expire after about 6 days; upload again if you need the file after that.
You can skip the upload entirely and pass a publicly reachable (or presigned) https:// URL as source_url. See Files & sources.

Step 2 — Parse it

The sync endpoint blocks until the result is ready (default 120 s, tune with ?timeout_seconds=, max 300):
curl -s "$NDI_BASE/parse" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "source_url": "ndi://file/7f4d2a10-3b7e-4c25-9f61-8f0f4f0a1b2c" }
  }'
{
  "job_id": "0c9b2c1e-8f4a-4a91-b7a5-3f2d1e0a9c8b",
  "action": "parse",
  "status": "succeeded",
  "result": {
    "result_type": "parse",
    "status": "success",
    "item": {
      "item_type": "file",
      "markdown_url": "https://…(time-limited download URL)…",
      "page_count": 96,
      "char_count": 184223,
      "extraction_duration_ms": 41250
    }
  }
}
Download the markdown from result.item.markdown_url — it is a time-limited URL (valid for 1 hour by default; re-fetch GET /jobs/{job_id} for a fresh one).
For large documents, prefer the async variant. POST /parse_async returns 202 {"job_id": "..."} immediately; poll GET /jobs/{job_id} until status is succeeded or failed. The sync endpoint may itself respond 202 (server at sync capacity) or 408 (timeout) — in both cases the job keeps running and you poll the same way, so handle those two codes even when you use sync. See Jobs.

Step 3 — Ground a value

Find where “Total liabilities” appears in the source document, with coordinates:
curl -s "$NDI_BASE/ground" \
  -H "X-API-Key: $NDI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file": { "source_url": "ndi://file/7f4d2a10-3b7e-4c25-9f61-8f0f4f0a1b2c" },
    "items": [
      { "item_id": "liab", "query": "total liabilities", "semantic": true }
    ]
  }'
{
  "job_id": "b1e6d5c4-2a3f-4e89-a0b1-c2d3e4f5a6b7",
  "action": "ground",
  "status": "succeeded",
  "result": {
    "result_type": "ground",
    "results": [
      {
        "item_id": "liab",
        "found": true,
        "matches": [
          {
            "match_type": "visual_region",
            "page": 42,
            "x": 0.12, "y": 0.55, "w": 0.31, "h": 0.021,
            "confidence": 0.93,
            "matched_text": "Total liabilities  $1,204,331"
          }
        ]
      }
    ],
    "total_items_found": 1,
    "total_items_requested": 1,
    "duration_ms": 8412
  }
}
The bounding box is normalized to the page (x, y, w, h in 0–1), so you can draw a highlight at any render resolution.

Where to go next

Jobs, polling & idempotency

Sync vs. async, statuses, retries that never double-charge you.

Categorize

Classify segments and sheets against your own taxonomy.

Files & sources

All the ways to get a document into NDI, plus supported formats.

Errors & limits

The error envelope, error codes, and rate limits.