Perceive

POST /v2/perceive renders a URL once in a real browser and hands back every output you ask for from that single render: Markdown, cleaned or raw HTML, a screenshot, a PDF, the link and image inventory, and structured data (page metadata, JSON-LD, headings, tables). It replaces a stack of separate calls — url-to-markdown, url-to-screenshot, url-to-pdf, plus your own scraping — with one request.

Here is the smallest useful call. Send a URL, get clean Markdown and the page's structured metadata back:

curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_live_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown", "structured"]
  }'

The response carries a pre-signed download URL for the Markdown file and the structured block inline:

{
    "operation_id": "per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7",
    "status": "completed",
    "url": "https://example.com/pricing",
    "url_final": "https://example.com/pricing",
    "content_hash": "9f2b8c1a...d4e5",
    "render_quality": 0.93,
    "cache_hit": false,
    "outputs": {
        "markdown": {
            "url": "https://spaces.example.com/...signed...",
            "object_key": "env/files/4127/v2-perceive/per_3f9a..._markdown.md",
            "size_bytes": 8421,
            "content_type": "text/markdown; charset=utf-8",
            "expires_in": 900
        }
    },
    "structured": {
        "metadata": {
            "title": "Pricing",
            "description": "Simple, usage-based pricing."
        },
        "structured_data": [
            {"@type": "Product", "name": "Pro", "offers": {"price": "99"}}
        ]
    },
    "extraction_tier": "heuristic",
    "tokens": {"input": 0, "output": 0},
    "cost_cents": 0.0,
    "duration_ms": 6230,
    "warnings": []
}

Worth flagging up front. /v2/perceive is a V2 endpoint with its own quota counter. V1 conversion plans do not include any V2 perceive operations — you need a V2-inclusive plan to call it. See the pricing page for the per-tier allowances.


Endpoints

Method Path Purpose
POST /v2/perceive Perceive a single URL and return the outputs you requested.
GET /v2/perceive/{operation_id} Re-fetch a past operation with freshly signed download URLs.
POST /v2/perceive/batch Perceive up to 1,000 URLs that share one set of options.
GET /v2/perceive/batch/{job_id} Poll the status and per-URL results of a batch.

Content-Type: application/json on every POST.


Authentication

Authenticate with a private key in the X-API-Key header for server-to-server calls. This is the path the examples below use.

X-API-Key: sk_live_your_private_key

Public keys with a JWT bearer token also work, using the same flow as every other endpoint — generate a token with your pk_ key, then send it as Authorization: Bearer <token>. The full flow, including domain locking and token refresh, is in the authentication guide.

Each API key carries an allowed-endpoints allowlist. If /v2/perceive is not on the key's list, the request is rejected with 403.


How perceive works

One request triggers one browser render through a shared headless Chrome singleton, then materialises every output from that render. You never pay for the same page twice in a single call.

  1. Render. The page loads in headless Chrome. Cookie banners are dismissed, the page is scrolled to trigger lazy-loaded content, sticky headers are handled, and images are given time to load — the same capture pipeline that powers the url-to-pdf endpoint.
  2. Materialise. From the rendered DOM, perceive builds whatever you listed in outputs: Markdown, cleaned/raw HTML, links, images, a screenshot, a PDF.
  3. Extract. If you requested structured output, perceive runs a heuristic pass for page metadata, JSON-LD, headings, and tables. If you also send a schema and your plan carries the LLM tier, an Anthropic Claude Haiku model fills the schema when the heuristic pass comes up short.
  4. Score. A render-quality score (0.0–1.0) flags pages that look blocked by anti-bot protection or hidden behind a login wall, so you can tell a real render from a challenge page.

Binary and text outputs (Markdown, HTML, screenshots, PDFs, the link and image JSON) are uploaded to storage and returned as pre-signed URLs that expire after 15 minutes. The structured block is returned inline in the JSON. Re-fetch any operation with GET /v2/perceive/{operation_id} to get a fresh set of signed URLs.


Request parameters

Core

