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

# LlamaParse

> Start a parse, poll the job by id, and get page text or markdown at one of four tiers. The page bound you declare settles to what the parse really used.

Parses PDFs and other documents through [LlamaParse](https://developers.llamaindex.ai/llamaparse/) (`https://api.cloud.llamaindex.ai`), returning page text or markdown at the fidelity you pick. A parse is a **job**: you start one, get an id back immediately, and poll until it finishes. Two routes are mounted:

```
POST /llamaparse/v2/parse           → https://api.cloud.llamaindex.ai/api/v2/parse
GET /llamaparse/v2/parse/{job_id}   → https://api.cloud.llamaindex.ai/api/v2/parse/{job_id}
```

LlamaCloud is a broad platform on one shared account key — job listing, file storage, projects and organizations, usage and billing, webhooks, indexing and retrieval. Only the two parse routes above are mounted, as fixed literal routes rather than a wildcard, so every other `/llamaparse/*` path draws the gateway's own `404`, as does either route under any other method.

Like [Mistral](/mistral), these routes are **policed**: a closed body policy runs before LlamaCloud is called, and the request the gateway forwards is rebuilt from the fields that policy admits — nothing rides along inside a nested object. `page_ranges` is **required**, because a job that states its page bound up front is a job that can be priced up front.

## Credential handling

* **`Authorization: Bearer` is injected server-side** with the gateway's LlamaCloud key. Your gateway token never goes upstream.
* The upstream request is built from a **fresh header envelope**. Your header bag is dropped wholesale rather than filtered, so no LlamaCloud key, project or organization selector, or cost-affecting header of your own reaches the upstream.
* **The create route accepts no query parameters.** Every control travels in the JSON body. The status route accepts `expand`, and nothing else.

## Supported scope

Documents are parsed **from a URL**. The gateway hands LlamaCloud a link and LlamaCloud fetches the document from its own network, so `source_url` must be reachable without your credentials — a signed, time-limited object URL is the intended shape.

`source_url` must be a credential-free `https` URL on a public host — a signed object-storage link, a document on your own site, or any public PDF LlamaCloud can reach. Refused with a `400` before LlamaCloud is called: any other scheme (`http` and inline `data:` included), an embedded `user:password`, an IP-literal host in any spelling, and a single-label or `.localhost`/`.local`/`.internal` hostname.

The parse contract is deliberately narrow and complete: a URL, a tier, a version, and a page selection. There are no uploads, callbacks, or model credentials to configure, and nothing to clean up afterwards.

## Request policy

The create body is a closed allow-set of exactly four fields, all required, at most 8,192 bytes. Anything else is rejected with a `400` before LlamaCloud is called — and because the cap counts the whole serialized body, an inline `data:` document could never fit even if the scheme were admitted. Send a URL.

| Field         | What it accepts                                           |
| ------------- | --------------------------------------------------------- |
| `source_url`  | A credential-free `https` URL on a public host.           |
| `tier`        | `fast`, `cost_effective`, `agentic`, or `agentic_plus`.   |
| `version`     | `latest`, or a past `YYYY-MM-DD` calendar version.        |
| `page_ranges` | Exactly one of `max_pages` or `target_pages` — see below. |

### Tiers

`tier` picks the parser and fixes the per-page rate for the whole job.

| Tier             | Relative cost                                                                       |
| ---------------- | ----------------------------------------------------------------------------------- |
| `fast`           | Lowest. Produces no markdown, so `expand=markdown` isn't available on a `fast` job. |
| `cost_effective` | 3× `fast`.                                                                          |
| `agentic`        | 10× `fast`.                                                                         |
| `agentic_plus`   | 45× `fast`.                                                                         |

The tiers are listed in ascending cost order, and higher tiers are the more capable parsers — see [LlamaParse's documentation](https://developers.llamaindex.ai/llamaparse/) for what each one does with a page. The exact per-page prices are in [Billing](#billing) below.

### Versions

`version` pins parser behavior. Send `latest` to follow LlamaParse's current parser, or a past `YYYY-MM-DD` calendar version to keep output stable across a parser update. A future date is refused — it can only be a mistake.

### Selecting pages

`page_ranges` is required and **1-based** — page one of a PDF is `1`. It carries exactly one of two spellings:

```json
{ "page_ranges": { "max_pages": 20 } }
{ "page_ranges": { "target_pages": [1, 2, 7] } }
{ "page_ranges": { "target_pages": "1-4,9" } }
```

`max_pages` is a ceiling from the start of the document, from 1 to 100. `target_pages` is an explicit selection, as an integer array or a string of comma-separated page numbers and ascending `start-end` ranges.

A request may select at most **100 unique pages**, and no page number may exceed `10000`. Sending both spellings is refused as an ambiguous cost basis rather than resolved by a precedence rule you can't see, and so is a repeated or overlapping selection, a descending or half-written range, and an empty one — a request should cost exactly what it looks like it costs.

A rejected call answers `400` with `{"error": "Rejected: <reason>."}`. LlamaCloud never sees it, and it's never billed or reserved against.

## Start a parse

```bash
curl -X POST "$GATEWAY/llamaparse/v2/parse" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "https://example-docs.s3.eu-west-1.amazonaws.com/contracts/msa-2026.pdf?X-Amz-Signature=EXAMPLE",
    "tier": "cost_effective",
    "version": "latest",
    "page_ranges": { "max_pages": 20 }
  }'
```

The answer is narrowed to the three fields you need:

```json
{
  "id": "3f0f2a1e-6c58-4a7a-9f2b-1d4e7c9a5b30",
  "status": "PENDING",
  "tier": "cost_effective"
}
```

**Persist `id` before anything else.** It's the only handle to the job, and to the balance already reserved for it.

## Read the job

```bash
curl "$GATEWAY/llamaparse/v2/parse/$JOB_ID?expand=markdown" \
  -H "Authorization: Bearer $TOKEN"
```

`expand` is optional and takes `text` or `markdown` — the parsed content the job produced, inline. Omit it to read status alone. It may be given once, and `markdown` isn't available on a `fast`-tier job, which produces none.

A job moves through five states:

| Status      | Meaning                                           |
| ----------- | ------------------------------------------------- |
| `PENDING`   | Queued. Keep polling.                             |
| `RUNNING`   | Parsing. Keep polling.                            |
| `COMPLETED` | Finished — the content is available via `expand`. |
| `FAILED`    | Finished without a result.                        |
| `CANCELLED` | Finished, stopped before completion.              |

The last three are terminal; nothing changes after them.

The content arrives **under the key you asked for**, not at the top level, and each
page carries `page_number`. Observed against the live API on 2026-09-10:

```json
{
  "job": {
    "id": "pjb-9rg3kcfdh8l4jpw94n8jbgrx41g3",
    "status": "COMPLETED"
  },
  "text": {
    "pages": [
      { "page_number": 1, "text": "Master Services Agreement\n\nThis Agreement is entered into as of…" },
      { "page_number": 2, "text": "2. Fees\n\nItem  Amount\nPlatform  $4,000" }
    ]
  }
}
```

`?expand=markdown` fills `markdown` in the same shape instead. Every key you did
not ask for is present and `null` — so a response with `"text": null` means the
read carried no `expand`, not that the job produced nothing.

The gateway re-serializes what LlamaCloud returns, dropping shared-account identifiers and its own internal fields; the parsed content itself comes through as it arrived. Field-level detail beyond `job.status` is LlamaParse's own — see [LlamaParse's documentation](https://developers.llamaindex.ai/llamaparse/).

## Ownership

The account that creates a job is the only account that can read it. A job id presented by anyone else answers the same `404` an unknown id answers, with no upstream call made on its behalf — there's no separate "forbidden" status, so nothing about another account's jobs is disclosed, including whether they exist.

A job of your **own** whose results LlamaCloud has since expired also ends in a `404`, but by a different route: that read is authorized, so it reaches LlamaCloud, and the `404` is the upstream's own answer relayed under the gateway's error shape. Treat it the same way in your code — the job is gone — without expecting it to be indistinguishable from the ownership refusal above.

Billing follows the creator too: whoever polls a job, its cost settles against the account that started it.

If a job is created upstream but the gateway can't record it as yours, it answers `500` and doesn't hand back the id — anything reserved for the job is released, and nothing is charged. Retry the request.

## Billing

Priced **per page**, at LlamaCloud's own credit rates with no per-request markup. One upstream credit costs \$0.00125:

| Tier             | Upstream credits/page | Per page      |
| ---------------- | --------------------- | ------------- |
| `fast`           | 1                     | **\$0.00125** |
| `cost_effective` | 3                     | **\$0.00375** |
| `agentic`        | 10                    | **\$0.0125**  |
| `agentic_plus`   | 45                    | **\$0.05625** |

Charging happens in two steps, and the second one is the real one:

1. **On create**, the gateway reserves the job's ceiling — the pages you selected times the tier's rate — against your balance, before LlamaCloud is called. A job that doesn't fit answers `402` naming the price and the page count (`Insufficient credits for this request: 75000 µc for 20 selected page(s). Top up, select fewer pages, or choose a cheaper tier.`) and is never forwarded. A create that produces no reachable job releases the reservation in full.
2. **Once the job finishes and LlamaCloud reports what it used**, that reservation is settled to the upstream's own exact figure — released or adjusted to the real cost, applied once. A verified zero is charged as zero: if the upstream reports the job cost nothing, it costs you nothing.

Because the first step is a ceiling and the second is exact, a job that turns out cheaper than its selection allowed for is charged what it actually cost, once that exact figure arrives. Until then the reservation stands — it is never replaced by an estimate. If the exact figure is delayed, the hold simply stays in place while reconciliation is pending; and in the rare case a job's usage is never reported at all before LlamaCloud stops recognizing the job, the conservative reservation is what you are charged, so the ceiling becomes the final cost for that job.

NativePort adds nothing per request: a parse is charged LlamaCloud's own metered cost, unmarked up. The gateway's only fee is applied when you add credits; see [pricing](https://nativeport.ai/pricing/).

**There is no retry on the create route.** LlamaParse documents no idempotency key for parse creation, so a silent second attempt would parse — and bill — the same document twice.

If a create times out ambiguously, **don't resend it blind**: the job may already exist upstream. Persist the job id from any response you did receive and poll the status route. A deliberate retry is your call, and counts as a new, separately priced job.

## Errors

| Status | What happened                                                                                                                            |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | The body or query policy rejected the call. Nothing reached LlamaCloud, and nothing was billed or reserved.                              |
| `401`  | The bearer token is missing, unknown, or revoked.                                                                                        |
| `402`  | Either the account balance is empty, or this job's own reserved cost exceeds it. The message says which.                                 |
| `404`  | No such route, a job id this account doesn't own, or a job of your own whose results LlamaCloud no longer has.                           |
| `500`  | The job couldn't be registered to your account, so it was discarded. Nothing charged — retry.                                            |
| `502`  | The upstream call failed, its credential was refused, or it returned something the gateway won't forward. Anything reserved is released. |

An upstream **credential** failure is masked: an upstream `401` or `403` comes back to you as the gateway's own `502`, never under the upstream's status, so nothing about the shared LlamaCloud credential — including whether it was the thing that failed — is exposed. A `2xx` whose body the gateway can't safely read is also answered as `502`.

Every other ordinary upstream refusal keeps **its** own status, so you can tell "I sent something wrong" from "back off" from "the upstream is down"; only the body is replaced, with the gateway's own `{"error": …}` shape naming that status. LlamaCloud's error payloads are never relayed, since they can echo your signed source URL or document content. A `retry-after` the upstream sends is carried over.

## Privacy

Parse input tends to be contracts, invoices, IDs, and medical records, so both log records these routes write keep **metadata only** — method, path, status, latency, and a fixed allowlist of transport headers (content type and length, user agent, the two `accept` headers, and the CDN's request id and country code). Headers are allowlisted rather than redacted, so a credential header under a name no redaction list knows still never reaches a log.

The query string is dropped whole rather than filtered, and request and response bodies are never stored — so neither the source URL (it travels in the body) nor a line of parsed text lands in a log. Usage records keep the tier, page counts, and rate — never document content.