Contents

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.

Quickstart

  1. Get an API key from your manifest dashboard.

  1. Send a URL, get back a manifest.

cURL

Python

Node

curl -X POST https://manifest.omfang.io/manifest \

-H "X-API-Key: your-key-here" \

-H "Content-Type: application/json" \

-d '{"url": "https://omfang.io/contact"}'

Response:

{

"url": "https://omfang.io/contact",

"authenticated_user": null,

"current_page_state": "Contact page with a form for name, email, and message",

"actions": [

{

"id": "name-input",

"label": "Name",

"type": "input",

"description": "Text field for the sender's name",

"required": true

},

{

"id": "email-input",

"label": "Email",

"type": "input",

"description": "Email field for the sender's contact address",

"required": true

},

{

"id": "message-input",

"label": "Message",

"type": "textarea",

"description": "Free text field for the inquiry message",

"required": true

},

{

"id": "submit-contact-form",

"label": "Send message",

"type": "button",

"description": "Submits the contact form",

"required": false,

"requires": ["name-input", "email-input", "message-input"]

}

],

"navigation": [

{ "label": "Home", "url": "/" },

{ "label": "Manifest", "url": "/manifest" }

]

}

cURL

Python

Node

curl -X POST https://manifest.omfang.io/manifest \

-H "Content-Type: application/json" \

-H "X-API-Key: YOUR_API_KEY" \

-d '{

"url": "https://example.com",

"country": "US"

}'

Response:

{

"url": "https://omfang.io/contact",

"authenticated_user": null,

"current_page_state": "Contact page with a form for name, email, and message",

"actions": [

{

"id": "name-input",

"label": "Name",

"type": "input",

"description": "Text field for the sender's name",

"required": true

},

{

"id": "email-input",

"label": "Email",

"type": "input",

"description": "Email field for the sender's contact address",

"required": true

},

{

"id": "message-input",

"label": "Message",

"type": "textarea",

"description": "Free text field for the inquiry message",

"required": true

},

{

"id": "submit-contact-form",

"label": "Send message",

"type": "button",

"description": "Submits the contact form",

"required": false,

"requires": [["name-input"], ["email-input"], ["message-input"]]

}

],

"navigation": [

{ "label": "Home", "url": "/" },

{ "label": "Manifest", "url": "/manifest" }

],

"requested_country": "US",

"served_locale": "en-US",

"locale_mismatch": false,

"fingerprint": "a3f5c9e1...",

"cache_status": "hit",

"captured_at": "2026-07-26T14:32:07Z",

"expires_at": "2026-07-26T20:32:07Z"

}

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.

Plan

Plan

Manifest calls/ Month

Manifest calls/ Month

Fingerprint calls/ Month

Fingerprint calls/ Month

Free

Free

50

50

250

250

Starter

Starter

1,000

1,000

5,000

5,000

Pro

Pro

5,000

5,000

25,000

25,000

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

Groups of action ids that must be satisfied before this one, ANDed together — any one id within a group satisfies that group (OR). A plain required field is a one-item group. Example: [["size-5","size-6"], ["color-red","color-blue"]] means one size AND one color. Omitted when an action has no dependencies.

Groups of action ids that must be satisfied before this one, ANDed together — any one id within a group satisfies that group (OR). A plain required field is a one-item group. Example: [["size-5","size-6"], ["color-red","color-blue"]] means one size AND one color. Omitted when an action has no dependencies.

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.

omfang logo

Follow us on social media

Contact us

Learn more about omfang

omfang logo

Follow us on social media

Contact us

Learn more about omfang

omfang logo

Follow us on social media

Contact us

Learn more about omfang