> For clean Markdown of any page, append `.md` to the page URL.
> For a complete documentation index, see https://docs.sarvam.ai/llms.txt.
> For full documentation content in one file, see https://docs.sarvam.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.sarvam.ai/_mcp/server.

# Document Intelligence Overview

> Transform documents into structured, queryable data with Sarvam's Document AI API. Powered by Sarvam Vision 1.5 for text digitization and schema-based field extraction across 23 languages (22 Indian + English).

Sarvam's Document AI API provides enterprise-grade document processing powered by [Sarvam Vision 1.5](/api/getting-started/models/sarvam-vision), our state-of-the-art multimodal model.

Turn any document into structured, searchable, machine-readable data, whether you need the full page digitized or specific fields extracted.

#### [Sarvam Vision 1.5](/api/getting-started/models/sarvam-vision)

An upgraded Sarvam Vision, trained to do both OCR and key-value extraction. SOTA performance on global and Indic document benchmarks.

## Two ways to process a document

Document AI gives you two endpoints over the same job lifecycle. Pick based on what you need out the other end.

#### Digitise

Full-document OCR. Extracts all text, preserves layout and reading order, and parses tables into HTML or Markdown. Use when you want the **whole document** as clean, structured content.

#### Extract

Schema-based key-value extraction. You define the fields you want; Sarvam Vision 1.5 returns them as structured JSON. Use when you want **specific fields**, not the whole page.

| Aspect          | Digitise                                                           | Extract                                        |
| --------------- | ------------------------------------------------------------------ | ---------------------------------------------- |
| **Output**      | Full text + layout (HTML or Markdown, plus JSON page data)         | Just the fields you define (JSON / CSV / XLSX) |
| **You provide** | A document                                                         | A document + a schema (or saved `config_id`)   |
| **Best for**    | Archival, search, RAG ingestion, reading-order-faithful conversion | KYC, invoices, forms, structured data capture  |
| **Endpoint**    | `POST /doc-ai/v1/job/digitise`                                     | `POST /doc-ai/v1/job/extract`                  |

## What's new

**Document AI supersedes the earlier Document Digitization API.** Two changes matter most:

1. Processing is now powered by an **upgraded Sarvam Vision 1.5**, trained to do both OCR and key-value extraction.
2. Alongside full-document **Digitise** (OCR), we now offer **Extract** for schema-based field extraction.

The API namespace is `doc_ai`, and jobs are created with a single `digitise()` or `extract()` call.

## Key Features

#### 23 Language Support

Native support for all Constitutionally recognized Indian languages and English with script-native accuracy.

#### Digitise + Extract

Full-document digitization and targeted key-value extraction from one API and one job model.

#### Table Extraction

Intelligent table detection and conversion to structured HTML or Markdown.

#### Schema-Based Fields

Define exactly the fields you want in Extract, returned as structured data.

#### Layout Preservation

Intelligent reading-order detection and complex layout handling.

#### Enterprise-Ready

Scalable API with job management, progress tracking, and error handling.

## Supported Languages

Document AI supports all 22 Constitutionally recognized Indian languages plus English:

| Language  | Code     |
| --------- | -------- |
| Hindi     | `hi-IN`  |
| Bengali   | `bn-IN`  |
| Tamil     | `ta-IN`  |
| Telugu    | `te-IN`  |
| Marathi   | `mr-IN`  |
| Gujarati  | `gu-IN`  |
| Kannada   | `kn-IN`  |
| Malayalam | `ml-IN`  |
| Odia      | `od-IN`  |
| Punjabi   | `pa-IN`  |
| Assamese  | `as-IN`  |
| Bodo      | `brx-IN` |
| Dogri     | `doi-IN` |
| Kashmiri  | `ks-IN`  |
| Konkani   | `kok-IN` |
| Maithili  | `mai-IN` |
| Manipuri  | `mni-IN` |
| Nepali    | `ne-IN`  |
| Sanskrit  | `sa-IN`  |
| Santali   | `sat-IN` |
| Sindhi    | `sd-IN`  |
| Urdu      | `ur-IN`  |
| English   | `en-IN`  |

