Markdown API

Convert any webpage to clean, structured Markdown — navigation, ads and boilerplate stripped. Optionally split it into chunks ready to embed, return only the passages that answer a question, or pull out the page's structure, all from one request.

API Version

v3.0 enables smart defaults — auto_proxy, auto_render, and retry are all on by default. The best proxy and rendering strategy is chosen automatically for each target domain.

Endpoint

HTTP
GET https://opengraph.io/api/3.0/markdown/{encoded_url}?app_id=YOUR_APP_ID

Parameters

Path Parameters

ParameterTypeDescription
encoded_urlstringRequired. URL-encoded target URL

Query Parameters

Grouped by what you are trying to do. If all you want is the page as Markdown, you only need app_id — everything else is optional.

Getting the page

ParameterTypeDefaultDescription
app_idstring-Required. Your API key
only_main_contentbooleantrueStrip navigation, sidebars, and footers — return only the primary content
include_tagsstring-Comma-separated CSS selectors to keep (e.g. article,main,section)
exclude_tagsstring-Comma-separated CSS selectors to drop (e.g. nav,.sidebar,footer)
max_charsinteger-Cap the Markdown at this many characters. When the cap is hit, the response says so rather than returning a short body silently

Choosing what comes back

By default the response body is the Markdown. Ask for format=json and you get an envelope instead, where each section can be switched off if you do not use it.

ParameterTypeDefaultDescription
formatstringmarkdownmarkdown returns a plain text/markdown body. json returns the envelope described below
include_markdownbooleantrueInclude the document itself. Set false when you only want chunks or structure. format=json only
include_metadatabooleantrueInclude the page metadata block. format=json only
include_chunksbooleantrueInclude the chunk array when chunking is on. Turn off to get chunk counts without the text
include_ai_safetybooleantrueInclude the ai_safety block when ai_sanitize=true

Splitting the page for embedding

Set chunking=true and the page comes back already split into pieces sized for a vector database. Each piece carries the heading path it came from, so a chunk retrieved on its own still knows which section of the page it belongs to — which a naive character split cannot tell you.

ParameterTypeDefaultDescription
chunkingbooleanfalseSplit the document into chunks. Requires format=json
chunk_sizeinteger2000Target chunk size in characters (200–20,000). Roughly 4 characters per token
chunk_overlapinteger0Characters repeated from the end of the previous chunk, so a sentence split across a boundary still appears whole in one of them. At most half of chunk_size
heading_awarebooleantrueStart a new chunk at each section heading instead of splitting purely by size
heading_aware_levelinteger2Which heading level starts a new chunk (1–6). 2 means every H2
max_chunksinteger500Most chunks to return (1–2,000). If the page produced more, the response says so and reports the full count

Finding the part that answers a question

Send a question with the request and you get back only the passages that match it, each with a relevance score, instead of the whole page. This is keyword matching against the words on the page — it is fast and free, and it makes no model call. It is not embedding-based semantic search, and it is a different feature from the Query API, which asks a model to answer in prose.

ParameterTypeDefaultDescription
querystring-The question or keywords to match. Implies chunking. Use the words the page would use. Up to 512 characters
query_top_kinteger5How many matching passages to return

Getting the page's structure

Ask for the shape of the page rather than its prose — useful for crawling, auditing, or building a table of contents without parsing the HTML yourself.

ParameterTypeDefaultDescription
include_headingsbooleanfalseReturn the heading outline, plus counts of tables and code blocks found
include_linksbooleanfalseReturn every link from the main content, resolved to an absolute URL
include_imagesbooleanfalseReturn every image with its source and alt text

Controlling the fetch

These behave the same way across our endpoints. On v3 the first three are on by default, so most callers never set them.

