botproxy

Docs

Request a chain, send it steps, check how it is going.

The idea

The servers are real browsers, each one a persona: one person in one place, on its own machine. A server does one chain at a time, and a chain can take up to an hour. botproxy sits in front of them: you ask for a chain, it waits in a queue until a server is free, then the server is yours until the chain ends.

Chain
Your turn on one server. It has one plugin and a list of steps, all run on the same server, in order, in a browser tab of its own.
Step
One operation of the chain's plugin, with its payload. Each step has a result.
Plugin and operation
What a server can do, e.g. plugin bper, operation init-login. List them with their parameters. A plugin that needs an account (Instagram, a bank) only runs on personas logged into it; each such plugin needs one account, so it runs either whole or not at all on a persona.
Class
Set on your account by the admin. It gives your chains their priority, how long the server is held for you, and whether you may use raw.

Get access

Your admin creates your client and gives you an API key (starts with brk_, shown only once). Send it on every request:

export BR=https://botproxy.example.com
export KEY=brk_...

curl -H "Authorization: Bearer $KEY" $BR/api/chains

No key yet? Ask for one with the email and password you were given:

curl -X POST $BR/api/api-key \
  -H 'Content-Type: application/json' \
  -d '{"identity": "pc1@example.com", "password": "..."}'
# {"api_key": "brk_..."}

Each call makes a new key and the old one stops working at once (keys are stored hashed, so they cannot be shown again). Ask once and keep it.

Keep the key secret. If it leaks, get a new one with the call above (or ask the admin): the old one stops working at once.

Find plugins

The plugins you can request a chain of right now (at least one server has them on), each with its operations and their parameters:

curl -H "Authorization: Bearer $KEY" $BR/api/plugins
[{"name": "instagram", "operations": [
   {"name": "open-account", "params": [{"name": "handle", "type": "text", "required": true}]},
   ...]}]

Only names and parameters, as the servers report them. raw is listed only if you may use it.

Request a chain

curl -X POST $BR/api/chains \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"plugin": "bper"}'
201 {"id": "k3x9...", "state": "queued"}

The chain is queued. Keep its id. Optional fields:

FieldMeaning
countryTwo lowercase letters, e.g. de: run on a persona in that country. See Choose a country.
labelsAn object, e.g. {"instagram": "alice"}: run only on a persona with all of these labels. See Choose by labels.
wait_for_metrue: keep the server after the last step, so you can decide what to do next. See Keep the server.
webhook_urlCalled when the chain changes state. See Webhooks.
preferred_serverServer name you would like. Only a hint, and only for privileged clients. An unknown name is refused with 400.
stepsThe chain's steps, already inside the request: a list of {operation, payload, idempotency_key}, run in that order. idempotency_key is optional here (it defaults to the step's number). See below.

The whole chain in one request

You do not have to talk to botproxy at every step. If you know the steps up front, put them in the request: the chain is created with them, waits in the queue, and runs them one after the other on its server, on its own.

curl -X POST $BR/api/chains \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"plugin": "instagram", "steps": [
        {"operation": "open-account", "payload": {"handle": "alice"}},
        {"operation": "info-profile", "payload": {}},
        {"operation": "get-posts-profile", "payload": {}}]}'
201 {"id": "k3x9...", "state": "queued", "closes_in_s": null,
     "steps": [{"seq": 1, "state": "pending"}, {"seq": 2, "state": "pending"}, {"seq": 3, "state": "pending"}]}

It is all or nothing: if one step is invalid (an operation not in the plugin, a missing parameter), nothing is created and the answer says which step. You can still add more steps later, as below. Then come back for the results whenever you like.

Choose a country

Some personas are always on. Others sleep, and botproxy starts them when a chain needs them, often abroad behind a local connection.

A country where no server can run your plugin is refused with 400 right away. If the persona there cannot start for now, the chain simply stays queued and is tried again later.

Choose by labels

Each persona has labels, key: value pairs that say what it is. Ask for some with labels, and the chain runs only on a persona that has every one of them, with those values:

curl -X POST $BR/api/chains \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"plugin": "instagram", "labels": {"instagram": "alice"}}'

Two kinds:

Use them when the persona matters, not only the plugin: the account that follows a private page, a mobile connection for a site that blocks the others. Like country, labels may start a sleeping persona, and a chain waits in the queue for a persona that has them rather than take another. Labels no enabled server has, together with your plugin, are refused with 400. See How the queue works for how they affect the wait.

Labels are checked once, when you request the chain. If the admin later changes the persona's labels, or disables the only server that has them, the chain stays queued with nothing to take it: a queued chain never expires on its own. If yours waits longer than you expect, check the labels (ask the admin) and close it.

Where your chain ran. Once a chain has a server, the chain you read back carries server_labels: every label of that persona, not only the ones you asked for, as the admin last imported them. So you can tell, for example, which Instagram account read a page without having asked for one. That and the server's name are all you see of it: never its address or its key.

