---
title: "Extract clean text content from HTML with various formatting options. A url is fetched once (15 s timeout; a 429 or 503 whose Retry-After is 10 s or less is retried once after that wait). A PDF or other binary returns 415 UNSUPPORTED_CONTENT_TYPE with next_step process_document; a JSON body is returned as text with a warning. A bot wall is a BLOCKED error, unbilled; with escalate: true the same call renders the page once in a stealth browser and extracts the text from that instead. An empty client-side shell succeeds with rendered: false and the warning \"page renders client-side; use scrape\"."
method: POST
path: "/tools/extract_text"
tags: ["tools"]
---

# Extract clean text content from HTML with various formatting options. A url is fetched once (15 s timeout; a 429 or 503 whose Retry-After is 10 s or less is retried once after that wait). A PDF or other binary returns 415 UNSUPPORTED_CONTENT_TYPE with next_step process_document; a JSON body is returned as text with a warning. A bot wall is a BLOCKED error, unbilled; with escalate: true the same call renders the page once in a stealth browser and extracts the text from that instead. An empty client-side shell succeeds with rendered: false and the warning "page renders client-side; use scrape".

`POST /tools/extract_text`

## Request

- Base URL: `https://www.crawlforge.dev/api/v1`
- URL: `https://www.crawlforge.dev/api/v1/tools/extract_text`
- Auth: API key in header `X-API-Key`

## Request body