Parameter Type Default Description
url string The page to perceive. Must start with http:// or https://. Max 2,048 characters. Required.
outputs string[] ["markdown", "structured"] Which outputs to produce. See Outputs.
extract string[] [] Which structured fields to pull when structured is in outputs. See Structured extraction.
schema object null A JSON schema describing the fields you want extracted. Triggers the LLM extraction tier on plans that include it.
cache_mode string "enabled" enabled, bypass, or refresh. See Caching.

Outputs

outputs accepts any combination of these names:

Output Returned as What you get
markdown signed URL Clean Markdown from the main content, links and images resolved to absolute URLs.
markdown_fit signed URL "Fit" Markdown — a pruned, denser variant tuned for LLM context windows.
html_cleaned signed URL The rendered HTML with scripts, styles, and boilerplate stripped.
html_raw signed URL The full rendered HTML exactly as the browser produced it.
screenshot signed URL A viewport PNG at the requested (or default) viewport size.
screenshot_full_page signed URL A full-page PNG capturing the entire scroll height.
pdf signed URL A PDF of the page. Accepts the full pdf_options surface (see below).
links signed URL A JSON array of every link found, with absolute URLs and anchor text.
images signed URL A JSON array of every image, with absolute src and alt text.
structured inline JSON Structured data extracted from the page (the structured response field).

Rendering and waiting

Parameter Type Default Description
viewport object 1920 x 1080 {"width": <int>, "height": <int>}. Width 320–3840, height 240–2160.
mobile boolean false Render at a mobile viewport (390 x 844) unless viewport is set explicitly.
wait_for string null Wait after navigation for a CSS selector (".price" or "css:.price") or a JS expression ("js:window.dataReady === true").
wait_timeout_ms integer 30000 How long wait_for may wait, in milliseconds. 0–60,000. A timeout degrades to a warning; the page is captured as-is.
js_code string null JavaScript to run on the page after navigation. Max 20,000 characters. An error becomes a warning, not a failure.
block_resources string[] [] Resource types to abort before they load. Any of image, media, font, stylesheet, script, xhr, fetch, websocket, manifest, other. Useful for faster, text-only renders.
respect_robots boolean false When true, a URL disallowed by the site's robots.txt is rejected with 403.
pdf_options object null Page format, margins, headers, footers, scale, and orientation for pdf output. Same object as url-to-pdf. With no pdf_options, perceive produces a single continuous page, byte-identical to V1 url-to-pdf.

Authenticated and custom requests

Parameter Type Default Description
auth object null HTTP Basic Auth for the target page: {"username": "...", "password": "..."}.
cookies array null Cookies to inject before navigation. Max 50. Each needs name, value, and either domain or url.
headers object null Custom request headers. Max 20. Blocked names: host, content-length, transfer-encoding, connection, upgrade, te, trailer.
Reserved, not yet live. proxy_url (Business+), geolocation, and action_chain are accepted by the request schema but return 422 today. They arrive in a later release; sending them now tells you exactly which knob is not ready instead of silently ignoring it.

Structured extraction

When structured is in outputs, the extract list controls which fields perceive pulls. Ask for nothing and it defaults to metadata and structured_data.

extract value Field in structured Status
metadata metadata Live
structured_data structured_data (JSON-LD) Live
headings headings Live
tables tables Live
main_content main_content (text, capped at 50,000 chars) Live
all expands to every live field above Live
prices Not yet — returns a warning, omitted
contacts Not yet — returns a warning, omitted
technologies Not yet — returns a warning, omitted

To be straight about it: prices, contacts, and technologies are reserved names. Request one today and it does not error — it lands in the warnings array and is dropped from structured.

Schema-driven extraction

Send a schema to pull specific fields into structured.extracted:

{
    "url": "https://example.com/product/widget",
    "outputs": ["markdown", "structured"],
    "schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "price": {"type": "number"},
            "in_stock": {"type": "boolean"}
        }
    }
}