Send steps

Besides the steps sent with the chain, you can add steps one by one, right away or later, even while the chain is still queued. They wait and run in order once a server is assigned.

curl -X POST $BR/api/chains/k3x9.../steps \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"operation": "init-login", "payload": {}, "idempotency_key": "init-login"}'
202 {"seq": 1, "state": "pending", "closes_in_s": 120}

The operation must belong to the chain's plugin, and the payload must carry its required parameters, or the step is refused with 400. Payloads up to 10 MB; results up to 50 MB.

The idempotency_key is yours to choose, unique within the chain. Sending the same key again does not add a second step, so retrying after a network error is safe.

A chain without wait_for_me ends as soon as it has no step left to run. Send all its steps before the last one finishes.

How a chain runs

Check status

All your chains, newest first (optionally ?state=running):

curl -H "Authorization: Bearer $KEY" $BR/api/chains

One chain, with its steps:

curl -H "Authorization: Bearer $KEY" $BR/api/chains/k3x9...
{"id": "k3x9...", "plugin": "bper", "state": "running", "server": "s1", "closes_in_s": 120,
 "server_labels": {"country": "it", "city": "milano"},
 "steps": [{"seq": 1, "operation": "init-login", "state": "running"}]}

server_labels is there once the chain has a server: see Choose by labels.

One step, with its result:

curl -H "Authorization: Bearer $KEY" $BR/api/chains/k3x9.../steps/1

Reading a result deletes it. Once the step has ended, the answer to this call carries the result and botproxy then drops it: the step is still there, with "result_pruned": true and no result. To read it without deleting it, for example while you poll or if you may need to read it again, add ?do-not-consume=true, and delete it yourself when you are done:

curl -H "Authorization: Bearer $KEY" "$BR/api/chains/k3x9.../steps/1?do-not-consume=true"
curl -X DELETE -H "Authorization: Bearer $KEY" $BR/api/chains/k3x9.../steps/1/result

If the answer is lost on the way (a network error), a plain read has already dropped the result: if you cannot afford that, read with do-not-consume first. Polling GET /chains/{id} never deletes anything.

Results nobody reads are kept for 1 day after the step ends (your admin may set a different time), then dropped the same way.

Download files