- union
  - object
    - `html` string, required: HTML content to extract text from
    - `url` string, uri: URL to fetch and extract text from
    - `selector` string: CSS selector to target specific elements
    - `clean` boolean: Collapse runs of spaces within a line and drop blank lines; line breaks between blocks are kept. Text mode only.
    - `preserve_links` boolean: Append the links as "text (href)" lines after the text. REST only (the MCP tool has no such option); text mode only.
    - `preserve_formatting` boolean: Keep paragraph and heading breaks from the HTML source. REST only (the MCP tool has no such option); text mode only.
    - `max_length` number: Maximum length of the returned text or markdown; a cut text ends in "..." and data.truncated is true (data.truncated is always present, false when nothing was cut)
    - `output_format` 'text' | 'markdown': text (default): data.text, one line per block element. markdown: data.markdown, the selected elements (or the body) converted with Turndown, data tables as GFM pipe tables and image and link URLs absolute. Either way data carries output_format, word_count and char_count, named as the MCP tool names them; markdown is counted without its # * ` _ [ ] syntax.
    - `respect_robots` boolean
    - `escalate` boolean: Retry a walled page in a stealth browser, in this same call (url input only). The plain fetch always runs first; only a named bot wall, a 403, 429 or 444, or an empty shell or error placeholder served as 2xx escalates — never a 404, a 5xx, a binary or a host whose Retry-After window is still open. The response carries escalated: true|false whenever this is set, plus stealth: { engine, vendor_detected } when the render ran. It costs the escalation add-on only when it ran. The stage needs the CrawlForge execution backend: where that is not configured it returns 503 TOOL_NOT_AVAILABLE and charges nothing. robots.txt is respected on the escalated path too.
    - `escalate_engine` 'playwright' | 'camoufox': Browser engine for the escalated render (camoufox only where installed on the backend). Ignored when no escalation runs.
    - `redact_pii` union: Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: "fast" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style ("tag" → <EMAIL>, "mask" → [REDACTED], "remove" → nothing; default "tag") and mode ("fast", the default, is regex-only and costs no extra credits; "model" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction.
      - boolean
      - object
        - `entities` string[]
        - `replace_style` 'tag' | 'mask' | 'remove'
        - `mode` 'fast' | 'model'
  - object
    - `html` string: HTML content to extract text from
    - `url` string, uri, required: URL to fetch and extract text from
    - `selector` string: CSS selector to target specific elements
    - `clean` boolean: Collapse runs of spaces within a line and drop blank lines; line breaks between blocks are kept. Text mode only.
    - `preserve_links` boolean: Append the links as "text (href)" lines after the text. REST only (the MCP tool has no such option); text mode only.
    - `preserve_formatting` boolean: Keep paragraph and heading breaks from the HTML source. REST only (the MCP tool has no such option); text mode only.
    - `max_length` number: Maximum length of the returned text or markdown; a cut text ends in "..." and data.truncated is true (data.truncated is always present, false when nothing was cut)
    - `output_format` 'text' | 'markdown': text (default): data.text, one line per block element. markdown: data.markdown, the selected elements (or the body) converted with Turndown, data tables as GFM pipe tables and image and link URLs absolute. Either way data carries output_format, word_count and char_count, named as the MCP tool names them; markdown is counted without its # * ` _ [ ] syntax.
    - `respect_robots` boolean
    - `escalate` boolean: Retry a walled page in a stealth browser, in this same call (url input only). The plain fetch always runs first; only a named bot wall, a 403, 429 or 444, or an empty shell or error placeholder served as 2xx escalates — never a 404, a 5xx, a binary or a host whose Retry-After window is still open. The response carries escalated: true|false whenever this is set, plus stealth: { engine, vendor_detected } when the render ran. It costs the escalation add-on only when it ran. The stage needs the CrawlForge execution backend: where that is not configured it returns 503 TOOL_NOT_AVAILABLE and charges nothing. robots.txt is respected on the escalated path too.
    - `escalate_engine` 'playwright' | 'camoufox': Browser engine for the escalated render (camoufox only where installed on the backend). Ignored when no escalation runs.
    - `redact_pii` union: Remove personal data from the text this call returns, before it is stored or sent back. true is shorthand for { mode: "fast" }: every entity, tagged. As an object: entities (any of EMAIL, PHONE, FINANCIAL, SECRET; omitted or empty means all four, and any other name is a 400 rather than a silent no-op), replace_style ("tag" → <EMAIL>, "mask" → [REDACTED], "remove" → nothing; default "tag") and mode ("fast", the default, is regex-only and costs no extra credits; "model" covers PERSON and LOCATION, needs an LLM and is rejected here — use the CrawlForge MCP server). The response carries redaction: { entities, count, mode } inside data, saying what was removed. Detection is deliberately conservative: a card number must pass Luhn and an IBAN mod-97, so a false positive cannot silently destroy real page content. URLs, queries and identifiers the response uses to name what was fetched are left intact, and counters derived from the text (content_length, word_count, character_count) describe the text as it was extracted, before redaction.
      - boolean
      - object
        - `entities` string[]
        - `replace_style` 'tag' | 'mask' | 'remove'
        - `mode` 'fast' | 'model'

## Response `200`

The tool's result; 1 credits charged (see x-credits-note and x-extra for add-ons).

- ToolSuccess: The envelope every tool returns on success. Credits are deducted only after the tool succeeded, so credits_used is the price of this call and an error response never charges.
  - `success` true, required
  - `data` object, required: The tool's result. Its shape is tool-specific and documented on the tool's docs page (x-docs-url on the operation).
  - `credits_used` integer, required: Credits charged for this call: the operation's x-credits, plus any add-on x-credits-note or x-extra describes.
  - `credits_remaining` integer, required: The account's credit balance after this call.
  - `processing_time` integer, required: Server-side processing time in milliseconds.
  - `warnings` string[]: Non-fatal notes: a robots.txt override that was recorded, a format that could not be produced, a redaction that changed the payload.

## Other responses

- `400`: VALIDATION_ERROR: the body does not match the request schema.
- `401`: MISSING_API_KEY or INVALID_API_KEY.
- `402`: INSUFFICIENT_CREDITS: the balance does not cover the tool's base price. X-Auto-Recharge-Triggered says whether an automatic recharge was started; retry once it lands.
- `429`: RATE_LIMIT_EXCEEDED: your plan's rate limit. Retry-After: 60.
- `500`: TOOL_ERROR, INTERNAL_ERROR or CREDIT_DEDUCTION_FAILED.
- `default`: Any other error. Which of 403, 404, 413, 422, 501, 502, 503 and 504 a tool can return depends on what it does (fetching a page, calling the execution backend, reading stored results); the code vocabulary is listed on the ErrorResponse schema.

---

[API](https://skmtc.dev/crawlforge/apis/crawlforge-rest-api.md) · [All operations](https://skmtc.dev/crawlforge/apis/crawlforge-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/crawlforge/apis/crawlforge-rest-api/revisions/a5532fa8d5d5?raw)
