Page ranges
The pages option selects which pages of a document to convert. It is the parameter that
determines the bill, so it has one syntax, no ambiguity, and no silent corrections.
curl -sS https://api.kaho.ai/v1/convert \ -H "Authorization: Bearer $KAHO_API_KEY" \ -F "file=@annual-report.pdf" \ -F 'options={"pages":"1-5,9,12-last"};type=application/json'Omitting pages, or sending null, converts the whole document.
Grammar
Section titled “Grammar”pages := WS? range (WS? "," WS? range)* WS?range := endpoint | endpoint WS? "-" WS? endpointendpoint := number | "last"number := [1-9] [0-9]{0,6}WS := (" " | "\t")*Pages are 1-based, and last is the only keyword.
| Expression | Selects |
|---|---|
5 | Page 5 only |
3-7 | Pages 3, 4, 5, 6, 7 |
1-5,9,12-last | Pages 1–5, page 9, and page 12 to the end |
last | The final page |
10-last | Page 10 to the end |
1-last | The whole document |
1 - 5, 9 | Same as 1-5,9. Whitespace is permitted |
What is rejected, and why
Section titled “What is rejected, and why”Every rejection here is deliberate. In a parameter that determines the bill, ambiguity is unacceptable at any convenience price.
| Rejected | Code | Why |
|---|---|---|
-1, -5 | page_range_invalid | A leading - is irreconcilably ambiguous between “the last page”, “up to page 5”, and a stray separator |
5- | page_range_invalid | Redundant with the self-documenting 5-last. The error names the replacement verbatim |
0 | page_range_zero | Pages are 1-based |
007 | page_range_invalid | Leading zeros invite octal confusion |
7-3 | page_range_reversed | Silently swapping would hide a caller bug that produces a wrong bill somewhere else |
last-2 | page_range_invalid | - is the range infix; arithmetic on last re-introduces the ambiguity we just removed |
[1,2,3] | invalid_parameter_type | One syntax |
Normalization
Section titled “Normalization”A selection is normalized before anything is converted, and the canonical result is echoed back to
you as document.pages_selected.
lastresolves to the document’s page count.- Ranges are expanded.
- The results are unioned, deduplicated and sorted ascending.
- Output is emitted in document order, regardless of the order you wrote the ranges in.
Each unique page is billed exactly once, regardless of how many ranges cover it.
| You send | On a 42-page document | Pages selected | Credits |
|---|---|---|---|
1-5,3-7 | Overlapping ranges | 1-7 | 7 |
9,3,1 | Out of order | 1,3,9 | 3 |
5,5,5 | Repeated | 5 | 1 |
40-last | last = 42 | 40-42 | 3 |
| (omitted) | Whole document | 1-42 | 42 |
{ "document": { "page_count": 42, "pages_selected": "1-7", "pages_selected_count": 7 }, "usage": { "pages_billed": 7, "credits_charged": 7, "line_items": [{ "kind": "page_convert", "quantity": 7, "credits": 7 }] }}Out of range
Section titled “Out of range”The two cases behave differently, and the difference is about intent.
A range that partially overshoots is clamped. Asking for 1-50 of a 30-page document gives you
pages 1 to 30, a 200, and an informational page_range_clamped warning. “Give me the first fifty
pages of whatever this is” is a benign idiom and we treat it as one. You are billed for the 30
pages you received.
{ "code": "page_range_clamped", "severity": "info", "scope": "document", "message": "Requested pages 1-50; the document has 30. The selection was clamped to 1-30.", "billed": true}A range entirely beyond the document is an error. Asking for 90-100 of a 30-page document is
422 page_range_out_of_bounds, billed 0. A total miss is never intentional, and returning an empty
success would hide a bug in your caller.
Complexity limits
Section titled “Complexity limits”| Limit | Exceeded gives |
|---|---|
| 2,048 characters in the expression | 422 page_range_too_complex |
| 1,000 ranges in the expression | 422 page_range_too_complex |
More than 200 selected pages on /v1/convert | 413 too_many_pages |
The 200-page ceiling is on the selection, not on the document: a 5,000-page document is fine as long as you select at most 200 pages of it at a time.
for start in $(seq 1 200 5000); do end=$(( start + 199 )) curl -sS https://api.kaho.ai/v1/convert \ -H "Authorization: Bearer $KAHO_API_KEY" \ -H "Idempotency-Key: report-2026-pages-${start}-${end}" \ -H "Content-Type: application/json" \ -d "{\"source\":{\"type\":\"file_id\",\"id\":\"$FILE_ID\"}, \"options\":{\"pages\":\"${start}-${end}\"}}" \ > "part-${start}.json"doneNote the stable idempotency key per window: a retry of any single window costs nothing extra, and the whole loop is safe to re-run.
Page ranges and the bill
Section titled “Page ranges and the bill”How you express the range determines how much is held while the request runs.
| What you send | Credits held |
|---|---|
A bounded expression such as 1-5 or 1-5,9 | Exactly the selected count |
pages: null, or an unbounded range such as 10-last | 200, the endpoint ceiling |
Any expression, converting from an uploaded file_id | Exactly the selected count |
An unbounded range on a freshly uploaded body means we do not know the page count until we parse, so a 200-credit hold — ten cents — is taken and released within one round trip when the request settles. You are never charged the hold; you are charged the settled figure, which is always less than or equal to it.
If ten cents of headroom matters, or you convert the same document repeatedly, upload it first:
FILE_ID=$(curl -sS https://api.kaho.ai/v1/files \ -H "Authorization: Bearer $KAHO_API_KEY" \ -F "file=@annual-report.pdf" | jq -r .id)
curl -sS https://api.kaho.ai/v1/convert \ -H "Authorization: Bearer $KAHO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"source\":{\"type\":\"file_id\",\"id\":\"$FILE_ID\"}, \"options\":{\"pages\":\"12-last\"}}"/v1/toc does not accept pages
Section titled “/v1/toc does not accept pages”Sending pages to /v1/toc is 400 unknown_parameter, and that is the design rather than an
omission. An outline is a property of a document, not of a page range, and a table of contents
inferred from a slice of a document looks complete and is not.
The outline path has no page ceiling at all, because it loads no page. Only the inference path is subject to the 200-page limit, and it is whole-document or nothing.
Page numbers versus page labels
Section titled “Page numbers versus page labels”pages always means the 1-based physical page index — the first page of the file is 1.
PDF page labels are a separate naming scheme: physical page 3 is routinely labelled "iii" and
physical page 5 is routinely labelled "1". Labels appear in document.page_labels and in
/v1/toc entries as page_label, and they are decoration. No label is ever accepted as input
anywhere in the API, so there is no path by which the two can be confused.