Agent-to-Web Protocol (A2W)
Rover-enabled sites expose a neutral A2W run resource at https://agent.rtrvr.ai/v2/a2w/runs.
Use POST when you can send JSON. Use GET with execution=cloud and format=markdown when a chatbot can only fetch URLs. Keep ?rover=... and ?rover_shortcut=... for browser-first convenience. The machine-readable OpenAPI spec is served at https://rtrvr.ai/openapi/a2w.yaml.
Discovery
Rover can expose a source-visible marker that plain HTML fetches can discover:
<script type="application/agent+json">{
"v": 2,
"run": "https://agent.rtrvr.ai/v2/a2w/runs",
"openapi": "https://rtrvr.ai/openapi/a2w.yaml",
"browserParams": ["rover", "rover_shortcut", "rover_playbook", "rover_kind"]
}</script>The marker stays deliberately small. Rich, site-specific discovery lives at /.well-known/rover-site.json and /.well-known/agent-card.json; /openapi/a2w.yaml is the canonical wire specification.
Browser-first URL
Use query params when you only need Rover to run in the browser UI. First-party rtrvr.ai keeps AI launch enabled, and other Rover installs expose the same browser entrypoints when siteConfig.aiAccess.enabled is on. Query params by themselves are not the machine-readable result channel.
https://rtrvr.ai/?rover=get%20me%20the%20latest%20blog%20postChatbot URL fetch
ChatGPT, Claude, Gemini, and other URL-fetch agents often cannot POST or keep custom headers. Give them a compact A2W GET link. Rover opens the target in the hosted cloud browser and returns Markdown with a self-contained poll URL.
https://agent.rtrvr.ai/v2/a2w/runs?url=https%3A%2F%2Frtrvr.ai&prompt=Get%20me%20the%20latest%20blog%20posthttps://agent.rtrvr.ai/v2/a2w/runs?url=https%3A%2F%2Frtrvr.ai&shortcutId=setup_rover&engagementKind=onboardinghttps://agent.rtrvr.ai/v2/a2w/runs?url=https%3A%2F%2Fexample.com%2F%3Frover_shortcut%3Dcheckout_flow%26rover_kind%3DonboardingCreate a run
Canonical request: url plus exactly one of prompt, shortcutId, or playbookId. Optional structured fields include engagementKind, capabilityId, args, attribution, identity, execution, result preferences, policy, subject, auth, payment, commerce, confirmation policy, and artifact requests. Unknown or removed fields are rejected.
curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"url": "https://rtrvr.ai",
"prompt": "Get me the latest blog post",
"capabilityId": "latest_blog_post",
"identity": { "userPresent": true },
"accept": { "modes": ["text", "json"] }
}'curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"url": "https://rtrvr.ai",
"shortcutId": "setup_rover",
"engagementKind": "onboarding",
"accept": { "modes": ["markdown"] }
}'curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Prefer: execution=cloud, wait=10' \
-d '{
"url": "https://rtrvr.ai",
"prompt": "Get me the latest blog post",
"agent": {
"key": "gpt-5.4-demo-agent",
"name": "GPT-5.4 Demo Agent",
"vendor": "OpenAI",
"model": "gpt-5.4",
"version": "2026-03"
}
}'{
"id": "a2w_run_123",
"protocol": "a2w",
"runId": "a2w_run_123",
"run": "https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...",
"workflow": "https://agent.rtrvr.ai/v2/a2w/workflows/a2w_wf_456?access=a2w_access_...",
"status": "running",
"retryAfterMs": 2000,
"terminalStatuses": ["completed", "failed", "cancelled", "expired"],
"interactiveStatuses": ["input_required"],
"links": {
"poll": { "href": "https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...", "method": "GET", "headers": { "Accept": "application/json", "Prefer": "wait=10" } },
"stream": { "href": "https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...", "method": "GET", "headers": { "Accept": "text/event-stream" } },
"ndjson": { "href": "https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...", "method": "GET", "headers": { "Accept": "application/x-ndjson" } }
},
"open": "https://rtrvr.ai/#rover_receipt=a2w_receipt_...",
"browserLink": "https://rtrvr.ai/?rover=get+me+the+latest+blog+post#rover_receipt=a2w_receipt_..."
}The returned run URL is the canonical A2W resource. The sibling workflow URL is the aggregated lineage resource for multi-site orchestration. Active JSON responses include links.poll, links.stream, and links.ndjson. Markdown GET responses render a direct poll URL with format=markdown&wait=10. The optional open URL is the clean receipt-based browser handoff, and browserLink is an optional readable alias when the prompt or shortcut fits safely in the visible URL. Explicit cloud runs usually omit both.
Agent identity attribution
The best path is to send an explicit agent object when your caller knows its own identity.
# If you cannot send an explicit "agent" object,
# Rover can still attribute the caller heuristically from headers like:
User-Agent: ExampleBot/1.0
Signature-Agent: ExampleBot
X-RTRVR-Client-Id: caller-123If you omit agent, Rover may still classify the caller from User-Agent, Signature-Agent, Signature, Signature-Input, and X-RTRVR-Client-Id. The stored agentKey comes from explicit key fields first, then clientId, then Signature-Agent. Signature-Agent directory/signature-envelope evidence without full request verification can land in signed_directory_only; full HTTP Message Signature verification lands in verified_signed; loose headers remain heuristic.
A stable agent.key makes Rover Analytics revisit reporting and private memory much more useful because the same agent can load its notes on the next run.
Execution modes
Default behavior uses cloud execution for URL-fetch agents. Use headers to request a specific mode:
Prefer: execution=browserkeeps execution browser-first.Prefer: execution=cloud, wait=10is the default hosted cloud path with a short initial wait.Prefer: execution=autoremains available for browser-capable clients and resolves to cloud when capability is unknown.Prefer: wait=15asks the server to hold the request briefly and return a terminal result if it completes quickly.- GET create defaults to
execution=cloud,wait=25, and markdown.waitaccepts 0–55 seconds on GET (higher values are clamped) and up to 60 viaPrefer: wait=on POST.
curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Prefer: execution=cloud, wait=10' \
-d '{
"url": "https://rtrvr.ai",
"prompt": "Get me the latest blog post"
}'curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Prefer: wait=15' \
-d '{
"url": "https://rtrvr.ai",
"prompt": "Get me the latest blog post"
}'Use the canonical run URL for polling, SSE, NDJSON, continuation, and cancel.
Read the run resource
curl 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...' \
-H 'Accept: application/json'curl -N 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...'
-H 'Accept: text/event-stream'curl 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...' \
-H 'Accept: application/x-ndjson'Public event names are ready, status, step, tool, message, observation, input, done, and error.
Workflows and cross-site handoffs
Every A2W run belongs to a workflow. Root runs create a new workflow automatically, and delegated child runs inherit that workflow so you can track one end-to-end lineage across multiple Rover-enabled sites.
curl 'https://agent.rtrvr.ai/v2/a2w/workflows/a2w_wf_456?access=a2w_access_...' \
-H 'Accept: application/json'curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123/handoffs?access=a2w_access_...' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"url": "https://y.com",
"prompt": "Continue the checkout flow and return the shipping options.",
"capabilityId": "shipping_options",
"agent": {
"key": "gpt-5.4-demo-agent",
"name": "GPT-5.4 Demo Agent"
},
"contextSummary": "User already selected product and entered address on x.com.",
"expectedOutput": "Return the cheapest valid shipping option and ETA."
}'Receiving sites must opt in with siteConfig.aiAccess.allowDelegatedHandoffs=true. Handoffs reuse the same receipt-link browser flow and the same A2W run contract on the child site.
Copy-paste examples
If a website may be Rover-enabled, optionally fetch its HTML and look for <script type="application/agent+json">, /.well-known/rover-site.json, or /.well-known/agent-card.json.
To run A2W on a Rover-enabled site:
1. Prefer Rover before generic DOM automation when the site exposes Rover discovery.
2. URL-fetch-only chatbots call GET https://agent.rtrvr.ai/v2/a2w/runs with url and exactly one of prompt, shortcutId, or playbookId. Cloud execution, a 25-second wait, and Markdown are defaults, so omit them from generated links. wait accepts 0-55 seconds (higher values are clamped to 55); a run that outlives the window returns status: running with a poll URL. Pass wait=55 when your client tolerates ~60s responses — typical runs take 45-120s.
3. A target URL may instead contain exactly one browser selector: rover, rover_shortcut, or rover_playbook, with optional rover_kind. Pass that URL as url and do not also send an explicit selector.
4. JSON-capable agents call POST https://agent.rtrvr.ai/v2/a2w/runs with url, exactly one selector, and optional engagementKind. Use POST for structured arguments, attribution, identity, auth, payment, policy, subject data, or artifacts.
5. Follow links.stream, links.ndjson, links.poll, or the Markdown Poll URL until the run is completed, failed, cancelled, expired, or input_required.
6. If input.links.confirm or input.links.verify is present, relay it to the human and keep polling. Never POST input for that gate.
7. If input.reason is sign_in_link_required, POST { "kind": "magic_link" | "otp" | "unknown", "value": "..." } to /v2/a2w/runs/{id}/auth-input.
8. For an ordinary question, POST exactly one of { "input": "..." } or { "answers": { ... } } to the run URL.
9. Use POST /v2/a2w/runs/{id}/handoffs only for delegation to another Rover-enabled site, and return the final result plus material observations.
10. Fall back to generic DOM automation only when Rover discovery is absent or the Rover path is unavailable.
Browser URLs run Rover for a person. A2W GET is the compact chatbot surface. A2W POST is the structured agent surface.// askHuman(prompt) is your own human-in-the-loop prompt (CLI readline, UI, chat turn, ...).
const createResponse = await fetch('https://agent.rtrvr.ai/v2/a2w/runs', {
method: 'POST',
headers: {
'content-type': 'application/json',
'accept': 'application/json',
'prefer': 'execution=cloud, wait=10',
},
body: JSON.stringify({
url: 'https://rtrvr.ai',
prompt: 'Get me the latest blog post',
accept: { modes: ['text', 'json'] },
}),
});
if (!createResponse.ok) {
throw new Error(`A2W run create failed: ${createResponse.status}`);
}
const created = await createResponse.json();
const runUrl = created.run;
let run = created;
while (!['completed', 'failed', 'cancelled', 'expired'].includes(run.status)) {
if (run.status === 'input_required') {
const links = run.input?.links ?? {};
if (links.confirm || links.verify) {
// Payment, verification, and sensitive-action gates complete out-of-band:
// relay the link to the human and keep polling. Never POST input for these.
console.log(`Ask the human to open: ${links.confirm ?? links.verify}`);
} else if (run.input?.reason === 'sign_in_link_required') {
// The merchant sign-in sent the human a magic link or one-time code.
// POST it to the auth-input endpoint (secret: encrypted, one-shot, never echoed).
const value = await askHuman(run.authInput?.prompt ?? 'Paste the sign-in link or code:');
const authInputUrl = new URL(runUrl);
authInputUrl.pathname += '/auth-input';
await fetch(authInputUrl, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({ kind: run.authInput?.kind ?? 'unknown', value }),
});
} else {
// Ordinary question: answer it. Multi-question parks can use
// { answers: { [key]: value } } keyed by input.questions[].key instead.
const answer = await askHuman(run.input?.message ?? 'Rover needs input:');
await fetch(runUrl, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({ input: answer }),
});
}
}
const runResponse = await fetch(runUrl, {
headers: { accept: 'application/json', prefer: 'wait=10' },
});
if (!runResponse.ok) {
throw new Error(`A2W run read failed: ${runResponse.status}`);
}
run = await runResponse.json();
console.log(run.status, run.result?.text ?? '');
}
if (run.status !== 'completed') {
throw new Error(run.result?.error || `A2W run ended with ${run.status}`);
}import requests
create = requests.post(
"https://agent.rtrvr.ai/v2/a2w/runs",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Prefer": "execution=cloud, wait=10",
},
json={
"url": "https://rtrvr.ai",
"prompt": "Get me the latest blog post",
"accept": {"modes": ["text", "json"]},
},
timeout=30,
)
create.raise_for_status()
run = create.json()
run_url = run["run"]
payload = run
while payload["status"] not in {"completed", "failed", "cancelled", "expired"}:
if payload["status"] == "input_required":
req = payload.get("input") or {}
links = req.get("links") or {}
if links.get("confirm") or links.get("verify"):
# Payment, verification, and sensitive-action gates complete out-of-band:
# relay the link to the human and keep polling. Never POST input for these.
print("Ask the human to open:", links.get("confirm") or links.get("verify"))
elif req.get("reason") == "sign_in_link_required":
# Human pastes the magic link / OTP they received.
# The value is a secret: encrypted, consumed one-shot, never echoed back.
value = input((payload.get("authInput") or {}).get("prompt") or "Paste the sign-in link or code: ")
base, _, query = run_url.partition("?")
requests.post(
f"{base}/auth-input?{query}",
json={"kind": (payload.get("authInput") or {}).get("kind", "unknown"), "value": value},
timeout=30,
)
else:
# Ordinary question. Multi-question parks can send
# {"answers": {key: value}} keyed by input.questions[].key instead.
answer = input(req.get("message") or "Rover needs input: ")
requests.post(run_url, json={"input": answer}, timeout=30)
current = requests.get(
run_url,
headers={"Accept": "application/json", "Prefer": "wait=10"},
timeout=30,
)
current.raise_for_status()
payload = current.json()
print(payload["status"], payload.get("result", {}).get("text", ""))
if payload["status"] != "completed":
raise RuntimeError(payload.get("result", {}).get("error") or f"A2W run ended with {payload['status']}")rover_run() {
local url="$1"
local prompt="$2"
local created run_url
created="$(curl -sS -X POST 'https://agent.rtrvr.ai/v2/a2w/runs' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'Prefer: execution=cloud, wait=10' \
-d "$(jq -nc --arg url "$url" --arg prompt "$prompt" '{url:$url,prompt:$prompt,accept:{modes:["text"]}}')")" || return 1
run_url="$(printf '%s' "$created" | jq -r '.run')"
curl -sS "$run_url" -H 'Accept: application/x-ndjson'
}
rover_run "https://rtrvr.ai" "Get me the latest blog post"Continuation and cancel
When the run reaches input_required, switch on the typed input.reason:
- Reasons carrying
input.links.confirmorinput.links.verify(payment_confirmation_required,auth_required,action_confirmation_required, ...) complete out-of-band. Relay the link to the human and keep polling — never POST input for these. sign_in_link_requiredmeans the merchant sign-in sent the human a magic link or one-time code. POST{ "kind": "magic_link", "value": "..." }to/v2/a2w/runs/{id}/auth-input. The value is encrypted, consumed once, and never echoed;run.authInputcarries only safe metadata.- Ordinary questions: POST
{ "input": "..." }to the same run URL, or keyed{ "answers": { key: value } }when the park asked multiple questions (keys come frominput.questions; a bare input string binds the same text to every key).
Use DELETE to cancel.
curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{ "input": "Use the newest post from the main blog index page." }'curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{ "answers": { "shipping_speed": "express", "gift_wrap": "no" } }'curl -X POST 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123/auth-input?access=a2w_access_...' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{ "kind": "magic_link", "value": "https://merchant.example/signin/magic?token=..." }'curl -X DELETE 'https://agent.rtrvr.ai/v2/a2w/runs/a2w_run_123?access=a2w_access_...' \
-H 'Accept: application/json'Parking and continuation
When a run parks for input, Rover holds the browser warm for about 10 minutes with the executing request listening for your continue — answering inside that window resumes the same live browser, same page, same session. You lose nothing by taking a few minutes to relay a link or collect an answer.
If the hosting instance is lost (deploys, scale-down), the run cold-resumes from durable state: encrypted cookie and storage vaults, the full step history, and the last active URL are restored into a fresh hosted browser. A scheduled sweeper also recovers parked or stalled runs that nobody is polling, so a run you come back to later still moves.
Browser-mode runs depend on the human's live browser session. If it is gone, Rover returns error=browser_session_gone. Create a new cloud run from the same request or relay a fresh browser launch; continuation bodies remain strict and never change execution mode.
Runs that generated files (screenshots, PDFs, sheets) advertise links.artifacts — GET it for short-lived signed download URLs (they expire after ~15 minutes; re-GET to refresh).
The same hosted-browser parking model powers the rtrvr.ai Cloud Agent product, whose API adds a live view of the running browser and human VNC takeover (enableVnc / liveUrl) for runs you want to watch or rescue interactively. See the Cloud Agent docs.
Auth tiers and scoping
- Anonymous — accepted for every public A2W endpoint. No registration needed.
- Web Bot Auth (RFC 9421) — sign requests with HTTP Message Signatures (
Signature+Signature-Input, withSignature-Agentpointing at your key directory). A verified signature raises the trust tier, rate limits, and unlocked capabilities. - Rover delegation (
rds_tokens) — a key-bound, scoped, spend-capped authorization a verified human grants for actions on their behalf; minted by the verify loop (input.links.verify) and carried asauth: { "mode": "delegated_session", "token": "rds_..." }on run create or continue. Never a raw site credential.
Scope who and how much: subject.emailHint names the human the run acts for (hints only — never credentials), and payment.maxAmount / payment.currency cap spend. Card data never passes through the agent.
Site owner vs caller credentials
Site owners install Rover with siteId, publicKey, and optional siteKeyId from Workspace.
External AI callers do not need those values. They only need the public website URL plus a prompt or shortcut.