Some operations make a file (e.g. instagram/get-post-media, a post's photos or video). Its result does not carry the bytes but a link, which botproxy rewrites to its own address:

{"success": true, "format": "zip", "media_url": "/api/chains/k3x9.../steps/4/media/0", ...}

Fetch it with your key, like any other call. botproxy streams the file from the server that ran the step, it keeps no copy:

curl -H "Authorization: Bearer $KEY" -o post.zip $BR/api/chains/k3x9.../steps/4/media/0

Chain states

queuedWaiting for a server.
leasedGot a server, no step started yet.
runningA step is running.
idleHolding the server for you, waiting for a new step (wait_for_me), or for the first one.
closedDone: all steps ran (some may have failed: check them), or you closed it.
expiredYou did not send a step in time; the server was released.
failedThe server was lost after some steps had run (closed_reason: server_lost).

Step states

pending → running → done or failed. If a step fails, the steps queued after it are not run: they are marked failed too. Then the chain ends as usual: closed, or idle with wait_for_me, so you can decide what to do.

Keep the server

Sometimes the next step depends on a result. Create the chain with "wait_for_me": true: when its last step ends, the chain goes idle and the server stays yours for a while (the admin sets how long, client by client). Then either:

Every answer about a chain (creating it, adding a step, reading it, its steps or the list) carries closes_in_s: the seconds left before it expires if you send nothing more.

Only a new step restarts the clock; reading the chain or a step does not. If you need time (a 2FA on your phone, say), watch closes_in_s and send the next step before it reaches 0. If the wait is too short for you, ask the admin to raise it.

Close as soon as you know you are done: someone else may be waiting for that server.

Close a chain

curl -X DELETE -H "Authorization: Bearer $KEY" $BR/api/chains/k3x9...

Frees the server. A queued chain leaves the queue. A running step cannot be stopped: the server finishes it first. Steps that had not started yet are marked failed.

How the queue works

Webhooks

With webhook_url, botproxy calls you: it sends a POST to that URL every time the chain changes state. You need a server listening there, reachable from the internet (no localhost or private addresses).

Webhooks are a shortcut, not a guarantee: if you miss one, poll the chain to catch up.

The raw plugin

For trusted clients only (anyone else gets 403): instead of a site's plugin, drive the browser yourself. Request a chain with "plugin": "raw" and send steps whose operation is one of the browser's own calls. They act on the chain's tab: you never name a tab.

OperationsDo
ops/navigate, ops/extract-content, ops/screenshot, ops/network, ops/search-objectGo to a URL, read the page's HTML, take a screenshot, read the network traffic, find something on the page.
primitive/click-element, primitive/type-text, primitive/scroll-page, ...Click, type, scroll, hover, press keys, like a person would.
system/ping, system/status, system/status/pages, system/metricsRead how the server is doing. Nothing that stops or restarts it.

The exact list and parameters are what the server reports; your admin can show them. Raw steps run while you wait: a navigate gives up after a minute.

ID=$(curl -s -X POST $BR/api/chains -H "$H" -H "$J" -d '{"plugin": "raw", "country": "de"}' | jq -r .id)
curl -s -X POST $BR/api/chains/$ID/steps -H "$H" -H "$J" \
  -d '{"operation": "ops/navigate", "payload": {"url": "https://example.com"}, "idempotency_key": "go"}'
curl -s -X POST $BR/api/chains/$ID/steps -H "$H" -H "$J" \
  -d '{"operation": "ops/extract-content", "payload": {}, "idempotency_key": "read"}'

The persona may be logged into accounts: raw browses with them. Trusted means trusted with those too.

Full example

Log in to the bank, then check where it got before deciding the next step, holding the server in between.

H="Authorization: Bearer $KEY"
J='Content-Type: application/json'

# 1. Request a chain that keeps its server
ID=$(curl -s -X POST $BR/api/chains -H "$H" -H "$J" \
  -d '{"plugin": "bper", "wait_for_me": true}' | jq -r .id)

# 2. Open the login form and submit it (queued until a server is free)
for op in init-login hydrate-login; do
  curl -s -X POST $BR/api/chains/$ID/steps -H "$H" -H "$J" \
    -d "{\"operation\": \"$op\", \"payload\": {}, \"idempotency_key\": \"$op\"}"
done

# 3. Wait for them to end (do-not-consume: polling must not drop the result)
step() { curl -s -H "$H" "$BR/api/chains/$ID/steps/$1?do-not-consume=true" | jq -r .state; }
while [ "$(step 2)" = pending ] || [ "$(step 2)" = running ]; do sleep 10; done

# 4. Approve on the phone; closes_in_s says how long the server stays yours
curl -s -H "$H" $BR/api/chains/$ID | jq .closes_in_s

# 5. Then check where the login got
curl -s -X POST $BR/api/chains/$ID/steps -H "$H" -H "$J" \
  -d '{"operation": "read-state", "payload": {}, "idempotency_key": "state-1"}'

# 6. ...and when you are done, free the server
curl -s -X DELETE -H "$H" $BR/api/chains/$ID

Send and forget

When you know every step up front, you do not need to stay around: no wait_for_me, no webhook_url, no polling while it runs. Send the whole chain in one request, then come back later for the results.

# One request: the chain and all its steps
ID=$(curl -s -X POST $BR/api/chains -H "$H" -H "$J" -d '{"plugin": "instagram", "steps": [
  {"operation": "open-account", "payload": {"handle": "alice"}},
  {"operation": "open-account", "payload": {"handle": "bob"}},
  {"operation": "open-account", "payload": {"handle": "carol"}}]}' | jq -r .id)

# ...later (within a day): the chain is closed, the results are there
curl -s -H "$H" $BR/api/chains/$ID
curl -s -H "$H" $BR/api/chains/$ID/steps/1   # reads and drops the result

Unread results stay for 1 day after each step ends. A webhook is optional: set webhook_url only if you want to know the moment it ends.

Reference

All paths start with /api. Every request needs Authorization: Bearer <key>.

RequestDoes
POST /api-keyNo auth. Body: identity (email), password. Returns a new api_key, replacing the old one.
GET /pluginsPlugins you can request now, with their operations and parameters.
POST /chainsRequest a chain. Body: plugin, optional country, labels, wait_for_me, webhook_url, preferred_server, steps ([{operation, payload, idempotency_key?}], created with the chain, all or nothing).
GET /chainsYour chains. Optional ?state=.
GET /chains/{id}One chain and its steps.
POST /chains/{id}/stepsAdd a step. Body: operation, payload, idempotency_key.
GET /chains/{id}/steps/{seq}One step and its result. Drops the result of an ended step once sent, unless ?do-not-consume=true.
DELETE /chains/{id}/steps/{seq}/resultDrop an ended step's result now. 204, also when already dropped; 409 while the step runs.
GET /chains/{id}/steps/{seq}/media/{n}Download the step's n-th file (the link in its result), streamed. Once, within 10 minutes of the step's end.
DELETE /chains/{id}Close the chain, free the server.

Errors

Errors come as {"status": 400, "message": "...", "data": {...}}, with field details in data.

400Invalid body, e.g. an operation not in the chain's plugin, a missing required parameter, or a country or labels no server has together with the plugin.
401Missing, wrong or revoked key, or your client is disabled.
403The raw plugin, or another plugin your admin keeps for trusted clients, and you are not trusted.
404No such chain or step (or not yours); or a file already downloaded, too late, or not in the step's result.
409The chain is already closed, expired or failed; or a result is dropped while its step still runs.
502The server that ran the step could not send the file (switched off, unreachable).