---
title: "Fetch a page in one call"
summary: "The same work as the extract call, with the request held open until the page is done and the Markdown returned directly."
category: "developer"
tags: ["kerf", "reference", "render", "synchronous"]
updated: "2026-09-14T04:25:20.237Z"
url: "https://support.quorumtech.ch/articles/kerf-render-url"
markdown: "https://support.quorumtech.ch/articles/kerf-render-url.md"
---

# Fetch a page in one call

`POST https://api.quorumtech.ch/kerf/v1/render`

Fetch and convert a page in one call.

The same work as `POST /kerf/v1/extract/url`, with the request held open until the page is
done and the extraction returned directly rather than as a job. Simpler for a script that
has nothing else to do while it waits; a page that needs a browser can take tens of
seconds, and a client with its own timeout should prefer the job route.

Costs the same: a tenth of a credit a page.

The simplest call to write: one request, one answer, no job to follow. It suits a script that has nothing else to do while it waits, and a page you expect to be quick.

The cost is the timeout. A page that needs a browser can take tens of seconds, and a client with its own deadline will give up while the work carries on here. If you are fetching pages you have not seen before, prefer `POST /kerf/v1/extract/url` with a `wait`: it degrades to a job instead of to a dropped connection.

```json
{
  "url": "https://example.com/"
}
```

## Request

The address, and the few choices that change what comes back. The defaults suit reading a page: the fetch picks its own path, links are kept, images are not, and the site's robots.txt is not consulted.

A JSON body with these fields:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | The page to fetch. `http` or `https`, and a public address. |
| `wait` | integer | no | Milliseconds to hold the request open for a page that finishes quickly, so a short fetch needs no second call. Past it you get a `202` with a job to collect. Ignored by `/kerf/v1/render`, which always waits. |
| `mode` | `auto` or `fast` or `browser` | no | `auto` fetches the page directly and falls back to a browser when what came back is a shell rather than the article. `fast` never opens a browser and fails instead. `browser` always opens one. The price is the same for all three. Default `"auto"`. |
| `furniture` | `drop` or `end` | no | Navigation, cookie notices, share bars and footers: dropped, or kept and moved to the end of the Markdown where they do not interrupt the reading. Default `"drop"`. |
| `hidden` | `include` or `drop` | no | Text the page holds but does not show yet: inactive tabs, closed accordions, collapsed panels. Included, it is written after the visible text under a `<!-- folded -->` comment, which is where a pricing page keeps its yearly prices. `drop` leaves it out. Default `"include"`. |
| `linksEnabled` | boolean | no | Keep links as Markdown links. Off, the text stays and the addresses go. Default `true`. |
| `imagesEnabled` | boolean | no | Include every image with an address. Off, only images carrying a description are kept, as alt text. Default `false`. |
| `followRobots` | boolean | no | Consult the site's robots.txt before fetching, and refuse the page with `robots_disallowed` when it disallows it. Off by default because this fetches one page you named rather than crawling a site. Default `false`. |
| `location` | `us` or `gb` or `de` or `ch` or `in` or `sg` | no | Fetch the page from a country, so prices, currency and terms are the ones a visitor there sees. A region the fleet does not offer answers `400 location_unavailable`. When the country cannot be reached the page is fetched from our own address and `report.locationFallback` says so; with `strictLocation` it answers `424 location_blocked` instead, and nothing is charged. |
| `strictLocation` | boolean | no | With `location`: fail with `location_blocked` rather than fall back to our own address. Nothing is charged for a page that fails this way. Default `false`. |
| `optimiseForRAG` | boolean | no | Shape the Markdown for a reader that cuts it into chunks and retrieves by similarity, so a chunk holds whole facts: a name stays with its figures and a table row keeps its column names. Fewer headings and no Markdown tables come back, so leave it off when people or a table renderer read the result. How the shaping is done changes as the extractor does; depend on the outcome, not on the shape of a line. Default `false`. |
| `humanizeEnabled` | boolean | no | Render in a browser that moves its cursor the way a person does. Slower, rarely needed, and it implies `mode: browser`. Cannot be combined with `location`. Default `false`. |

## Example request

