> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Extract structured data

> Pull JSON that matches a schema, with per-field status and citations

Extract fills a JSON Schema from a document. `not_found` on a field is an
answer about the document, not an HTTP error. Exactly one of `json_schema` /
`schema` or `schema_id` is required.

<Tabs>
  <Tab title="Python">
    ```python theme={"dark"}
    from ndi_sdk import NdiClient, ParseResultSource

    schema = {
        "type": "object",
        "properties": {
            "invoice_number": {"type": "string"},
            "total": {"type": "number"},
        },
        "required": ["invoice_number", "total"],
    }

    with NdiClient() as client:
        upload = client.documents.create_upload("invoice.pdf")
        parsed = client.jobs.wait(client.documents.parse(upload, wait_seconds=30).job_id)
        job = client.jobs.wait(
            client.documents.extract(
                ParseResultSource(job_id=parsed.job_id),
                json_schema=schema,
                wait_seconds=30,
            ).job_id
        )
        print(job.result.data)
        for field in job.result.fields:
            print(field.path, field.status, field.value)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts theme={"dark"}
    import { NdiClient } from "ndi-sdk";

    const schema = {
      type: "object",
      properties: {
        invoice_number: { type: "string" },
        total: { type: "number" },
      },
      required: ["invoice_number", "total"],
    };

    const client = new NdiClient();
    const upload = await client.documents.createUpload("invoice.pdf");
    const parsed = await client.jobs.wait(
      (await client.documents.parse(upload, { wait_seconds: 30 })).job_id,
    );
    const job = await client.jobs.wait(
      (
        await client.documents.extract(
          { type: "parse_result", job_id: parsed.job_id },
          { json_schema: schema, wait_seconds: 30 },
        )
      ).job_id,
    );
    console.log(job.result && "data" in job.result ? job.result.data : job.result);
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={"dark"}
    UPLOAD_ID=$(curl -s -X POST "$NDI_BASE_URL/v1/uploads" \
      -H "X-API-Key: $NDI_API_KEY" \
      -F "file=@invoice.pdf" | jq -r '.upload_id')

    curl -s -X POST "$NDI_BASE_URL/v1/extract?wait_seconds=60" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{\"source\":{\"type\":\"upload\",\"upload_id\":\"$UPLOAD_ID\"},\"schema\":{\"type\":\"object\",\"properties\":{\"invoice_number\":{\"type\":\"string\"},\"total\":{\"type\":\"number\"}},\"required\":[\"invoice_number\",\"total\"]}}"
    ```
  </Tab>
</Tabs>

Reuse a parse with `ParseResultSource` so the parse is not paid for twice.
`page_ranges` is not available on a parse-result source.

Validate a schema first with `client.documents.validate_extract_schema` or
`POST /v1/extract/schema-validation`. Register reusable schemas with
[extraction schemas](/guides/extraction-schemas) and pass `schema_id`.

## Next

* [Extract response format](/guides/extract-response)
* [Ground a quote](/guides/ground)
