Skip to main content

manifest-api

Stop guessing selectors. Manifest tells your agent what's clickable, fillable, and submittable on any page.

Python SDK for the Manifest API — extracts structured action manifests from web pages so AI agents know what they can do, not just what's on screen.

Raw browser access Content extraction Manifest
Gives agents a browser ✓ ✗ ✓
Returns page content ✓ ✓ ✓
Returns available actions ✗ ✗ ✓
Required fields, input types ✗ ✗ ✓
Survives UI redesigns ✗ ✓ ✓

Install

pip install manifest-api

Quickstart

Sync

from manifest_api import ManifestClient

client = ManifestClient(api_key="your-key")  # or set MANIFEST_API_KEY env var
manifest = client.get("https://example.com")

print(manifest.current_page_state)
print(manifest.actions)

# Convenience helpers
action = manifest.action("submit-form")
inputs = manifest.actions_of_type("input")
required = manifest.required_actions

# Cheap structural-change check (no LLM call, never cached)
fp = client.fingerprint("https://example.com")
print(fp.fingerprint)

Async

import asyncio
from manifest_api import AsyncManifestClient

async def main():
    async with AsyncManifestClient(api_key="your-key") as client:
        manifest = await client.get("https://example.com")
        print(manifest.current_page_state)

asyncio.run(main())

LangChain

pip install "manifest-api[langchain]"
from manifest_api.integrations.langchain import create_manifest_tool

tool = create_manifest_tool()  # reads MANIFEST_API_KEY from env
# or: create_manifest_tool(api_key="...")

# use with an agent, e.g.:
from langchain.agents import create_agent
agent = create_agent(model=..., tools=[tool])

create_manifest_tool wraps AsyncManifestClient as a get_manifest(url) StructuredTool — the API key is bound at creation time, so the LLM never sees it.

All methods

# Both ManifestClient and AsyncManifestClient expose:
manifest = client.get("https://example.com")          # POST /manifest → Manifest
manifest = client.get("https://example.com", country="US")  # POST /manifest → Manifest, Accept-Language: en-US,en;q=0.9
manifest = client.get("https://example.com", storage_state="storage_state.json")  # authenticated fetch, see below
manifest = client.get_from_dom(url, dom_context)      # POST /manifest/from-dom → Manifest, see below
fp       = client.fingerprint("https://example.com")  # POST /fingerprint → Fingerprint
health   = client.health()                             # GET  /health → dict
valid    = client.session_valid()                      # GET  /session-status → bool

Manifest helpers

manifest.action("id")              # → Action | None
manifest.actions_of_type("input")  # → list[Action]
manifest.required_actions          # → list[Action]
manifest.blocked_actions([...])    # → list[Action], see "Action dependencies" below
manifest.fingerprint               # → str | None, same hash `fingerprint()` returns

Fingerprint

fingerprint(url) runs the same page-render + extraction pipeline as get(url) but skips the LLM translation step, returning just a stable hash of the page's interactive surface (Fingerprint.fingerprint). Useful for cheaply polling whether a page's action surface has changed since your last get() call, without paying for another manifest generation. Every Manifest also carries this same hash on manifest.fingerprint, so you can compare it against a later fingerprint() call directly.

fp = client.fingerprint("https://example.com")
print(fp.url, fp.fingerprint)

Geo/locale

manifest = client.get("https://example.com", country="US")  # ISO 3166-1 alpha-2
print(manifest.requested_country, manifest.served_locale, manifest.locale_mismatch)

country sets the scan's Accept-Language header to steer server-side geo/locale redirects (e.g. a store redirecting to a different country's pricing). This is a first attempt, not a guarantee — some sites geo-route on IP and ignore Accept-Language entirely, no proxy routing yet. Check manifest.locale_mismatch: True means a country was requested and the page actually captured (manifest.served_locale, best-effort from the URL) didn't match — an honest flag, not corrected pricing/currency data.

Authenticated pages

If your agent already holds a logged-in session for the target site, pass its Playwright storage_state so the scan perceives the authenticated view instead of the logged-out wall:

# a dict, or a path to a storage_state JSON file
manifest = client.get("https://app.example.com/dashboard", storage_state="storage_state.json")
manifest = client.get("https://app.example.com/dashboard", storage_state={"cookies": [...], "origins": [...]})

Capture storage_state from your own browser automation once it's logged in:

context.storage_state(path="storage_state.json")   # Playwright

