> ## Documentation Index
> Fetch the complete documentation index at: https://docs.somark.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# Document catalog tree

> Extract a readable, reusable outline from documents supported by SoMark or from SoMark JSON.

SoMark can identify document heading levels and return a nested catalog tree in `data.result.catalog` of the JSON result. Each catalog node includes its heading text, heading level, page number, and source block index. Use it to build reading navigation, locate the original content, or split content by section—avoiding the loss of section context and poor topic retrieval caused by fixed-length RAG chunking.

| Scenario                                                         | Recommended method                              |
| ---------------------------------------------------------------- | ----------------------------------------------- |
| You are building your own application or service                 | Read `data.result.catalog` from the parsing API |
| You have a document or SoMark JSON and use a Skill-capable agent | Use the `get-catalog-tree` Skill                |

## Method 1: Use the parsing API

<Note>
  The catalog tree is not a separate endpoint. Enable `feature_config.enable_title_level_recognition` during parsing and request `json` output to receive it.
</Note>

<Steps>
  <Step title="Enable title-level recognition">
    Set `enable_title_level_recognition` to `true` in `feature_config`. Its default value is `false`.
  </Step>

  <Step title="Request JSON output">
    Include `json` in `output_formats`. When parsing is complete, read the tree from `data.result.catalog` in the response.
  </Step>

  <Step title="Use catalog nodes as needed">
    Render `children` directly for multi-level navigation. Use `page_num` and `block_idx` to link a catalog entry to its source page and content block.
  </Step>
</Steps>

The following example uses the sync parsing endpoint:

<CodeGroup dropdown>
  ```python Python theme={null}
  import json
  import requests

  url = "https://somark.cn/api/v1/parse/sync"
  data = {
      "output_formats": ["json"],
      "api_key": "sk-***",
      "feature_config": json.dumps({
          "enable_title_level_recognition": True,
      }),
  }

  with open("example.pdf", "rb") as file:
      response = requests.post(
          url,
          data=data,
          files={"file": ("example.pdf", file)},
      )

  response.raise_for_status()
  catalog = response.json()["data"]["result"]["catalog"]
  print(json.dumps(catalog, ensure_ascii=False, indent=2))
  ```

  ```bash cURL theme={null}
  curl -X POST https://somark.cn/api/v1/parse/sync \
    -F "file=@example.pdf" \
    -F "output_formats=json" \
    -F "api_key=sk-***" \
    -F 'feature_config={"enable_title_level_recognition":true}'
  ```

  ```javascript JavaScript theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append(
    "file",
    new Blob([await readFile("example.pdf")], { type: "application/pdf" }),
    "example.pdf",
  );
  form.append("output_formats", "json");
  form.append("api_key", "sk-***");
  form.append("feature_config", JSON.stringify({
    enable_title_level_recognition: true,
  }));

  const response = await fetch("https://somark.cn/api/v1/parse/sync", {
    method: "POST",
    body: form,
  });

  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const result = await response.json();
  console.log(result.data.result.catalog);
  ```

  ```php PHP theme={null}
  <?php
  // Requires: composer require guzzlehttp/guzzle
  require __DIR__ . "/vendor/autoload.php";

  use GuzzleHttp\Client;

  $client = new Client();
  $response = $client->post("https://somark.cn/api/v1/parse/sync", [
      "multipart" => [
          [
              "name" => "file",
              "contents" => fopen("example.pdf", "rb"),
              "filename" => "example.pdf",
          ],
          ["name" => "output_formats", "contents" => "json"],
          ["name" => "api_key", "contents" => "sk-***"],
          [
              "name" => "feature_config",
              "contents" => json_encode([
                  "enable_title_level_recognition" => true,
              ]),
          ],
      ],
  ]);

  $result = json_decode($response->getBody(), true);
  print_r($result["data"]["result"]["catalog"]);
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "fmt"
      "io"
      "mime/multipart"
      "net/http"
      "os"
      "path/filepath"
  )

  func check(err error) {
      if err != nil {
          panic(err)
      }
  }

  func main() {
      file, err := os.Open("example.pdf")
      check(err)
      defer file.Close()

      var body bytes.Buffer
      writer := multipart.NewWriter(&body)
      part, err := writer.CreateFormFile("file", filepath.Base(file.Name()))
      check(err)
      _, err = io.Copy(part, file)
      check(err)

      check(writer.WriteField("output_formats", "json"))
      check(writer.WriteField("api_key", "sk-***"))
      check(writer.WriteField(
          "feature_config",
          `{"enable_title_level_recognition":true}`,
      ))
      check(writer.Close())

      request, err := http.NewRequest(
          http.MethodPost,
          "https://somark.cn/api/v1/parse/sync",
          &body,
      )
      check(err)
      request.Header.Set("Content-Type", writer.FormDataContentType())

      response, err := http.DefaultClient.Do(request)
      check(err)
      defer response.Body.Close()

      fmt.Println(response.Status)
      _, err = io.Copy(os.Stdout, response.Body)
      check(err)
  }
  ```

  ```java Java theme={null}
  // Requires: org.apache.httpcomponents.client5:httpclient5
  import java.io.File;
  import java.nio.charset.StandardCharsets;

  import org.apache.hc.client5.http.classic.methods.HttpPost;
  import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
  import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
  import org.apache.hc.client5.http.impl.classic.HttpClients;
  import org.apache.hc.core5.http.ContentType;
  import org.apache.hc.core5.http.io.entity.EntityUtils;

  public class ParseDocument {
      public static void main(String[] args) throws Exception {
          HttpPost request = new HttpPost(
              "https://somark.cn/api/v1/parse/sync"
          );
          request.setEntity(MultipartEntityBuilder.create()
              .addBinaryBody(
                  "file",
                  new File("example.pdf"),
                  ContentType.APPLICATION_PDF,
                  "example.pdf"
              )
              .addTextBody("output_formats", "json")
              .addTextBody("api_key", "sk-***")
              .addTextBody(
                  "feature_config",
                  "{\"enable_title_level_recognition\":true}",
                  ContentType.APPLICATION_JSON
              )
              .build());

          try (
              CloseableHttpClient client = HttpClients.createDefault();
              CloseableHttpResponse response = client.execute(request)
          ) {
              System.out.println(EntityUtils.toString(
                  response.getEntity(),
                  StandardCharsets.UTF_8
              ));
          }
      }
  }
  ```

  ```ruby Ruby theme={null}
  require "json"
  require "net/http"
  require "uri"

  uri = URI("https://somark.cn/api/v1/parse/sync")
  request = Net::HTTP::Post.new(uri)

  File.open("example.pdf", "rb") do |file|
    request.set_form([
      ["file", file, {
        filename: "example.pdf",
        content_type: "application/pdf"
      }],
      ["output_formats", "json"],
      ["api_key", "sk-***"],
      ["feature_config", JSON.generate({
        enable_title_level_recognition: true
      })]
    ], "multipart/form-data")

    response = Net::HTTP.start(
      uri.hostname,
      uri.port,
      use_ssl: uri.scheme == "https"
    ) { |http| http.request(request) }

    catalog = JSON.parse(response.body).dig("data", "result", "catalog")
    puts JSON.pretty_generate(catalog)
  end
  ```
