> ## 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.

# Extraction schema registry

> Version immutable JSON Schemas and pass schema_id to Extract

The registry is **REST-only** — there is no SDK namespace. `documents.extract`
still accepts `schema_id` once a family exists. Validate a draft first with
`documents.validate_extract_schema` or `POST /v1/extract/schema-validation`.

<Tabs>
  <Tab title="Python">
    ```python theme={"dark"}
    import os

    import httpx
    from ndi_sdk import NdiClient

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

    with NdiClient() as client:
        check = client.documents.validate_extract_schema(json_schema=schema)
        print(check)

    base = os.environ["NDI_BASE_URL"]
    headers = {"X-API-Key": os.environ["NDI_API_KEY"], "Content-Type": "application/json"}
    created = httpx.post(
        f"{base}/v1/extraction-schemas",
        headers=headers,
        json={"name": "Invoice", "description": "Core invoice fields", "schema": schema},
    ).json()
    schema_id = created["schema_id"]

    with NdiClient() as client:
        upload = client.documents.create_upload("invoice.pdf")
        job = client.jobs.wait(
            client.documents.extract(upload, schema_id=schema_id, wait_seconds=30).job_id
        )
        print(job.result.schema_id, job.result.schema_version, job.result.data)
    ```
  </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 check = await client.documents.validateExtractSchema({ json_schema: schema });
    console.log(check);

    const created = await fetch(`${process.env.NDI_BASE_URL}/v1/extraction-schemas`, {
      method: "POST",
      headers: {
        "X-API-Key": process.env.NDI_API_KEY ?? "",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ name: "Invoice", description: "Core invoice fields", schema }),
    });
    const { schema_id } = (await created.json()) as { schema_id: string };

    const upload = await client.documents.createUpload("invoice.pdf");
    const job = await client.jobs.wait(
      (await client.documents.extract(upload, { schema_id, wait_seconds: 30 })).job_id,
    );
    console.log(job.result);
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={"dark"}
    curl -s -X POST "$NDI_BASE_URL/v1/extract/schema-validation" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"schema":{"type":"object","properties":{"invoice_number":{"type":"string"},"total":{"type":"number"}},"required":["invoice_number","total"]}}'

    SCHEMA_ID=$(curl -s -X POST "$NDI_BASE_URL/v1/extraction-schemas" \
      -H "X-API-Key: $NDI_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name":"Invoice","description":"Core invoice fields","schema":{"type":"object","properties":{"invoice_number":{"type":"string"},"total":{"type":"number"}},"required":["invoice_number","total"]}}' \
      | jq -r '.schema_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\":\"url\",\"url\":\"https://example.com/invoice.pdf\",\"file_name\":\"invoice.pdf\"},\"schema_id\":\"$SCHEMA_ID\"}"
    ```
  </Tab>
</Tabs>

## Versioning

* `POST /v1/extraction-schemas` creates a family (`sch_…`) and version 1
* `POST /v1/extraction-schemas/{schema_id}/versions` appends an immutable version
* Extract `schema_version` pins a version; omit it to use latest
* Tenant keys create and append only their own schemas
* Reads include tenant and platform families; a tenant row wins on id clash
* No drafts, in-place edits, or deletes

See [Extract](/guides/extract).