```bash
curl -X POST "https://api.quorumtech.ch/kerf/v1/render" \
  -H "Authorization: Bearer $QUORUM_API_KEY" \
  -H "content-type: application/json" \
  -d '{
  "url": "https://example.com/"
}'
```

## Responses

### 200

The extraction.

| Field | Type | Description |
| --- | --- | --- |
| `url` | string | The address asked for. |
| `finalUrl` | string | The address actually read, after any redirects. |
| `status` | integer | The HTTP status the site answered with. |
| `title` | string | May be null. |
| `markdown` | string | The page, in reading order. |
| `report` | ExtractionReport | What the fetch did, for logging and for deciding whether to trust a thin result. The fields below are stable; the report also carries diagnostics that change as the extractor does, so read it by name rather than by shape. |
| `report.path` | `fast` or `browser` | Whether the page was fetched directly or rendered in a browser. |
| `report.words` | integer | Words in the Markdown. A page that should be an article and is not is usually a wall rather than an empty page. |
| `report.items` | integer | How many blocks the page was read as, counting paragraphs, headings, list items and table rows. |
| `report.tables` | integer | Tables recovered, including ones drawn as boxes rather than marked up as tables. |
| `report.images` | integer |  |
| `report.links` | integer |  |
| `report.uncertain` | boolean | The layout did not settle into one reading order, so the Markdown may not read the way the page does. Browser path only. |
| `report.interstitial` | string | A wall the page put up, when it did: what kind, and whether waiting cleared it. Present only when one was met. |
| `report.fastFallback` | string | Why the direct read was not served, when the page went to a browser after one: `client_rendered`, a `script_filled:` reason, or the direct read's failure. |
| `report.location` | string | The region the page was asked from, when one was. |
| `report.locationFallback` | object | Present when a `location` could not be served and the page came from our own address instead. `to` is `direct`; `reason` is `refused` or `thin`. |
| `report.locationFallback.to` | string |  |
| `report.locationFallback.reason` | string |  |
| `report.optimiseForRAG` | boolean | Whether the Markdown was shaped for retrieval. |
| `renderMs` | integer | Milliseconds spent fetching and converting. |
| `cached` | boolean | Present and `true` when this came from a recent fetch of the same page rather than a new one. |

## Errors

The body is `{ "error": { "code", "message" } }`; the full catalogue with what to do about each code is on the Errors page.

| Status | Codes | When |
| --- | --- | --- |
| 400 |  | The request does not match the schema. `error.issues` names each failing field. |
| 401 |  | The key is missing, not recognised, or revoked. |
| 402 | `payment_required`, `credits_exhausted`, `remaining`, `needed` | The workspace has no active API subscription (`payment_required`), or is prepaid and has no credits left for this request (`credits_exhausted`, with `remaining` and `needed`). Manage billing and add credits at https://developer.quorumtech.ch/billing. |
| 403 | `insufficient_scope`, `url_not_allowed`, `robots_disallowed` | The key is not scoped to Kerf (`insufficient_scope`), the address is one this service does not fetch (`url_not_allowed`: a private or loopback address, or a redirect to one), or the site's robots.txt disallows the page and you asked for it to be followed (`robots_disallowed`). |
| 424 | `site_refused`, `location_blocked`, `load_failed`, `timeout` | The page could not be fetched, and the site is the reason rather than us: it refused to serve the page (`site_refused`, which is the site answering 401, 403, 407, 429 or 451, or a wall it would not lower), the country asked for with `strictLocation` could not serve it (`location_blocked`), or it would not load (`load_failed`, `timeout`). Nothing is charged for a request that answers this, and sending it again unchanged answers the same way. |
| 429 | `rate_limited`, `busy` | Over the key's requests-per-minute limit (`rate_limited`), or the fetching fleet's queue is full (`busy`). Both carry `Retry-After`; a `busy` clears in seconds. |
| 503 | `no_edge_available` | No part of the fetching fleet is answering (`no_edge_available`). Retry shortly. |

## Cost

A tenth of a credit ($0.001) a page, the same as the job route, charged on the answer because the page is in it. A page the site refuses charges nothing.
