Human in the loop
Let the agent ask you questions, wait for your approval before it submits, hand you the browser for a CAPTCHA, and let you take over at any time.
Some steps need a person: a one-time code sent to your phone, a decision before a purchase, a CAPTCHA. The agent pauses the run, tells you what it needs, and continues when you reply. Your code (or a person using it) answers through one endpoint, POST /v1/browser/runs/{id}/reply.
| The run needs | needs.kind | You reply with |
|---|---|---|
| Information it doesn't have | question | {"answer": "…"} or {"secret": "…"} |
| Your OK before it submits | approval | {"approve": true} or {"approve": false, "reason": "…"} |
| A person in the browser | takeover | Finish in Live View, then {"done": true} or {"failed": true} |
How a pause works#
When the agent needs you, the run's status becomes waiting_for_human and needs describes what it needs (abridged):
{
"id": "brun_5b7d9f1a3c5e7a9b1d3f5a7c",
"object": "browser_run",
"status": "waiting_for_human",
"steps": 6,
"current_url": "https://bank.example.com/verify",
"needs": {
"kind": "question",
"prompt": "What is the 6-digit code sent to your phone ending 42?",
"expects_secret": true,
"screenshot": "/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/screenshot",
"since": "2026-10-11T09:31:12.000Z",
"reply": "/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/reply"
}
}| Field | Meaning |
|---|---|
needs.kind | question, approval or takeover |
needs.prompt | What the agent needs, in one sentence, ready to show a person |
needs.screenshot | Path of a JPEG of the page when the pause began. Fetch it with your API key. |
needs.since | When the pause began |
needs.reply | Path to reply to |
While a run waits:
- The browser stays open on the same page, so the agent continues exactly where it stopped.
- A
?wait=status request returns at once instead of holding. Reply, or poll with your own delay. - If you set a webhook, it receives a
run.waitingevent. - Each pause includes one minute. Beyond that, waiting costs $0.005 per minute (pricing).
- If nobody replies within
human_timeout_minutes(default 30), the run ends astimed_outwith the errorno human response within 30 minutes.
Handle pauses in your code#
This loop runs a task that may ask for approval or a code, and asks the person at the terminal:
import os
import requests
API = "https://lovelyrouter.com/v1/browser"
H = {"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"}
run = requests.post(f"{API}/runs", headers=H, json={
"goal": "In the rewards shop, redeem 500 points for the Blue Mug and tell me the order number.",
"start_url": "https://shop.example.com/rewards",
"profile": "shop-login",
"approve": "risky",
}).json()
while run["status"] in ("running", "waiting_for_human"):
if run["status"] == "waiting_for_human":
needs = run["needs"]
if needs["kind"] == "approval":
print(needs["prompt"], *needs["fields"], sep="\n ")
reply = {"approve": input("Approve? [y/N] ").strip().lower() == "y"}
elif needs["kind"] == "question":
value = input(needs["prompt"] + " ")
reply = {"secret": value} if needs["expects_secret"] else {"answer": value}
else: # takeover
print(needs["prompt"], "\nOpen:", needs["live_view_url"])
input("Press Enter when you are done. ")
reply = {"done": True}
# A 409 here means the run already moved on; the next status request shows where it is.
requests.post(f"{API}/runs/{run['id']}/reply", headers=H, json=reply)
run = requests.get(f"{API}/runs/{run['id']}", headers=H, params={"wait": 30}).json()
print(run["status"], run["answer"])For a person who isn't at a terminal, send them needs.prompt (and the screenshot or Live View link) through your own app, chat or email, and post their reply when it arrives. A webhook tells you when to.
Answer a question#
The agent asks when the page needs something the goal doesn't contain: a verification code sent to your phone or email, a missing detail, or a choice only you can make. Reply in one of two ways:
{"answer": "…"}— text the agent reads and uses, up to 2,000 characters. Use it for details and choices:{"answer": "Use the billing address, not the shipping one."}{"secret": "…"}— a value the agent types into the page without ever seeing it, up to 4,096 characters. Use it for one-time codes:{"secret": "481923"}
needs.expects_secret is true when the question asks for a code, password, PIN or one-time code. A secret reply can be typed only on the site the run was on when it asked, into any text-like field there, password and PIN fields included. Like other secrets, a value of six characters or more is replaced with [secret:<name>] wherever it would appear in the run's output, and the value is deleted when the run ends. For passwords you use on every run, store them in the vault instead, so the agent doesn't have to ask.
curl https://lovelyrouter.com/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/reply \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{"secret": "481923"}'For codes from an authenticator app, store the TOTP seed in the vault instead. The agent then types the current code itself and doesn't need to ask. See Two-factor codes.
Approve before submitting#
approve decides when the agent stops for your OK before an action that submits something: a button that submits a form, a button labelled as a purchase, booking, payment, message or deletion ("Buy now", "Book", "Pay", "Send", "Delete" and similar), or pressing Enter in a field of a form that isn't a search box, which submits that form. Links are never paused on: following one only opens a page.
approve | The agent pauses before |
|---|---|
risky (default) | Those actions when it judges them irreversible or consequential: a payment or purchase, an order or booking, sending a message or email, deleting data, submitting an application. Logins, searches, navigation and intermediate form steps go ahead. |
submit | Every one of those actions. |
none | Nothing. The agent submits on its own. |
Any other value is rejected with 400. An approval pause looks like this:
{
"status": "waiting_for_human",
"needs": {
"kind": "approval",
"prompt": "Approve: click button \"Redeem 500 points\" on Blue Mug – Rewards?",
"fields": ["Quantity: \"1\"", "Ship to: \"Ada Lovelace, 12 Main St\"", "Gift wrap: unchecked"],
"action": "Redeem 500 points",
"screenshot": "/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/screenshot",
"since": "2026-10-11T09:33:40.000Z",
"reply": "/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/reply"
}
}fields lists the form's current values and action is the label of the button it wants to click. Reply:
{"approve": true}— the agent clicks that button as its next step.{"approve": false, "reason": "Wrong address"}— the agent doesn't submit. It reads your reason (up to 500 characters) and tries another way, or finishes and reports why.{"deny": true}does the same.
curl https://lovelyrouter.com/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/reply \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{"approve": true}'Hand over for CAPTCHAs#
The agent can't solve CAPTCHAs or "verify you are human" checks. When a page shows one, or a step only a person can do, it hands the browser to you with needs.kind set to takeover:
{
"status": "waiting_for_human",
"needs": {
"kind": "takeover",
"prompt": "Please solve the CAPTCHA and press Continue.",
"live_view_url": "https://…",
"live_view_expires_at": "2026-10-11T10:35:02.000Z",
"screenshot": "/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/screenshot",
"since": "2026-10-11T09:35:02.000Z",
"reply": "/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/reply"
}
}- Open
live_view_urlin a browser. It shows the agent's browser tab live, and the person can click and type in it. The link is valid for one hour. - The person does what
promptsays. - Reply
{"done": true}, optionally with anote(up to 500 characters) the agent reads, such as"Solved the CAPTCHA, now on the search results". If the person couldn't do it, reply{"failed": true, "note": "…"}.
If the run notices on its own that the handover has ended, it continues without your reply, and a later reply returns 409, which you can ignore. The agent also hands over when it is stuck: after three failed steps in a row, or after repeating the same action three times without progress.
A CAPTCHA or "verify you are human" page is handed over at most twice per run. If it is still there after that, or the person replied {"failed": true}, the run ends as failed with the error blocked by the website. So does a page that blocks the browser outright (access denied, 403), since a person can't get past it either, and any human check when the run was started with "human": false.
Take over a run yourself#
You can take the browser at any time, for example to show the agent the way or to finish a step by hand:
curl https://lovelyrouter.com/v1/browser/runs/brun_5b7d9f1a3c5e7a9b1d3f5a7c/takeover \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{"instructions": "I will pick the delivery slot myself."}'The request returns 202 at once. The run pauses after the step in progress, and the next status shows needs.kind takeover with a live_view_url. Do what you need in Live View, then hand back with {"done": true, "note": "Picked Saturday 10–12"}. The agent continues from the page you left it on.
instructions (optional, up to 500 characters) becomes the pause's prompt. If the run is already waiting for a question or an approval, the takeover starts after you reply to it. A finished run returns 409 with This run has finished.
Set the timeout#
human_timeout_minutes sets how long each pause waits for a reply: 1 to 1,440 minutes, default 30 (5 for a run started by a cloud agent, which usually has nobody watching). When a cloud agent's session ends, its unfinished browser runs are stopped. The timer restarts at every pause. A run that times out ends as timed_out and is charged for its steps and the billable waiting time.
A longer timeout gives people more time to reply, but reserves more of your balance: the hold includes one wait to the timeout at $0.005 per minute. See How the hold works.
Run without a person#
For unattended jobs, turn pauses off:
{
"goal": "Get the current mortgage rates table.",
"start_url": "https://bank.example.com/rates",
"human": false,
"approve": "none"
}"human": false— the agent never asks questions or hands over. If it lacks information or is stuck, it carries on as best it can and the run may end asfailed."approve": "none"— it submits forms without waiting for approval.
With both set, the run reserves nothing for waiting time.
Reply reference#
POST /v1/browser/runs/{id}/reply takes exactly one of these bodies:
| Body | For needs.kind | Effect |
|---|---|---|
{"answer": "…"} | question | The agent reads your text (up to 2,000 characters). |
{"secret": "…"} | question | The agent types the value on the current site without seeing it (up to 4,096 characters). |
{"approve": true} | approval | The agent clicks the button as its next step. |
{"approve": false, "reason": "…"} or {"deny": true, "reason": "…"} | approval | The agent doesn't submit and reads your reason. |
{"done": true, "note": "…"} | takeover | The agent continues from the current page. |
{"failed": true, "note": "…"} | takeover | The agent continues, knowing the person couldn't finish. |
It returns the run with status back to running. Errors:
| Status | Message | Cause |
|---|---|---|
400 | Reply with one of: … | The body matched none of the shapes above. |
404 | No such run | Wrong id, or the run belongs to another account. |
409 | This run is not waiting for a reply | The run is running or has finished. |
409 | This run is waiting for a reply of kind "question" | The reply doesn't match needs.kind (the message names the kind expected). |