ParameterTypeDefaultDescription
auto_renderbooleantrueDetect a JavaScript app shell and re-fetch it rendered. Not billed as a render
auto_proxybooleantrueAutomatically select a proxy for domains known to need one
retrybooleantrueRetry a failed fetch, escalating to a higher proxy tier each attempt
max_retriesinteger4Attempts before giving up (1–4)
retry_escalatebooleantrueWhether each retry moves up a proxy tier, or stays on the one you chose
full_renderbooleanfalseAlways render in a browser first. Use when you need a render guaranteed rather than detected. Adds 5–10 seconds and is billed
wait_for_selectorstring-Wait for this CSS selector to appear before reading the page. Turns rendering on by itself
scroll_to_bottombooleanfalseScroll down to trigger lazy-loaded content. Turns rendering on by itself
load_more_selectorstring-Click this selector up to 3 times to page in more content. Turns rendering on by itself
use_proxy / use_premium / use_superiorbooleanfalseForce a specific proxy tier. Premium and superior require a plan that includes them
proxy_countrystring-Two-letter country code to fetch from, e.g. de. Only applies alongside a proxy
cache_okbooleantrueAllow a cached result
max_cache_ageinteger-Maximum age of an acceptable cached result, in milliseconds
accept_langstring-Accept-Language header sent to the page, e.g. en-US

Checking the page for prompt injection

Web pages can contain text written to hijack a model that reads them. Turn this on and the response tells you what was found and how risky it looks.

ParameterTypeDefaultDescription
ai_sanitizebooleanfalseScan the page for prompt-injection attempts. Costs 1 extra credit
ai_sanitize_modestringsanitizeWhat to do when something is found (only applies when ai_sanitize=true). sanitize — strip the risky content and return cleaned Markdown plus an ai_safety summary. warn — return the original Markdown with the summary attached. block — return a 422 instead of Markdown when the risk is high. Invalid values fall back to sanitize

Response Headers

HeaderPresentDescription
X-Request-IdAlwaysUnique ID for this request — also included as request_id in every JSON body (success or error). Use it when reporting an issue.
X-BILLING-REQUESTSAlwaysCredits billed for this request. Reflects the proxy tier that actually succeeded when retry escalated tiers, not just the tier initially requested.
X-Markdown-Character-Countformat=markdown onlyLength of the returned Markdown body, in characters.
X-Markdown-Truncated / X-Markdown-Truncation-ReasonOnly when truncatedSet when max_chars was exceeded and the body was cut off.
X-AI-Safety-Score / X-AI-Safety-LevelOnly with ai_sanitize=trueRisk score (0–1) and level (low/medium/high) for the fetched page, in format=markdown mode. In format=json mode this is the ai_safety object instead.

Example Request

# Default: plain text/markdown body
curl "https://opengraph.io/api/3.0/markdown/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWeb_scraping?app_id=YOUR_APP_ID"

# format=json: structured envelope with metadata, usage, and debug info
curl "https://opengraph.io/api/3.0/markdown/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWeb_scraping?app_id=YOUR_APP_ID&format=json"

# Split into chunks ready to embed, each keeping its heading path
curl "https://opengraph.io/api/3.0/markdown/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWeb_scraping?app_id=YOUR_APP_ID&format=json&chunking=true&chunk_size=1200&chunk_overlap=100"

# Return only the passages that answer a question
curl -G "https://opengraph.io/api/3.0/markdown/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWeb_scraping?app_id=YOUR_APP_ID&format=json" \
  --data-urlencode "query=legal issues around scraping" \
  -d query_top_k=3

# Structure only — no document body
curl "https://opengraph.io/api/3.0/markdown/https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FWeb_scraping?app_id=YOUR_APP_ID&format=json&include_markdown=false&include_headings=true&include_links=true"

Raw Markdown Response (default)

When format=markdown (default), the response body is unchanged from previous versions of this endpoint — plain text with Content-Type: text/markdown. The headers below are additive.

Response headers
Content-Type: text/markdown
X-Request-Id: 3f1a9c2e-8b4d-4e7a-9c1a-2d6f8e0b5a7c
X-BILLING-REQUESTS: 1
X-Markdown-Character-Count: 4820
# Only present when max_chars was exceeded:
X-Markdown-Truncated: true
X-Markdown-Truncation-Reason: max_chars_exceeded
# Only present when ai_sanitize=true:
X-AI-Safety-Score: 0.12
X-AI-Safety-Level: low

JSON Response (format=json)

When format=json, the response is a structured envelope. ai_safety is only present when ai_sanitize=true was requested — it's omitted entirely otherwise, rather than being sent as null.