The LLM extraction tier (Anthropic Claude Haiku) fills the schema, and it fires only when all of these hold: you sent a schema, your plan carries the LLM tier (Starter and up), the page was not scored as blocked, and the heuristic pass left schema fields empty. When it runs, extraction_tier is "llm", and tokens and cost_cents report what that extraction cost; otherwise extraction_tier is "heuristic" and both are zero.

Note. Schema extraction is hard-capped to protect your bill: a single extraction is capped per request, and per-project daily spend is capped by plan. If a cap is hit, perceive returns the heuristic result with a note in warnings rather than overspending. On a plan without the LLM tier, you get heuristic structured data only.


Response

Both POST /v2/perceive and GET /v2/perceive/{operation_id} return the same object.

Field Type Description
operation_id string Opaque ID (per_...). Use it with the GET endpoint and quote it to support.
status string queued, processing, completed, or failed.
url string The URL you sent.
url_final string The URL after redirects.
content_hash string SHA-256 of the rendered page. Drives the 1-hour cache.
render_quality number 0.0–1.0. Low scores flag anti-bot challenges or login walls.
cache_hit boolean true when the result came from the cache instead of a fresh render.
outputs object Map of output name to {url, object_key, size_bytes, content_type, expires_in}. Signed URLs expire in 900 seconds.
structured object Inline structured data, present when structured was requested.
extraction_tier string heuristic, css, or llm.
tokens object {input, output} LLM tokens used. Zero unless the LLM tier ran.
cost_cents number LLM cost in cents for this operation. Zero unless the LLM tier ran.
duration_ms integer End-to-end render time.
error string Set only when status is failed.
warnings string[] Non-fatal notes: a wait_for timeout, a skipped extract, a blocked-page flag.

Retrieve an operation

Signed URLs expire after 15 minutes. To download an output later, re-fetch the operation — perceive re-signs every URL from the stored object keys. No re-render happens, so this does not consume quota.

curl https://api.enconvert.com/v2/perceive/per_3f9a2c1b8e7d4a6f90b1c2d3e4f5a6b7 \
  -H "X-API-Key: sk_live_your_private_key"

An unknown operation ID, or one that belongs to a different project, returns 404 — existence is never leaked across projects.


Batch perception

POST /v2/perceive/batch perceives a list of URLs that share one options block. Each URL is rendered through the same pipeline as a single call and produces its own operation row.

curl -X POST https://api.enconvert.com/v2/perceive/batch \
  -H "X-API-Key: sk_live_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com/a",
      "https://example.com/b",
      "https://example.com/c"
    ],
    "options": {"outputs": ["markdown"]},
    "output_mode": "manifest"
  }'

Batches of 10 URLs or fewer run inline and answer 200 with every result populated. Larger batches answer 202 with a job_id; the URLs are drained one at a time and you poll for results:

curl https://api.enconvert.com/v2/perceive/batch/{job_id} \
  -H "X-API-Key: sk_live_your_private_key"

The batch response reports aggregate progress and carries one full perceive result per URL once rendered:

{
    "job_id": "bat_8c1a...",
    "status": "partial",
    "output_mode": "manifest",
    "total": 3,
    "completed": 2,
    "failed": 1,
    "pending": 0,
    "items": [
        {"operation_id": "per_...", "status": "completed", "url": "https://example.com/a"},
        {"operation_id": "per_...", "status": "completed", "url": "https://example.com/b"},
        {"operation_id": "per_...", "status": "failed", "url": "https://example.com/c"}
    ]
}

status is queued, processing, completed, failed, or partial (some URLs succeeded, some failed). Set output_mode to zip to bundle every artifact into a single ZIP, returned under the zip field once the batch finishes. Batch access and the maximum batch size are plan-gated; see the pricing page.


Caching

cache_mode controls how perceive treats its 1-hour result cache, keyed by your project, the URL, and the render-affecting request options.

cache_mode Behaviour
enabled (default) Return a cached result when an identical request rendered within the last hour. cache_hit is true, cost_cents is 0.
bypass Skip the cache and render fresh.
refresh Render fresh and replace the cached entry.

Worth flagging: a cache hit still counts as one perceive operation against your monthly quota. The quota meters operations, not browser renders — the cache saves you render time, not quota.


