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_statusis"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. (freshis implied; you don't need to pass it.)manifest.session_domainslists the domains/origins yourstorage_stateis 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
storageStateparameter 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.
Shared/singleton widget rebinding
Some pages have a shared/singleton field whose real target depends on whichever prior selection was made most recently — e.g. a wizard with one editable name box that stays bound to whatever step chip you clicked last, regardless of which step you just added. An agent that clicks add-step then fill-name repeatedly can silently overwrite the same box each time, since nothing about the field itself changes to signal this.
Action.rebinds_on lists the sibling action ids sharing that field's real
target — the field is only correctly bound to whichever one you selected most
recently, not any one you selected in the past. stale_targets() checks that
against your most recent selection, not an ever-completed set like
blocked_actions():
manifest = client.get_from_dom(url, dom_context, previous_manifest=prev, last_action_id="step-chip-3")
stale = manifest.stale_targets("step-chip-3") # fields not correctly bound right now
previous_manifest/last_action_id on get_from_dom() feed the detector
that populates rebinds_on — pass the Manifest you got back last call (as
Manifest.to_dict()) and the id of whichever action you invoked since then.
Both-or-neither; omit both to skip detection.
Detection fires on either of two signals once a field's proximity to a selection group is established: an existing sibling being explicitly reselected, or a new sibling appearing in that same group since your last call while the field's own identity didn't move (the latter is GA4's actual funnel-step-name mechanism — clicking "Add step" isn't itself a reselection of anything, so only the second signal catches it).
Like requires, this is a best-effort DOM-signal inference, not a guaranteed
diagnosis — it's a proximity + prior-selection heuristic (see manifest.py's
_detect_rebinding), and it cannot tell "shared by bug" from "shared by
design." A field that's genuinely, correctly shared across selections (by the
site's own design) but happens to sit DOM-close to an unrelated selection
control can still be flagged here.
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
Metadata
Release files for manifest-api 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| manifest_api-0.7.0.tar.gz | 24.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| manifest_api-0.7.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.0 kB
Release files / manifest_api-0.7.0.tar.gz
| Download URL | manifest_api-0.7.0.tar.gz |
|---|---|
| Size | 24.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d833abc6554804871e75979aa29c98db65d7a7e5d779cd6f1e9a5908bfd6bac
|
|
BLAKE2b-256 checksum How to use checksums |
e5fb7750258b636c007a63c4b4878d167888441c882c415f6c075c2b74520de7
|
| 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.7.0-py3-none-any.whl
| Download URL | manifest_api-0.7.0-py3-none-any.whl |
|---|---|
| Size | 22.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef9bbb0a0783b89f093d78d9ba9c3be15707d0ef0596578859b86362b78e4c1c
|
|
BLAKE2b-256 checksum How to use checksums |
1f1ba792a50abd6684ecdb423ce52febd50e26bc91a26cf29b51908ad36e9638
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.1
|