List PDF bookmarks
Turn a PDF outline tree into a clean, ordered bookmark list that is easy to inspect, test, export, or feed into another workflow.
Run — free
Runs in your browser. Free, unlimited — your data never leaves this page.
Each returned item contains the bookmark title, its one-based nesting level, and its one-based target page. The traversal preserves the document’s outline order and places every child immediately after its parent. Empty outlines are rejected explicitly, so a missing table of contents cannot be mistaken for a successful result with no data.
Convert a nested outline into practical rows
PDF bookmarks are usually represented as an outline tree. Chapters sit at the top level, sections appear below chapters, and deeper headings may continue for several more levels. That structure is useful in a viewer, but it is awkward when you need to audit navigation, create a report, compare two editions, or send the information to a spreadsheet. This capability walks the outline in pre-order: it emits a parent first, then each of that parent’s descendants in their original order, before moving to the next sibling. Every output row contains only the information needed for a bookmark inventory: title, level, and target page. Levels start at one, matching how people normally describe top-level chapters, and pages are also one-based. The resulting list remains faithful to the visible reading order while removing recursive nesting. It does not open or modify a PDF file; it accepts the outline tree already obtained from a PDF parser or document-processing step.
Understand validation and deterministic ordering
Each bookmark must be an object with a non-empty title and a positive integer page. Optional children must be an array containing more bookmark objects. Whitespace around a title is removed so accidental padding does not leak into reports, but title text inside that boundary is preserved exactly. The algorithm never sorts bookmarks, because alphabetical or page-based sorting could destroy the author’s intended outline order. Instead, it follows the sequence supplied by the parser and assigns the level from the bookmark’s depth in the tree. The same input therefore always produces the same output, without network calls, timestamps, random identifiers, or environment-dependent behavior. An empty top-level outline returns an invalid-input error rather than an empty success. That distinction is useful in automation: a pipeline can stop when a document has no bookmarks instead of publishing an apparently valid, blank navigation index. Malformed child collections, missing titles, invalid page targets, excessive depth, cycles, and unreasonably large trees are rejected with focused messages.
Use the list in document workflows
A flat bookmark list works well as a boundary between PDF parsing and later business logic. You can render it as a table of contents, check that every expected chapter is present, compare titles and page destinations across document versions, or map pages to section names for downstream extraction. Because the response includes both count and ordered bookmark records, simple systems can verify the total before processing individual entries. The level value lets a consumer reconstruct indentation or rebuild a tree when necessary, while the page value supports navigation and page-range calculations. Keep in mind that this capability reports the outline it receives; it does not verify that target pages exist in a particular PDF, infer headings from page text, or repair broken destinations. Validate page bounds when the source parser provides the PDF page count. Browser execution is available for interactive work, and API requests cost $0.002 when you need repeatable automation. In both channels, the shared deterministic implementation returns the same structured result.
What you can do with it
Audit document navigation
Review every bookmark in visible order and spot missing chapters, unexpected nesting, or invalid targets before publication.
Build a table of contents
Turn parser output into ordered rows that can be indented by level and linked to their target pages.
Compare PDF editions
Create stable bookmark inventories for two versions and compare their titles, hierarchy, order, and page destinations.
FAQ
Does this capability read the PDF file itself?
No. It accepts an outline tree produced by a PDF parser and flattens that tree into bookmark records.
What order does the result use?
It uses pre-order traversal: each parent appears before its children, and siblings remain in the supplied document order.
How are bookmark levels numbered?
Top-level bookmarks have level 1. Their children have level 2, and each additional nesting step increases the level by one.
What happens when the outline is empty?
The request fails with an invalid-input error so an absent bookmark outline cannot be confused with a valid empty report.
Does it check whether target pages exist?
It requires every target to be a positive integer, but it cannot compare that value with a PDF page count unless your workflow performs that separate check.
What does an API request cost?
An API request costs $0.002. You can also run the same deterministic logic free in your browser.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/pdf/bookmarks-list \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"outline":[{"title":"Introduction","page":1,"children":[{"title":"Background","page":3}]},{"title":"Methods","page":8}]}'const res = await fetch("https://api.kit.forhosting.com/pdf/bookmarks-list", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"outline": [
{
"title": "Introduction",
"page": 1,
"children": [
{
"title": "Background",
"page": 3
}
]
},
{
"title": "Methods",
"page": 8
}
]
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/pdf/bookmarks-list",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"outline": [
{
"title": "Introduction",
"page": 1,
"children": [
{
"title": "Background",
"page": 3
}
]
},
{
"title": "Methods",
"page": 8
}
]
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/pdf/bookmarks-list", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"outline":[{"title":"Introduction","page":1,"children":[{"title":"Background","page":3}]},{"title":"Methods","page":8}]}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"outline":[{"title":"Introduction","page":1,"children":[{"title":"Background","page":3}]},{"title":"Methods","page":8}]}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/pdf/bookmarks-list", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"outline": [
{
"title": "Introduction",
"page": 1,
"children": [
{
"title": "Background",
"page": 3
}
]
},
{
"title": "Methods",
"page": 8
}
]
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "pdf.bookmarks_list",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 25 |
max_pages | 200 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |