The HTTP API behind the connector lets you upload documents, read their pages and tables, and delete them.

The base URL is `https://api.mdfi.ai`, and the server's own OpenAPI reference, with every field, is at [https://api.mdfi.ai/docs](https://api.mdfi.ai/docs). The examples below use `curl`, with your key in `$MDFI_API_KEY`.

## Authentication

Every request sends your API key, which you get on the dashboard's Connect page, as a bearer token:

```text
Authorization: Bearer mdfi_…
```

Without a valid key the answer is HTTP 401: `API key required` (no header), `Invalid API key format` (the key does not start with `mdfi_`) or `Invalid or revoked API key`. A valid key whose account has no active plan gets HTTP 402: `Subscription required. Choose a plan at …/dashboard/billing`.

Documents belong to the account that uploaded them. Another account's document id answers 404, as if it did not exist.

## Upload a document

`POST /v1/documents` takes a form with these fields:

| Field | Required | What it is |
| --- | --- | --- |
| `sha256` | Yes | The file's SHA-256: 64 hex characters, in any case. |
| `file` | No | The file itself. Leave it out to ask whether your account already has the document. |
| `max_tier` | No | The highest tier a re-read may use, 1 to 3. Default 3. `max_tier=2` keeps every page away from the vision model. |

Send the hash first, without the file:

```bash
SHA=$(shasum -a 256 report.pdf | cut -d' ' -f1)
curl -s -H "Authorization: Bearer $MDFI_API_KEY" -F sha256=$SHA https://api.mdfi.ai/v1/documents
```