Code examples

curl — Markdown only

curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_live_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/blog/post",
    "outputs": ["markdown"]
  }'

curl — Markdown plus structured data

curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_live_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "outputs": ["markdown", "structured"],
    "extract": ["metadata", "structured_data", "tables"]
  }'

curl — Full outputs plus PDF

curl -X POST https://api.enconvert.com/v2/perceive \
  -H "X-API-Key: sk_live_your_private_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/report",
    "outputs": ["markdown", "html_cleaned", "screenshot_full_page", "pdf"],
    "pdf_options": {"format": "A4", "print_background": true}
  }'

Python

import requests

response = requests.post(
    "https://api.enconvert.com/v2/perceive",
    headers={"X-API-Key": "sk_live_your_private_key"},
    json={
        "url": "https://example.com/pricing",
        "outputs": ["markdown", "structured"],
        "extract": ["metadata", "tables"],
    },
)
response.raise_for_status()
data = response.json()

# Download the Markdown artifact from its signed URL
markdown_url = data["outputs"]["markdown"]["url"]
markdown_text = requests.get(markdown_url).text

print(data["structured"])
print(markdown_text)

Node.js

const res = await fetch("https://api.enconvert.com/v2/perceive", {
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "X-API-Key": "sk_live_your_private_key"
    },
    body: JSON.stringify({
        url: "https://example.com/pricing",
        outputs: ["markdown", "structured"],
        extract: ["metadata", "tables"]
    })
});

const data = await res.json();

// Download the Markdown artifact from its signed URL
const markdownText = await fetch(data.outputs.markdown.url).then(r => r.text());

console.log(data.structured);
console.log(markdownText);

If you call EnConvert from Claude, Cursor, or another MCP client, the same capability is exposed as the perceive_url tool — see the MCP server page.


Plan gating

V2 perceive operations are a separate counter from V1 conversions. V1 plans include no perceive quota; you need a V2-inclusive plan.

Plan Perceive operations / month Schema extraction (LLM tier)
Free 50 Not included
Starter 850 Claude Haiku
Pro 8,500 Claude Haiku
Business 42,500 Claude Haiku
Enterprise Unlimited Included

Batch access and per-batch size limits, basic-auth/cookie/header support, and proxy_url (Business+, when it ships) are gated by plan. Current numbers live on the pricing page.


Error responses

Status Condition
400 Bad Request URL is not http(s), carries embedded credentials, or resolves to a private, loopback, or link-local address (SSRF protection).
400 Bad Request Invalid auth (missing username/password), cookies (not an array, over 50 entries, missing fields), or headers (not an object, over 20 entries, blocked name).
401 Unauthorized Missing or invalid API key / JWT token.
402 Payment Required Perceive is not on your current plan, or your monthly perceive quota is exhausted.
403 Forbidden /v2/perceive is not in the API key's allowed endpoints.
403 Forbidden Batch not available on your plan, or batch size exceeds your plan's limit.
403 Forbidden respect_robots=true and the site's robots.txt disallows the URL.
404 Not Found Unknown operation_id or job_id, or one owned by another project.
422 Unprocessable Entity Request validation failed (bad enum in outputs/extract, wait_timeout_ms out of range, viewport out of bounds).
422 Unprocessable Entity proxy_url, geolocation, or action_chain was sent — reserved for a later release.
500 Internal Server Error The render failed. The message includes the operation_id to quote to support.

The full status-code reference is in the error-codes guide.


Limits

Limit Value
URL length 2,048 characters
wait_timeout_ms 0–60,000 ms
js_code length 20,000 characters
Viewport width 320–3,840 px
Viewport height 240–2,160 px
Cookies per request 50
Custom headers per request 20
main_content extract 50,000 characters
Batch URLs per request 1,000 (schema cap; your plan's batch limit is lower)
Inline batch threshold 10 URLs (larger batches run asynchronously)
Result cache TTL 1 hour
Signed URL expiry 15 minutes
Monthly perceive operations Plan-dependent (Free: 50)