What changes for an authenticated request:

  • manifest.cache_status is "bypass" — authenticated responses are never read from or written to the shared cache, so one caller's logged-in manifest can't be served to another. (fresh is implied; you don't need to pass it.)
  • manifest.session_domains lists the domains/origins your storage_state is scoped to (names only, never cookie values) — a quick check that you didn't hand over a broader session than the task needs.
  • A stale or invalid session raises APIError (HTTP 503) with a message that says the supplied session failed — it never silently falls back to an anonymous fetch and hands you a logged-out manifest you can't distinguish from a real one.

The caller supplies an already-valid session — Manifest does no login automation, credential capture, 2FA/challenge handling, or session refresh, and stores no credential profiles.

Privacy note. An authenticated page's content — which may include PII — is sent to Anthropic for the extraction step, the same path every get() call uses. Manifest does not persist page content.

Security note. A supplied storage_state is held in memory for that one request only. It is never written to disk, a database, a cache, or a queue; it is scrubbed from request logs, error responses, and traces; and it is dropped when the request's browser context closes.

Authenticated fetch is available in this Python SDK only. The JavaScript SDK has no storageState parameter yet.

State a fresh navigation can't see

get() (with or without storage_state) always re-navigates Manifest's own browser to url from scratch. That's fine for a URL-addressable page, but a client-side-only overlay — a wizard step, a search picker, anything that opened without changing the URL — doesn't exist on that fresh load. No amount of storage_state or retrying fixes this; there's nothing wrong with the request, there's just nothing for the next get() call to see.

get_from_dom() sidesteps it: instead of Manifest rendering the page, you capture the DOM state from your own already-open, already-interacted-with page and submit that directly.

from manifest_api import EXTRACTOR_JS

page.evaluate(EXTRACTOR_JS)  # defines window.__semanticAgentLayerExtractDomContext
dom_context = page.evaluate("window.__semanticAgentLayerExtractDomContext()")

manifest = client.get_from_dom(page.url, dom_context)

No storage_state needed here — your page already carries whatever session got it to this state. Omit cache_scope (the default) for anything whose DOM changes between calls, like a wizard step; passing one enables caching for that url+scope pair. The response has no requested_url / redirected / session_domains (nothing on this path navigates) but still carries fingerprint, computed directly from your dom_context.

Action dependencies

Some actions are disabled until others are completed (e.g. a submit button gated on required fields). Action.requires lists the ids of actions that must be completed first; blocked_actions() filters a manifest down to actions not yet unblocked by a given set of completed ids:

completed = ["email-input"]
still_blocked = manifest.blocked_actions(completed)  # actions still waiting on something

requires is inferred by the LLM translation step from DOM signals (disabled attributes, aria-disabled, form field proximity) — it's best-effort, not a guaranteed-accurate dependency graph, and won't capture custom JS validation logic.

Action types

button · input · textarea · select · checkbox · radio · other

Locators

Each action may carry a locator with css, role, and name — enough to find and act on the underlying element without guessing a selector yourself. It's best-effort: locator is None if no element on the page plausibly matched the action.

Prefer role/name over css where possible — they hold up better across redesigns, since css can be a brittle positional fallback when the element has no id or name attribute.

action = manifest.action("continue")
if action.locator and action.locator.css:
    page.click(action.locator.css)

Error handling

from manifest_api import AuthenticationError, RateLimitError, APIError

try:
    manifest = client.get("https://example.com")
except AuthenticationError:
    print("Check your API key")
except RateLimitError:
    print("Slow down — rate limit hit")
except APIError as e:
    print(f"Server error {e.status_code}")

Docs

https://omfang.io/manifest-docs

Metadata

Release files for manifest-api 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for manifest-api 0.5.0
File Size Uploaded
manifest_api-0.5.0.tar.gz 22.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for manifest-api 0.5.0
File Interpreter ABI Platform
manifest_api-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 42.6 kB

Release files / manifest_api-0.5.0.tar.gz

Download URL manifest_api-0.5.0.tar.gz
Size 22.0 kB
Tags Source
SHA-256 checksum
How to use checksums
2bc4a2323da5e1d8083f9b339613d662b5c1747edd5c98c686f1d089f3792efc
BLAKE2b-256 checksum
How to use checksums
e8a70b10a5d1977bf87bb84b6abceb96b33805093b751038d1ebe68c53573cc1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.1

Release files / manifest_api-0.5.0-py3-none-any.whl

Download URL manifest_api-0.5.0-py3-none-any.whl
Size 20.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3deb20bdc0d4ecbc8eac53cbbe686039f8e98384de0e5ab38fcb7967b154473c
BLAKE2b-256 checksum
How to use checksums
0871d2690603f1ef3c580c533ac9b073c04cc94be44dc293cc4aff0082eaf832
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.1

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.0

2 release files

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page