</CodeGroup>

For the complete request parameters, see [Sync parsing](/en/api-reference/endpoint/sync#feature-config).

## Method 2: Use the get-catalog-tree Skill

The `get-catalog-tree` Skill supports every file format accepted by the SoMark parsing API, including PDF, image, Word, PowerPoint, and Excel files. It can also read SoMark JSON directly. Give the file to a Skill-capable agent to receive a standard nested catalog tree JSON. See [Supported file formats](/en/documentation/index#supported-file-formats) for the complete list.

<Note>
  An outline depends on a clear heading hierarchy. Excel files and other documents that mainly contain tabular data can be parsed, but this feature is not recommended when the source has no outline structure. The resulting outline may be empty or lack meaningful levels.
</Note>

Choose one way to install the Skill before you use it for the first time:

<Tabs>
  <Tab title="Install with an agent">
    Enter this prompt:

    ```text theme={null}
    Install the get-catalog-tree Skill from https://github.com/SoMarkAI/skills.
    ```
  </Tab>

  <Tab title="Install with a command">
    ```bash theme={null}
    npx skills add SoMarkAI/skills --skill get-catalog-tree
    ```
  </Tab>
</Tabs>

After installation, enter this prompt in the agent:

```text theme={null}
Use the SoMark get-catalog-tree Skill to generate a catalog tree JSON for document.pdf.
```

For a source document, the agent asks for one confirmation before starting SoMark parsing. The Skill creates only one file:

```text theme={null}
document.catalog.json     # Standard nested catalog JSON
```

You can also supply SoMark JSON directly:

```text theme={null}
Use the SoMark get-catalog-tree Skill to read result.json and generate a nested catalog tree JSON.
```

Each catalog node preserves `page_num` and `block_idx`, which link it to the corresponding heading block in the original parsed JSON for section chunking and downstream RAG processing.

You can also run the script included with the Skill:

```bash theme={null}
python <skill-directory>/scripts/get_catalog_tree.py --file document.pdf --output catalog
python <skill-directory>/scripts/get_catalog_tree.py --json result.json --output catalog
```

## Response structure

The catalog tree is in `data.result.catalog` as an array of catalog nodes. Top-level nodes normally represent first-level headings; child nodes are recursively organized through `children`.

```json theme={null}
{
  "code": 0,
  "message": "ok",
  "data": {
    "result": {
      "catalog": [
        {
          "page_num": 0,
          "block_idx": 6,
          "title_level": 1,
          "content": "Chapter 1 Introduction",
          "children": [
            {
              "page_num": 0,
              "block_idx": 14,
              "title_level": 2,
              "content": "1.1 Background",
              "children": []
            }
          ]
        }
      ]
    }
  }
}
```

| Field         | Type    | Description                                                                                         |
| ------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `page_num`    | integer | Zero-based page number of the heading. Add `1` if your user-facing display starts from page 1.      |
| `block_idx`   | integer | Index of the heading's content block on that page. Use it to look up and locate the matching block. |
| `title_level` | integer | Heading level: `1` is H1, `2` is H2, and so on.                                                     |
| `content`     | string  | Heading text.                                                                                       |
| `children`    | array   | Next-level heading nodes under this heading. It is an empty array when no child heading exists.     |

## Usage tips

* The catalog nodes are already nested. You do not need to rebuild parent-child relationships from `title_level`.
* To split content by section, treat a node and its descendants as one section range, then use `page_num` and `block_idx` to retrieve its source JSON content blocks.
* Heading levels are inferred from document layout and content. Sample-check results before integration for files with no clear heading hierarchy, low-quality scans, or irregular layouts.
* Keep the feature disabled when you only need document content and do not need section navigation.