## Supported Input Formats

| Format | Extension       | Description                                                      |
| ------ | --------------- | ---------------------------------------------------------------- |
| PDF    | `.pdf`          | Multi-page PDF documents (max 10 pages)                          |
| PNG    | `.png`          | Document page images                                             |
| JPEG   | `.jpg`, `.jpeg` | Document page images                                             |
| ZIP    | `.zip`          | Flat archive of document page images (JPG or PNG, max 10 images) |

**Page Limit:** Both PDF and ZIP uploads are limited to a maximum of **10 pages**. Exceeding this limit returns a `400 Bad Request` with code `invalid_request_error`. Split larger documents into batches of 10 pages or fewer before uploading.

For ZIP files, include only JPG and PNG document pages in a flat structure (no nested folders). The API processes all pages in the archive and maintains page order based on filename.

## Output Formats

### Digitise

The `output_format` parameter controls the primary content format. Choose `html` (the default) or `md` (Markdown).

**JSON page-level data is always included by default.** Whether you choose HTML or Markdown, a JSON file with structured page-level data is always included alongside it, so there is no `json` option here — passing one returns `400 OUTPUT_FORMAT_INVALID`. (Extract does accept `json`; see below.)

| Format   | `output_format` value | Description                                                   |
| -------- | --------------------- | ------------------------------------------------------------- |
| HTML     | `html`                | Rich HTML output for web rendering + JSON page data (default) |
| Markdown | `md`                  | Human-readable Markdown output + JSON page data               |

The download is a ZIP archive holding the primary file, a `metadata/page_NNN.json` per page, and a `manifest.json`.

### Extract

| Format | `output_format` value | Description                               |
| ------ | --------------------- | ----------------------------------------- |
| JSON   | `json`                | Structured field values (default)         |
| CSV    | `csv`                 | Flat tabular output, one row per document |
| XLSX   | `xlsx`                | Spreadsheet output                        |

## Quick Start: Digitise