Response
{
  "markdown": "# Example Article\n\nMain content here...",
  "metadata": {
    "title": "Example Article Title",
    "description": "A short description from the page's meta tags.",
    "canonical_url": "https://example.com/article",
    "site_name": "Example Site",
    "image": "https://example.com/og-image.jpg",
    "language": "en",
    "final_url": "https://example.com/article"
  },
  "usage": {
    "character_count": 4820,
    "estimated_token_count": 1205,
    "truncated": false,
    "truncation_reason": null,
    "max_chars": null
  },
  "debug": {
    "full_render_used": false,
    "auto_render_triggered": false,
    "proxy_used": null,
    "retry_used": false,
    "retry_attempts": 1,
    "final_status_code": 200
  },
  "request_id": "3f1a9c2e-8b4d-4e7a-9c1a-2d6f8e0b5a7c"
}

Adding ai_sanitize=true attaches an ai_safety object:

ai_safety (when ai_sanitize=true)
"ai_safety": {
  "risk_score": 0.12,
  "risk_level": "low",
  "action_taken": "sanitize",
  "signals": [],
  "recommendation": "No action needed."
}

Chunks (chunking=true)

Each chunk carries the heading path it came from, its own counts, and its position in the document — so a chunk retrieved later still knows where it sat on the page. usage.chunk_count is how many the page produced in total, which can be larger than the number returned.

chunks
"chunks": [
  {
    "index": 0,
    "text": "Install the client with your package manager...",
    "heading_path": ["Getting Started", "Installation"],
    "character_count": 1840,
    "estimated_token_count": 460,
    "start_offset": 0,
    "end_offset": 1840,
    "overlap_character_count": 0,
    "split_mid_block": false
  }
],
"usage": {
  "chunk_count": 12,
  "chunks_truncated": false
}

Matched passages (query=...)

Sending a query returns the same chunk shape with a relevance_score from 0 to 1 added, ordered best match first. The index stays the chunk's position in the page, not its rank — so passage 1 in the list may be chunk 7 of the document.

chunks (with query)
"chunks": [
  {
    "index": 7,
    "text": "Every plan includes a monthly credit allowance...",
    "heading_path": ["Pricing", "Usage limits"],
    "relevance_score": 0.87,
    "character_count": 920,
    "estimated_token_count": 230
  }
]

Structure (include_headings / include_links / include_images)

Returned from the same fetch that produced the Markdown — no second request, no parsing the HTML yourself. Long lists are capped, and the response says so.

structure
"headings": [
  { "level": 1, "text": "Getting Started" },
  { "level": 2, "text": "Installation" }
],
"links": [
  {
    "url": "https://example.com/docs/install",
    "text": "installation guide",
    "rel": null,
    "internal": true
  }
],
"images": [
  { "url": "https://example.com/diagram.png", "alt": "Architecture diagram" }
],
"tables_detected": 2,
"code_blocks_detected": 5

When part of the response can't be produced

Splitting, ranking and structure extraction are optional steps. If one of them fails, the request does not fail — you still get the document, and usage tells you which step did not finish. Check these if a section you asked for is missing.

FieldMeans
usage.chunk_errorChunking or ranking could not complete. The document is still returned
usage.structure_errorHeading, link or image extraction could not complete
usage.chunks_truncatedMore chunks existed than max_chunks allowed. usage.chunk_count has the real total
usage.headings_truncated / links_truncated / images_truncatedThat list hit its cap and was cut short
usage.truncated / truncation_reasonThe Markdown itself was cut off, normally because max_chars was reached

Structured Errors

Every error response — regardless of format — is JSON with an error object and the same request_id that was returned in X-Request-Id. Check error.code, not the HTTP status alone, since several distinct failure modes can share a status code.

Example error response
{
  "error": {
    "code": -2201,
    "message": "Invalid max_chars value. Must be a positive integer."
  },
  "request_id": "3f1a9c2e-8b4d-4e7a-9c1a-2d6f8e0b5a7c"
}
HTTP Statuserror.codeMeaning
400-2200Invalid format — must be markdown or json
400-2201Invalid max_chars — must be a positive integer
400variesThe target URL could not be fetched (DNS failure, timeout, blocked, etc.)
404-4000The fetch succeeded but returned no HTML content
422-4001Blocked by ai_sanitize_mode=block — high prompt-injection risk detected. The response also includes an ai_safety object explaining why.
500-4002HTML-to-Markdown conversion failed unexpectedly

Use Cases

  • LLM and AI content ingestion pipelines
  • RAG (Retrieval-Augmented Generation) data preparation
  • Content migration between platforms
  • Documentation scraping and archival
  • Clean text extraction for NLP processing
  • Safe ingestion of untrusted external pages with AI safety sanitization

Related