- **200:** your account already has this document. The answer is its current status, with its manifest while it converts or once it is ready (see [Read a document](#read-a-document)). Nothing is charged, and `max_tier` is ignored.
- **404 `upload required`:** your account does not have it. Send the same request with the file:

```bash
curl -s -H "Authorization: Bearer $MDFI_API_KEY" -F sha256=$SHA -F file=@report.pdf https://api.mdfi.ai/v1/documents
```

- **202:** the conversion has started. The answer is `{"id": "…", "status": "converting", "source": "report.pdf", "error": null, "manifest": null}`. Use the `id` with the requests below.

Before converting, mdfi checks the upload. A failed check converts nothing and charges nothing:

| Status | Message | When |
| --- | --- | --- |
| 400 | `sha256 must be exactly 64 hex characters (SHA-256)` | `sha256` is not a SHA-256. |
| 400 | `sha256 does not match the uploaded file` | The file's hash is not `sha256`. |
| 400 | `Unsupported file type: <extension>` | mdfi does not convert this type. It converts the types the [Connector](connector.md#which-files-it-reads) reads, and also other plain-text and code files such as `.csv`, `.json`, `.yaml`, `.py` and `.log`, which the connector does not send. |
| 400 | `Could not read this PDF — the file may be damaged.`, `Password-protected PDF — can't read it.`, `Could not read this file — it may be damaged.` | mdfi could not open the file to count its pages. |
| 400 | `Too large: limit is 1,000 pages / 100 MB per document.` | The file has more than 1,000 pages. |
| 413 | `Too large: limit is 1,000 pages / 100 MB per document.` | The file is over 100 MB. |
| 429 | `Not enough pages left this month: needed N, M left.` followed by when the allowance resets | The pages left this month do not cover the file ([Pricing](pricing.md#when-the-month-runs-out)). |

A conversion that fails is not kept: sending its hash again answers 404 `upload required`, and uploading the file converts it again. The document is charged once, when its conversion completes.

## Read a document

`GET /v1/documents/{id}` returns the document's status:

```bash
curl -s -H "Authorization: Bearer $MDFI_API_KEY" https://api.mdfi.ai/v1/documents/$ID
```

| Field | What it holds |
| --- | --- |
| `id` | The document id. |
| `status` | `converting`, `ready` or `failed`. |
| `source` | The uploaded file's name. |
| `error` | Why the conversion failed; `null` otherwise. The [Connector](connector.md#conversion-failures) page lists the reasons. |
| `manifest` | What the agent needs to choose pages. `null` when the document failed, and while it converts until its outline or first pages exist. |

The manifest:

| Field | What it holds |
| --- | --- |
| `page_count` | The number of pages. While converting, the count known before conversion where it is known. |
| `weighted_pages` | What the document is charged, in weighted pages. While converting, the weight of the work done so far; nothing is charged before the document is ready. |
| `approx_tokens` | The characters of all pages divided by 4. A rough size, never used for billing. |
| `pages` | One entry per page: `number`, `state` (`ready` or `converting`), `score` (0 to 100), `charged_tier`, `kept_tier` and `flags`. `score` and both tiers are `null` for a page not converted yet. |
| `outline` | Entries with `title`, `level` (1 to 6), `start_page` and `end_page`. |
| `warnings` | Messages about the conversion as a whole, such as pages a tier could not re-read. |
| `tables` | One entry per table: `id`, `start_page`, `end_page`, `rows` (data rows, the header not counted), `cols`, `uncertain` and `notes`. |
| `first_text` | The text of the ready pages: all of it up to about 10,000 tokens, otherwise whole pages from the first, up to about 3,000 tokens. Each page starts with a `--- page N ---` line. |
| `first_text_pages` | The pages in `first_text`. |
| `estimated_seconds_left` | While converting, an estimate of the seconds until the document is ready, from the pace of the pages converted so far. It is an estimate, not a promise. `null` before the first page is converted, when only re-reads are left, when the conversion has run past its estimate, and once the document is ready. |
| `complete` | `true` when `first_text` is the whole document. Always `false` while converting; use `status` to know when the document is ready. |

[How it works](how-it-works.md) explains [scores and flags](how-it-works.md#flags-and-warnings), [tiers](how-it-works.md#three-tiers) and [tables](how-it-works.md#tables). For example, a ready 3-page report whose third page was re-read by the vision model (a few fields shortened):

```json
{
  "id": "1b4e28ba-2fa1-41d2-883f-0016d3cca427",
  "status": "ready",
  "source": "report.pdf",
  "error": null,
  "manifest": {
    "page_count": 3,
    "weighted_pages": 7,
    "approx_tokens": 49,
    "pages": [
      {"number": 1, "score": 100.0, "charged_tier": 1, "kept_tier": 1, "flags": [], "state": "ready"},
      {"number": 2, "score": 96.0, "charged_tier": 1, "kept_tier": 1, "flags": [], "state": "ready"},
      {"number": 3, "score": 88.0, "charged_tier": 3, "kept_tier": 3, "flags": ["ocr", "enhanced_tier3"], "state": "ready"}
    ],
    "outline": [
      {"title": "Summary", "level": 1, "start_page": 1, "end_page": 1},
      {"title": "Results", "level": 1, "start_page": 2, "end_page": 3}
    ],
    "warnings": [],
    "tables": [
      {"id": "p2-t1", "start_page": 2, "end_page": 2, "rows": 4, "cols": 3, "uncertain": false, "notes": []}
    ],
    "first_text": "--- page 1 ---\n## Summary\nRevenue grew in every region except the north.\n\n--- page 2 ---\n…",
    "first_text_pages": [1, 2, 3],
    "estimated_seconds_left": null,
    "complete": true
  }
}
```

While a document converts, PDF, image and PowerPoint pages become `ready` before the whole document is. The pages of other formats stay `converting` until it is ready ([How it works](how-it-works.md#outline-first-then-pages)).

A document mdfi does not have for your account answers 404 `Document not found or expired — upload it again with POST /v1/documents`: the id is wrong, the document was deleted, or it is older than 7 days.

## Read pages

`GET /v1/documents/{id}/pages` returns the text of some pages. Pass exactly one of:

- `range`: page numbers and ranges, such as `3,7,9-11`;
- `section`: a title from the outline. A title several entries share is chosen as `Parent > Title`; the [Connector](connector.md#choosing-a-section) page explains the matching.

```bash
curl -s -H "Authorization: Bearer $MDFI_API_KEY" "https://api.mdfi.ai/v1/documents/$ID/pages?range=2-3"
curl -s -G -H "Authorization: Bearer $MDFI_API_KEY" --data-urlencode "section=Results" https://api.mdfi.ai/v1/documents/$ID/pages
```

The answer lists the pages whose text it holds and the requested pages that are not ready yet (text shortened):

```json
{
  "id": "1b4e28ba-2fa1-41d2-883f-0016d3cca427",
  "pages": [2, 3],
  "text": "--- page 2 ---\n## Results\n[table p2-t1 — 4 rows × 3 cols]\nRegion\t2024\t2025\n…\n\n--- page 3 ---\nFigures are unaudited.",
  "not_ready": []
}
```

While the document converts, `text` holds the requested pages that are ready, and `not_ready` the rest; ask for those again later. While only the outline exists, every requested page is in `not_ready`. Reading pages never charges.

| Status | Message | When |
| --- | --- | --- |
| 400 | `Pass exactly one of range or section` | Both or neither were given. |
| 400 | `range/section must be at most 200 characters` | The selector is too long. |
| 400 | `Invalid page range '<range>': use a form like 3,7,9-11` | `range` is not a list of numbers and ranges. |
| 400 | `Invalid page range '<part>': pages start at 1 and ranges run low to high` | A page is 0, or a range runs backwards. |
| 400 | `Page N is out of range: the document has M pages` | A page is past the last page. |
| 404 | `Section not found in outline: <section>` | No outline entry has this title. |
| 409 | `Section '<section>' matches N outline entries; pass one as "Parent > Title": …` | Several entries share the title; the message lists them. |
| 409 | `Document is converting` | Nothing of the document is available yet. |
| 409 | `Document is failed: <reason>` | The conversion failed. |

## Export a table

`GET /v1/documents/{id}/tables/{table_id}` returns one table, with the id from the manifest's `tables` or from the table's label in the page text:

```bash
curl -s -H "Authorization: Bearer $MDFI_API_KEY" https://api.mdfi.ai/v1/documents/$ID/tables/p2-t1
```

```json
{
  "id": "p2-t1",
  "start_page": 2,
  "end_page": 2,
  "headers": ["Region", "2024", "2025"],
  "rows": [["North", "12", "11"], ["South", "20", "24"], ["East", "9", "10"], ["West", "15", "17"]],
  "uncertain": false,
  "notes": []
}
```

A table is returned only once the document is ready, because a page still being converted or re-read can extend it. Exporting never charges.

| Status | Message | When |
| --- | --- | --- |
| 409 | `Document is converting` | Nothing of the document is available yet. |
| 409 | `table <id> is still being enhanced (R of N pages ready, about M minutes left); tables can be exported once the document is ready` | The document is converting and the pages converted so far have this table. The time left appears only when there is an estimate. |
| 409 | `table <id> is not on the pages converted so far; table ids come from labels such as [table p13-t1 — …] in the page text, so read the page again once the document is ready` | The document is converting and no converted page has this table. |
| 409 | `Document is failed: <reason>` | The conversion failed. |
| 404 | `Table <id> not found in this document` | The document is ready and has no table with this id. |

## Delete a document

`DELETE /v1/documents/{id}` deletes a document at once and answers 204:

```bash
curl -s -X DELETE -H "Authorization: Bearer $MDFI_API_KEY" https://api.mdfi.ai/v1/documents/$ID
```

It removes the document's pages, outline and tables, and the record that recognises the file. Afterwards the id answers 404, and uploading the same file converts and charges it again. An unknown id answers 404 `Document not found`.

A document deleted while it is still converting is charged for the work already done, and its result is not kept. [Data and privacy](data.md) says what mdfi keeps and for how long.

## Error codes

| Status | Meaning |
| --- | --- |
| 200 | Success. For `POST /v1/documents`: your account already has the document. |
| 202 | The upload was accepted and the conversion has started. |
| 204 | The document was deleted. |
| 400 | The request or the file is not valid; the message says why. |
| 401 | No API key, or one mdfi does not accept. |
| 402 | No active plan. The message links to the Billing page. |
| 404 | `upload required`, or the document, section or table does not exist for your account. |
| 409 | The document is still converting or failed, or a section title matches several entries. |
| 413 | The file is over 100 MB. |
| 422 | A field is missing or out of range, such as no `sha256` or `max_tier=4`. The answer lists the fields. |
| 429 | Not enough pages left this month. |
| 500 | An error inside mdfi. Try again; if it persists, [contact us](https://mdfi.ai/#contact). |
| 503 | A service mdfi depends on is briefly unavailable: `Usage service unavailable, try again`, `Storage unavailable, try again`, `Could not register the upload; retry` or `Conversion service unavailable, try again`. Nothing was converted or charged; try again in a minute. |

Error answers carry their message in a JSON `detail` field (for 422, a list of the fields in error). An unexpected 500 may be plain text.

## Older endpoints

The server also has the endpoints of mdfi's first API. They convert a whole file, or a web page from a URL, to a single markdown text, without the per-page scores, outline and table ids of `/v1/documents`:

- `POST /convert`: converts and answers in the same request.
- `POST /jobs`, `GET /jobs/{id}`, `GET /jobs`, `DELETE /jobs/{id}`: background conversions, with an optional `webhook_url` that receives the result.
- `POST /batches`, `GET /batches/{id}`, `GET /batches`, `DELETE /batches/{id}`: up to 50 files and URLs at once.

On mdfi they read at Tier 1 only, and each conversion counts as one weighted page whatever its size. A request is refused with HTTP 429 when no page is left this month; a batch needs one page per item. Uploads are limited to 100 MB: a larger file answers HTTP 413, `File too large (NMB). Maximum is 100MB.` (in a batch, `File '<name>' too large (NMB). Maximum is 100MB.`).

`POST /convert` waits up to 30 seconds for a free conversion slot. If none frees up, it answers HTTP 503 `Server busy converting other documents — use POST /jobs or /v1/documents and poll`.

The `schema` option of these endpoints takes the name of a built-in [template](templates.md#built-in-examples) and adds the fields it extracts to the result. To do that, it sends the whole converted text to a language model; [Data and privacy](data.md#what-leaves-mdfi) names the provider. What these endpoints keep, and for how long, is under [Older endpoints](data.md#older-endpoints) on the same page.

Use `/v1/documents` for new work: it is the API the connector uses, and the only one that re-reads poor pages.

## Templates (coming soon)

A `/v1/templates` API is coming soon. Until then, extraction templates are kept on the dashboard ([Templates](templates.md)).
