Getting started
Manifest API makes any webpage agent-readable. Pass a URL, get back a structured JSON manifest describing what the page contains and what actions are available.
country (optional) — ISO 3166-1 alpha-2 code (e.g. "US", "DE"). Sets the scan's Accept-Language header, and for some sites, an additional locale cookie — best-effort steering of server-side geo/locale redirects, not a guarantee. Check locale_mismatch in the response either way.
storage_state (optional) — a Playwright storage_state object containing cookies and origins. The scan loads the URL with this session so the manifest reflects the authenticated view. Send it in the POST body, never as a query parameter.
Geo/locale honesty, not correction. Some sites redirect based on IP, headers, or a stored locale — Manifest doesn't attempt to override this universally or convert prices. country improves the odds of landing on the right locale for sites we've mapped, but for others it has no effect (and rarely, can even shift which wrong locale you get). locale_mismatch is the field to trust: if it's true, treat prices, availability, and copy on that page as locale-specific, not the one you asked for.
Authentication
All requests require an API key passed in the X-API-Key header. Keys are issued from your dashboard and can be rotated or revoked at any time.
X-API-Key: your-key-here
Requests without a valid key return
401 Unauthorized.
Endpoints
POST/manifest
Returns a structured action manifest for any URL.
Request:
JSON
{
“url”: “https://example.com”
“storage_state”: { “cookies”: [ … ], “origins”: [ … ] }
}
Response:
JSON
{
“url”: “string”,
“authenticated_user”: “string | null”,
“current_page_state”: “string”,
“actions”: [
{
“id”: “string”,
“label”: “string”,
“type”: “button | input | textarea | select | checkbox | radio | other”,
“description”: “string”,
“required”: “boolean”,
“requires”: [[“string”]]
}
],
“navigation”: [
{ “label”: “label”, “url”: “string” }
],
“fingerprint”: “string”,
“cache_status”: “hit | miss | bypass”,
“session_domains”: [“string”] | null,
“captured_at”: “ISO-8601 timestamp”,
“expires_at”: “ISO-8601 timestamp”
}
Authenticated requests
Pass a storage_state to perceive a page behind a login. You supply an already-valid session — Manifest does no login automation, credential storage, 2FA or challenge handling, or session refresh.
Obtaining storage_state: use context.storage_state() from your own logged-in Playwright browser context, or provide the contents of a saved storage_state.json. Authenticated requests bypass the shared cache entirely: they are never read from it or written to it, and cache_status is “bypass”.
A malformed storage_state returns 400. A stale or expired session returns 503 with a message identifying the supplied session as the cause — Manifest never silently falls back to an anonymous fetch.
Privacy and security: authenticated page content, which may include personal data, is sent to Anthropic for extraction through the same path every scan uses and is not persisted. A supplied storage_state is held in memory for one request only, never written to disk, a database, or a cache, scrubbed from logs, errors, and traces, and dropped when the browser context closes. Scope it as narrowly as possible and check session_domains in the response to confirm what you sent.
Available in the Python SDK (manifest-api ≥ 0.4.0, client.get(url, storage_state=…)). The JavaScript SDK has no equivalent yet.
curl -X POST https://manifest.omfang.io/manifest \
-H “Content-Type: application/json” \
-H “X-API-Key: YOUR_API_KEY” \
-d ‘{
“url”: “https://app.example.com/dashboard”,
“storage_state”: { “cookies”: [ … ], “origins”: [ … ] }
}’
POST/manifest/from-dom
Returns a structured action manifest from a dom_context you captured yourself, instead of letting Manifest render the page with its own browser.
Request:
JSON
{
“url”: “https://example.com”,
“dom_context”: { “interactive_elements”: [ … ] },
“fresh”: false,
“cache_scope”: “string | null”,
“previous_manifest”: { … } | null,
“last_action_id”: “string | null”,
“goal”: “string | null”
}
Response:
JSON
{
“url”: “string”,
“authenticated_user”: “string | null”,
“current_page_state”: “string”,
“actions”: [
{
“id”: “string”,
“label”: “string”,
“type”: “button | input | textarea | select | checkbox | radio | other”,
“description”: “string”,
“required”: “boolean”,
“requires”: [[“string”]],
“rebinds_on”: [“string”]
}
],
“navigation”: [
{ “label”: “label”, “url”: “string” }
],
“fingerprint”: “string”,
“cache_status”: “hit | miss | bypass”,
“captured_at”: “ISO-8601 timestamp”,
“expires_at”: “ISO-8601 timestamp”
}
DOM-based extraction
Use /manifest/from-dom when the state that matters isn’t reachable by re-navigating to a URL — a client-side-only overlay, a wizard step, anything that never touched the URL. Capture dom_context in your own already-open page and submit that directly; no storage_state needed, your page already carries whatever session got it to this state.
cache_scope (opaque, non-identifying, e.g. a hash of the session id) enables caching for that url+scope pair — omit it (the default) for anything whose DOM state changes between calls, like a wizard step, since caching that would serve a stale snapshot back on the next call. This response has no requested_url, redirected, or session_domains fields (nothing here does its own navigation), but does still carry fingerprint, computed directly from the submitted dom_context.
previous_manifest/last_action_id feed the shared/singleton-rebinding detector: pass the Manifest you got back from your previous call on this same page, and the id of whichever action you invoked since then, so a fill target that silently rebound to a different prior selection gets flagged via rebinds_on on the affected action. Both-or-neither — omit both (the default) to skip detection.
goal, when given, pre-filters dom_context.interactive_elements to what’s plausibly relevant before the manifest call, cutting latency on large, noisy authenticated pages like a social feed. It’s gated server-side and off by default, so passing it today is always safe and a no-op until the server has it enabled; any filter-call failure fails open to sending everything, never to sending too little.
Available in the Python SDK (manifest-api ≥ 0.5.0, client.get_from_dom(url, dom_context)); previous_manifest/last_action_id since 0.6.0, goal since 0.7.0. The JavaScript SDK has no equivalent yet.
curl -X POST https://manifest.omfang.io/manifest/from-dom \\
-H “Content-Type: application/json” \\
-H “X-API-Key: YOUR_API_KEY” \\
-d ‘{
“url”: “https://www.linkedin.com/sharing/compose”,
“dom_context”: { “interactive_elements”: [ … ] },
“goal”: “Publish a short text post as the Omfang AB company page.”
}’
POST /fingerprint
GET /fingerprint?url=
Returns a stable hash of a page's interactive surface — no LLM call, never cached. Use it to cheaply check whether a page's actions have changed since your last /manifest call, before paying for a new manifest generation.
Request:
bash
curl -X POST https://manifest.omfang.io/fingerprint \ -H "X-API-Key: your-key" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com"}'
Or as a GET:
bash
curl "https://manifest.omfang.io/fingerprint?url=https://example.com" \ -H "X-API-Key: your-key"
Response:
JSON
{ "url": "https://example.com", "fingerprint": "a3f5c9e1..." }
fingerprint is a SHA-256 hex digest computed from the page's interactive elements (role, name, input type). It's stable across unrelated layout shifts and DOM reordering — an ad or news ticker inserted earlier in the page won't change it — but it changes the moment a button, field, or link is added, removed, or renamed.
Every /manifest response also includes this same field, so you can compare a manifest you already fetched against a later /fingerprint call without re-running the full pipeline:
JSON
{ "url": "https://example.com", "actions": [ ... ], "fingerprint": "a3f5c9e1..." }
When to use it
If you're polling a page you've already generated a manifest for — waiting for a form to appear, checking whether a checkout flow changed — call /fingerprint on a short interval instead of /manifest. Only re-fetch the full manifest when the hash changes. This skips the LLM translation step entirely, so it's far cheaper per call than /manifest.
Limits
/fingerprint has its own monthly quota, separate from /manifest — exhausting one never blocks the other.
Rate limits (requests/minute) are shared with /manifest per plan.
SDK
python
fp = client.fingerprint(
"https://example.com"
)
print(fp.url, fp.fingerprint)
# or compare against an existing manifest
manifest =client.get(
"https://example.com"
)
if manifest.fingerprint != client.fingerprint(
"https://example.com"
).fingerprint:
manifest =client.get(
"https://example.com", fresh=True)
GET/health
Returns the API health status. No authentication required.
JSON
{ "status": "ok" }
GET/session-status
Returns whether the current browser session is valid. No authentication required
JSON
{ "valid": true }
or
JSON
{ "valid": false, "message": "Session expired" }
Response fields:
Field
Type
Description
url
string
The url that was requested
authenticated_user
string or null
Detected logged-in user, if any
current_page_state
string
Brief description of the page
actions
array
Interactive elements on the page
navigation
array
Primary navigation links
fingerprint
string
SHA-256 hash of the page's interactive surface — see /fingerprint
cache_status
"hit" | | "miss" | "bypass"
hit | miss | bypass — bypass when a storage_state was supplied
requested_country
string or null
The country param you sent, echoed back. null if not provided.
served_locale
string or null
Best-effort detected locale of the page actually captured (e.g. "en-DE"). null if no locale signal was found.
locale_mismatch
boolean
true if you requested a country and the page served a different locale — the site geo-routed elsewhere despite the request. false if they matched, or if no country was requested.
captured_at
ISO-8601 timestamp
When the page was actually scanned
expires_at
ISO-8601 timestamp
When this cached manifest expires
session_domains
array | null
Domains/origins the supplied storage_state was scoped to (names only). null when no session was supplied.
Action fields:
Field
Type
Description
id
string
Unique identifier for the action
label
string
Human-readable label
type
string
button, input, textarea, select, checkbox, radio, other
description
string
What the action does
required
boolean
Whether the field is required
requires
array of arrays of strings | omitted
Rate limits
Plans
Calls/month
Per-minute limit
Free
50
10/min
Starter
1,000
10/min
Pro
5,000
10/min
Exceeding the per-minute limits returns
429 Too Many Requests
Error codes
Code
Meaning
401
Missing or invalid API key
429
Rate limit exceeded
503
Browser session unavailable, or a supplied storage_state is expired — retry with a fresh session
400
Malformed request — e.g. storage_state is not a valid Playwright storage_state object
Cache & Freshness
Manifest caches anonymous page extractions for 6 hours to keep repeated requests fast and cheap. Authenticated requests (those carrying a storage_state) are never read from cache nor written to it. Every response tells you exactly how fresh that data is:
Field
Type
Description
cache_status
"hit" | | "miss" | "bypass"
Whether this response came from cache or a live scan. Bypass when a storage_state was supplied.
captured_at
ISO-8601 timestamp
When the page was actually scanned
expires_at
ISO-8601 timestamp
When this cached manifest expires (captured_at + 6h)
By default, requests may return a cached manifest up to 6 hours old. If your workflow involves forms, pricing, inventory, or any destructive action, pass fresh=true to bypass the cache and force a live scan — this counts as one billable API call.
JSON
// Request
POST /manifest
{ "url": "https://example.com/checkout", "fresh": true }
// Response
{
"cache_status": "miss",
"captured_at": "2026-07-26T14:32:07Z", "expires_at": "2026-07-26T20:32:07Z", "current_page_state": { ... }, "actions": [ ... ]
}
Rule of thumb: use the default cache for read-heavy or exploratory calls. Use fresh=true whenever the agent is about to submit a form, complete a purchase, or take any action where a stale DOM could cause a bad outcome.