**API parameter names:** Document AI uses `language` and `output_format` values `"md"`, `"html"`, or `"json"`. Do not use `language_code` (it's ignored) or `"markdown"` (it returns `400`; use `"md"` instead). This differs from STT, translate, and LID endpoints.

#### Python

```python
import os, time
from sarvamai import SarvamAI
from sarvamai.core.api_error import ApiError

client = SarvamAI(api_subscription_key=os.environ["SARVAM_API_KEY"])

try:
    # Create and submit a digitise job in one call
    with open("./sample-document.pdf", "rb") as f:
        job = client.doc_ai.digitise(
            file=[("sample-document.pdf", f, "application/pdf")],
            language="en-IN",
            output_format="md",
        )
    print("job created:", job.job_id, "| status:", job.status)

    # Poll until the job reaches a terminal state
    TERMINAL = {"completed", "partially_completed", "failed", "rejected"}
    while True:
        st = client.doc_ai.get_status(job_id=job.job_id)
        print(f"status={st.status} pages={st.usage.pages_processed}/{st.usage.pages_total}")
        if st.status.lower() in TERMINAL:
            break
        time.sleep(5)

    # Fetch the output
    if st.status.lower() in ("completed", "partially_completed"):
        dl = client.doc_ai.get_download_url(job_id=job.job_id)
        print("download:", dl.method, dl.url)
except FileNotFoundError:
    print("Document file not found: ./sample-document.pdf")
except ApiError as e:
    print(f"API error {e.status_code}: {e.body}")
```

#### JavaScript

```javascript
import fs from "fs";
import { SarvamAIClient } from "sarvamai";

const client = new SarvamAIClient({ apiSubscriptionKey: process.env.SARVAM_API_KEY });

async function main() {
  try {
    // Create and submit a digitise job in one call
    const job = await client.docAi.digitise({
      file: [fs.createReadStream("./sample-document.pdf")],
      language: "en-IN",
      output_format: "md",
    });
    console.log("job created:", job.job_id, "| status:", job.status);

    // Poll until the job reaches a terminal state
    const TERMINAL = new Set(["completed", "partially_completed", "failed", "rejected"]);
    let st;
    while (true) {
      st = await client.docAi.getStatus(job.job_id);
      console.log(`status=${st.status} pages=${st.usage.pages_processed}/${st.usage.pages_total}`);
      if (TERMINAL.has(st.status.toLowerCase())) break;
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }

    // Fetch the output
    if (["completed", "partially_completed"].includes(st.status.toLowerCase())) {
      const dl = await client.docAi.getDownloadUrl(job.job_id);
      console.log("download:", dl.method, dl.url);
    }
  } catch (err) {
    console.error("Digitise job failed:", err.message ?? err);
  }
}

main();
```

#### cURL

```bash
# Create and submit a digitise job
curl -X POST https://api.sarvam.ai/doc-ai/v1/job/digitise \
  -H "api-subscription-key: $SARVAM_API_KEY" \
  -F file=@./sample-document.pdf \
  -F language="en-IN" \
  -F output_format="md"
# {"job_id": "<JOB_ID>", "status": "pending", ...}

# Poll until the job reaches a terminal state
curl https://api.sarvam.ai/doc-ai/v1/job/<JOB_ID>/status \
  -H "api-subscription-key: $SARVAM_API_KEY"
# {"status": "running", "usage": {"pages_processed": 6, "pages_total": 10}, ...}

# Once status is "completed" or "partially_completed", fetch the output
curl https://api.sarvam.ai/doc-ai/v1/job/<JOB_ID>/download-url \
  -H "api-subscription-key: $SARVAM_API_KEY"
# {"method": "GET", "url": "https://..."}
```

## Quick Start: Extract

Extract takes the same document input, plus a **schema** describing the fields you want (or a saved `config_id`). The result is structured JSON with the field values you defined.

#### Python

```python
import json, os, time
from sarvamai import SarvamAI
from sarvamai.core.api_error import ApiError

client = SarvamAI(api_subscription_key=os.environ["SARVAM_API_KEY"])

# Define the fields you want back
schema = {
    "type": "object",
    "properties": {
        "policy_number": {"type": "string", "description": "Insurance policy number"},
        "insured_name":  {"type": "string", "description": "Name of the insured person"},
        "sum_insured":   {"type": "number", "description": "Total sum insured, in INR"},
    },
}

try:
    # Create and submit an extract job in one call
    with open("./insurance-policy.pdf", "rb") as f:
        job = client.doc_ai.extract(
            file=[("insurance-policy.pdf", f, "application/pdf")],
            schema=json.dumps(schema),   # or: config_id="cfg_abc123"
            language="en-IN",
            output_format="json",
        )
    print("job created:", job.job_id, "| status:", job.status)

    # Poll until the job reaches a terminal state
    TERMINAL = {"completed", "partially_completed", "failed", "rejected"}
    while True:
        st = client.doc_ai.get_status(job_id=job.job_id)
        print(f"status={st.status} pages={st.usage.pages_processed}/{st.usage.pages_total}")
        if st.status.lower() in TERMINAL:
            break
        time.sleep(5)

    # Fetch the extracted fields
    if st.status.lower() in ("completed", "partially_completed"):
        results = client.doc_ai.get_results(job_id=job.job_id)
        print(results.result)        # {"policy_number": ..., "insured_name": ..., "sum_insured": ...}
except FileNotFoundError:
    print("Document file not found: ./insurance-policy.pdf")
except ApiError as e:
    print(f"API error {e.status_code}: {e.body}")
```

#### JavaScript

```javascript
import fs from "fs";
import { SarvamAIClient } from "sarvamai";

const client = new SarvamAIClient({ apiSubscriptionKey: process.env.SARVAM_API_KEY });

// Define the fields you want back
const schema = {
  type: "object",
  properties: {
    policy_number: { type: "string", description: "Insurance policy number" },
    insured_name: { type: "string", description: "Name of the insured person" },
    sum_insured: { type: "number", description: "Total sum insured, in INR" },
  },
};

async function main() {
  try {
    // Create and submit an extract job in one call
    const job = await client.docAi.extract({
      file: [fs.createReadStream("./insurance-policy.pdf")],
      schema: JSON.stringify(schema),   // or: config_id: "cfg_abc123"
      language: "en-IN",
      output_format: "json",
    });
    console.log("job created:", job.job_id, "| status:", job.status);

    // Poll until the job reaches a terminal state
    const TERMINAL = new Set(["completed", "partially_completed", "failed", "rejected"]);
    let st;
    while (true) {
      st = await client.docAi.getStatus(job.job_id);
      console.log(`status=${st.status} pages=${st.usage.pages_processed}/${st.usage.pages_total}`);
      if (TERMINAL.has(st.status.toLowerCase())) break;
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }

    // Fetch the extracted fields
    if (["completed", "partially_completed"].includes(st.status.toLowerCase())) {
      const results = await client.docAi.getResults(job.job_id);
      console.log(results.result);   // { policy_number: ..., insured_name: ..., sum_insured: ... }
    }
  } catch (err) {
    console.error("Extract job failed:", err.message ?? err);
  }
}

main();
```

#### cURL

```bash
# Create and submit an extract job
curl -X POST https://api.sarvam.ai/doc-ai/v1/job/extract \
  -H "api-subscription-key: $SARVAM_API_KEY" \
  -F file=@./insurance-policy.pdf \
  -F schema='{
    "type": "object",
    "properties": {
      "policy_number": {"type": "string", "description": "Insurance policy number"},
      "insured_name": {"type": "string", "description": "Name of the insured person"},
      "sum_insured": {"type": "number", "description": "Total sum insured, in INR"}
    }
  }' \
  -F language="en-IN" \
  -F output_format="json"
# {"job_id": "<JOB_ID>", "status": "pending", ...}

# Poll until the job reaches a terminal state
curl https://api.sarvam.ai/doc-ai/v1/job/<JOB_ID>/status \
  -H "api-subscription-key: $SARVAM_API_KEY"
# {"status": "running", "usage": {"pages_processed": 6, "pages_total": 10}, ...}

# Once status is "completed" or "partially_completed", fetch the extracted fields
curl https://api.sarvam.ai/doc-ai/v1/job/<JOB_ID>/results \
  -H "api-subscription-key: $SARVAM_API_KEY"
# {"result": {"policy_number": ..., "insured_name": ..., "sum_insured": ...}}
```

**Provide exactly one of `schema` or `config_id`.** Use an inline `schema` for ad-hoc extraction, or a saved `config_id` for a schema you've set up on the platform. Sending both, or neither, returns a `400`.

**Pass `schema` as a JSON string, not an object.** It's sent as a multipart form field, so both SDKs type it as a string — use `json.dumps(schema)` in Python and `JSON.stringify(schema)` in JavaScript. Passing a dict in Python raises `AttributeError: 'dict' object has no attribute 'read'`.

Note also that the JavaScript SDK is generated without a serialization layer, so its request parameters and response fields use the wire names (`output_format`, `job_id`, `pages_processed`), not camelCase. `file` takes an **array**, and `job_id` is a **positional** argument — `getStatus(job_id)`, not `getStatus({ job_id })`.

### Schema rules

The inline schema is a JSON object with these constraints:

* Root must be `type: "object"` with a non-empty `properties` map.
* Every field needs a `type` and a non-empty `description`. The description guides the model, so make it specific.
* Supported field types: `string`, `number`, `integer`, `boolean`, `object`, `array`.
* An optional `enum` may be supplied to constrain values.
* Maximum nesting depth is 4.

## Job Lifecycle

Both `digitise()` and `extract()` create and submit a job in a single call, returning a `job_id` and an initial status. You then poll `get_status()` until the job reaches a terminal state, and fetch output with `get_results()` (structured data) or `get_download_url()` (output file).

```
digitise() / extract()  →  poll get_status()  →  get_results() / get_download_url()
```

### Job States

| State                 | Description                                                     |
| --------------------- | --------------------------------------------------------------- |
| `pending`             | Job created, queued for processing                              |
| `running`             | Job is being processed                                          |
| `completed`           | All pages processed successfully                                |
| `partially_completed` | Some pages succeeded, some failed                               |
| `failed`              | All pages failed or a job-level error occurred                  |
| `rejected`            | Job rejected before processing (e.g. validation or entitlement) |

Terminal states are `completed`, `partially_completed`, `failed`, and `rejected`.

### Status Response

```json
{
  "job_id": "019fb81e-2543-70bd-a737-b5a2d62a3bb7",
  "status": "completed",
  "pipeline": "digitise",
  "usage": {
    "pages_total": 1,
    "pages_processed": 1,
    "pages_succeeded": 1,
    "pages_failed": 0
  },
  "created_at": "2026-07-31T12:20:11Z",
  "updated_at": "2026-07-31T12:20:15Z"
}
```

## Error Handling

#### Error Handling Code Example

```python
import json, os
from sarvamai import SarvamAI
from sarvamai.core.api_error import ApiError

client = SarvamAI(api_subscription_key=os.environ["SARVAM_API_KEY"])

try:
    with open("./insurance-policy.pdf", "rb") as f:
        job = client.doc_ai.extract(
            file=[("insurance-policy.pdf", f, "application/pdf")],
            schema=json.dumps({
                "type": "object",
                "properties": {
                    "policy_number": {"type": "string", "description": "Insurance policy number"},
                },
            }),
            output_format="json",
        )

    # ... poll get_status() until terminal ...

    results = client.doc_ai.get_results(job_id=job.job_id)
    print(results.result)

except ApiError as e:
    if e.status_code == 400:
        print(f"Bad request or invalid schema: {e.body}")
    elif e.status_code in (402, 403):
        print("Billing or entitlement error")
    elif e.status_code == 409:
        print("Results requested before the job reached a terminal state")
    elif e.status_code == 413:
        print("File too large")
    elif e.status_code == 429:
        print("Rate limit exceeded")
    else:
        print(f"Error {e.status_code}: {e.body}")
except FileNotFoundError:
    print("Document file not found: ./insurance-policy.pdf")
```

### Error Codes

The full error-code table, retry guidance, and SDK exception reference live on the central [Errors & Troubleshooting](/api/getting-started/errors-troubleshooting) page. Errors specific to this API:

| HTTP Status   | Description                                                                |
| ------------- | -------------------------------------------------------------------------- |
| `400`         | Invalid request or schema (includes documents exceeding the 10-page limit) |
| `402` / `403` | Billing or entitlement error                                               |
| `404`         | Resource unavailable (e.g. job not found)                                  |
| `409`         | Results or download requested before the job reached a terminal state      |
| `413`         | File too large                                                             |
| `422`         | Invalid file format or corrupted file                                      |
| `429`         | Rate or admission limit                                                    |
| `503`         | Billing unavailable                                                        |

## Best Practices

#### Digitise vs. Extract

Reach for Digitise when you want the whole document, and Extract when you want specific fields. Don't post-process a full digitise just to pull three fields; define a schema and let Extract do it.

#### Write Specific Descriptions

In Extract, each field's `description` guides the model. "Insurance policy number, top-right, format ABC-1234" outperforms "policy number".

#### Specify Language

Always specify the correct language code for optimal accuracy, especially for Indian-language documents.

#### Handle Large Documents

Monitor `usage` in the status response to track progress, and handle `partially_completed` jobs gracefully.

## Limits

| Limit                   | Value                                                                              |
| ----------------------- | ---------------------------------------------------------------------------------- |
| Max pages per PDF       | 10 (`400 invalid_request_error` if exceeded)                                       |
| Max images per ZIP      | 10                                                                                 |
| Max file size           | 200 MB                                                                             |
| Supported input formats | PDF, PNG, JPG, ZIP                                                                 |
| Rate limit              | 10 requests/minute (all plans); see [Rate Limits](/api/getting-started/ratelimits) |

## Next Steps

#### [Sarvam Vision 1.5](/api/getting-started/models/sarvam-vision)

Learn about the upgraded model powering Document AI.

#### [API Reference](/api/api-guides-tutorials/document-intelligence/overview)

Complete API documentation with all parameters and options.

#### [Try in API Dashboard](https://dashboard.sarvam.ai)

Upload and process documents in the API Dashboard.