ARMANOS

Automation

Local API, keys and webhooks

The interface for your own programs lives on your computer: a port on 127.0.0.1, its own token and twelve routes. On our server there are separate keys with rights, and subscriptions that call your server when something happens.
50325
Default port on 127.0.0.1
12
Routes in the local API
7
Scopes a server key can carry
13
Events a webhook can carry

The API runs on your computer, not on our server

The local API is a small HTTP server inside the desktop app. It listens on 127.0.0.1 and answers programs running on the same machine.

It lives there because that is where the work is. Profiles, cookies, the engine and the proxy bridge are all on your disk. Opening a browser window is a local act, and no server of ours can perform it for you.

That also decides what happens when the network is down. Your scripts keep calling the app over loopback, and the app itself keeps working offline for 24 hours on a checked licence.

The answer envelope follows the shape this category settled on: code, data and msg. A script written for a neighbouring tool usually needs a new address and a new token, not a rewrite.

  • Drive profiles from your own code

    List, create, start, stop and delete them without touching the window.

  • Attach Puppeteer, Playwright or Selenium

    The start route answers with the debugging endpoint for that profile.

  • Run saved flows from outside

    Your own scheduler or build server can start a flow over the profiles you name.

  • Let an AI client act

    The bundled MCP server speaks to these same routes and needs the API switched on.

One switch, one port, one token

The API is off until you turn it on. Enabling it opens a port, so it is never on by default.

On first run the app writes local-api.json into its data folder. It generates a random token there, 24 bytes shown as 48 characters, and writes the file so only your account can read it.

The port is 50325 by default. If something else already holds it, the server takes a free port instead and the panel says which one it took. That matters, because your scripts and your AI client still point at the old address.

The token travels three ways: as the token parameter in the URL, as the x-api-token header, or as Authorization: Bearer. Regenerating it stops every script that still carries the old one, so the app asks you to confirm first.

  1. 1

    Open the API screen in the app

    It shows the switch, the address, the token and the list of routes.

  2. 2

    Turn the local API on

    The status has to read Running. If the usual port was busy, the panel names the port it took instead.

  3. 3

    Copy the address and the token

    The panel offers a ready curl line with your own token already in it.

  4. 4

    Point your script at it

    Every call except /status carries the token. A wrong token comes back as 401.

The API screen in ARMANOS: the enable switch, the local address, the access token and the list of routes.
The app UI is in English. The same screen also carries the ready config block for an AI client.

A request has to look like a program, not a web page

Binding to loopback sounds like a boundary. It is not one. The pages you visit run on this same computer, so they can reach 127.0.0.1 as well.

So the server first asks whether the caller looks like a page. Any request carrying an Origin header is refused. So is any request whose Sec-Fetch-Site is anything other than none, because only a typed address gives none. curl, Python and the MCP client send neither header.

The Host header is checked too, against the address actually being listened on. A domain name that resolves to 127.0.0.1 on the second lookup arrives here as evil.example, and that request is refused. Without the check the browser treats the answer as same-origin and hands the page the body.

Sec-Fetch-Mode is deliberately not used as a tell. Node's own fetch sends cors with no origin, and reading that as a browser would reject most automation scripts ever written.

Even /status sits behind the page check

GET /status needs no token, because a script has to be able to ask whether the app is up. It names the product and its exact version, and for an antidetect browser that single fact is worth hiding: a site that learns this machine runs ARMANOS could tie every profile it ever opens to one installation. The page check runs before /status, so a page gets 403 and learns nothing, not even that something answered.

Twelve routes and one envelope

Everything sits under 127.0.0.1:50325, and every answer has the same three fields. code is 0 on success and msg says success, while data carries the payload.

A refusal returns code -1 with the reason. A missing or wrong token is HTTP 401 with code -401, a page-shaped request is 403 with -403, an unknown path is 404 with -404. The envelope and the HTTP status always agree, so your script can branch on either.

Request bodies are read as JSON and capped at about a megabyte. Anything bigger is cut off and the call is handled as if no body had arrived.

Call
curl -H x-api-token:YOUR_TOKEN http://127.0.0.1:50325/api/v1/browser/list
Envelope
{ code: 0, msg: success, data: { list: [ ... ] } }
One profile
profileId, name, scenario, group, fingerprintTemplate, tags, lastUsedAt, running
Its proxy field
protocol, host, port. Never the login, never the password
Wrong token
HTTP 401, { code: -401, msg: unauthorized }
Called from a page
HTTP 403, { code: -403, msg: forbidden }

The token can also travel as ?token= in the URL or as Authorization: Bearer.

RouteMethodWhat it does
/statusGETSays the app is up and names its version. The only route with no token.
/api/v1/browser/listGETYour profiles, each with the state it is in right now.
/api/v1/profile/listGETThe same set, under the name other tools in this category use.
/api/v1/browser/startPOSTOpens a profile and answers with its debugging endpoint.
/api/v1/browser/stopPOSTCloses every window of a profile and answers how many it closed.
/api/v1/browser/activeGETThe ids of the profiles open at this moment.
/api/v1/profile/createPOSTCreates a profile through the same quota gate as the window.
/api/v1/profile/deletePOSTMoves a profile to the recycle bin.
/api/v1/profile/codeGETThe current one-time code for a profile that has a 2FA key stored.
/api/v1/proxy/listGETThe saved proxy library: hosts and ports, never the passwords.
/api/v1/flow/listGETSaved automation flows with the number of steps in each.
/api/v1/flow/runPOSTRuns a flow over the profile ids you name.

Starting a profile, and what you attach to it

POST /api/v1/browser/start takes profileId in the body or in the query string. The examples for Puppeteer and Playwright pass it in the query, so both are accepted.

You can ask for one run without a window. Send headless in the body, or headless=1 in the query. The strings 0, false and empty read as no, not as a non-empty string meaning yes. Send nothing at all and the profile's own setting decides, because a night run and a day of manual work are two different visits to the same profile.

What comes back depends on the engine. On the ARMANOS Browser each profile gets its own DevTools endpoint, isolated from every other profile. The app reads the port from that profile's own file, polling for up to about four and a half seconds, and answers with debugPort and a ws address for that profile alone.

The built-in Electron engine has no per-profile socket. Its answer names the app-level port and says plainly that it is shared by every profile and unauthenticated on loopback. That port is opened only when the built-in engine is selected: it is protected by nothing, and through it any program on the machine drives the manager window, not just profiles.

  • ARMANOS Browser engine

    debugPort and ws for this profile only. Attach your automation library straight to that address.

  • Built-in engine

    The shared app-level port, named in the answer together with the warning that it is shared.

  • Neither available

    The answer says no endpoint is open and why, instead of handing back an empty field.

The API passes the same gate as the buttons

The API is not a side door. Creating a profile goes through the same licence and quota check as the button in the window, so plan limits apply: 2 profiles on Free, 10 to 100 on Professional, 200 to 1000 on Business, 5000 and up on Enterprise.

Starting a profile checks the licence first. If the copy is locked, the call comes back with the reason in words instead of a blank failure.

flow/run is strict about ids. Profile ids it does not know are named back to you in data.unknown, so you can fix your call. Zero profiles taken into work is a refusal, not a success: the route once answered started: 0 while a run was going fine, and callers launched the same profiles a second time.

Two routes exist because scripts stall without them. profile/code returns the current one-time code for a profile with a 2FA key stored, with the seconds left, the issuer and the account. proxy/list returns your saved proxies with host and port, and never the credentials, so a leaked token does not hand over every proxy you bought.

Deleting through the API is a soft delete

profile/delete moves the profile to the recycle bin and frees the quota slot. Nothing is erased from your disk by that call, and the profile can be restored from the window.

Server keys carry rights, and they are a paid tool

The token above drives one installation on one computer. A server key is a different thing: it talks to our server from anywhere, and it never opens a browser window. The server route called start marks a profile busy and writes the open log, and that is all it does.

A key looks like ak_ followed by 24 random bytes. You see it once, at creation. We keep a sha256 hash, the first ten characters and the name you gave it, which is enough to tell your keys apart and not enough to use one. Every key carries scopes, and a route with no declared scope is closed to keys entirely, even to a key holding every scope you can hand out.

Keys belong to the paid plans. Free has none. Creating one on Free is refused, and a key whose paid time has run out answers 402, not 401 and not 403. The difference matters to a program: 401 sends it to issue a new key, 403 sends it to ask for more rights, 402 tells it the truth.

Some rules exist for the day a key leaks. A key cannot create keys or webhooks, only a person signed in with a password can. Blocking an account kills its keys on the next call. Revoking keeps the row, so the log can still name the key that acted, and the log records which key it was, not only which person. You may hold 20 live keys, the API allows 300 requests a minute by default with tighter limits on the sign-in routes, and the plan behind a key is re-checked at most once a minute.

ScopeWhat it opens
profiles:readThe profile list.
profiles:writeCreate, edit and delete a profile.
browser:runMark a profile open or closed on the server, without the right to edit it.
proxies:readThe saved proxy list, without passwords.
proxies:writeCreate, edit and delete a proxy, if the member's role allows it.
flows:readThe list of saved automation flows.
logs:readThe action log, the open log, sign-in addresses and the suspicion rules.

Webhooks tell your own server what happened

A webhook is our server calling yours when something happens in your workspace. Subscriptions are created in the cabinet by a person signed in with a password, never by a key, and you may keep up to ten. Events come from the audit trail, so they name what was actually recorded.

Each call is signed. The signature is an HMAC-SHA256 over the timestamp and the body, sent as X-Armanos-Signature next to X-Armanos-Event and X-Armanos-Timestamp. Both SDKs verify it and both refuse a signature older than five minutes, so an intercepted call cannot be replayed forever. Fields whose names look like a credential are stripped before sending: code, token, secret, password, link, hash. An invite code or a password reset link never leaves for a third party server, even one you own.

Delivery is honest about failure. Three attempts, ten seconds each, retried only on 5xx and 429, with a pause between them. Redirects are not followed, because an answer saying go to 127.0.0.1 would undo the address check. The last status, the reason in words and the number of failures in a row are written on the subscription and shown in the cabinet. After fifteen failures in a row the subscription switches itself off, with the reason written down rather than in silence.

The address is re-checked before every send, with the name resolved again. Private ranges are refused, including the cloud metadata address that hands out credentials for a whole account. A test send from the cabinet carries test: true, so your system does not create a phantom profile from it.

Method
POST, Content-Type application/json
X-Armanos-Event
profile.start
X-Armanos-Timestamp
Unix seconds, also part of the signature
X-Armanos-Signature
sha256=... HMAC of timestamp and body
Body
{ id, event, createdAt, data }
Delivery
At least once. Keep the id and drop repeats

Redirects are not followed, and 4xx answers other than 429 are not retried.

  • Profiles

    profile.create, profile.update, profile.delete, profile.start, profile.stop

  • Proxies

    proxy.create, proxy.update, proxy.delete

  • Keys

    apikey.create, apikey.revoke

  • Team

    team.invite, team.remove

  • Money

    plan.change

The SDKs stay a thin layer on purpose

There are two packages: one for Node, and a single file for Python that imports nothing outside the standard library. Both are at version 0.1.0, and both are deliberately thin. A kit that knows more than the server drifts away from it and starts to lie.

Both read the key and the address from the environment, default to our API address and give up after 30 seconds. Both check that the key looks like ak_ plus latin letters and digits before sending anything. A stray space from the clipboard used to break the request header itself, and the person saw server unreachable while the server was alive.

Errors are typed, not prose. A missing scope, a bad or revoked key and no connection at all are three different codes, so your script branches on the code and not on a message that may be translated.

Coverage follows the scopes: profiles list, create, update, delete, start and stop; proxies list, create, update and delete; the flow list; and the four log views. start and stop send a device id built from your machine name, because the profile lock has to tell two machines apart, and a single word sent from every machine broke both the lock and the seat count.

The half your receiver needs

Both kits ship a webhook signature check: verifyWebhook in Node, verify_webhook in Python. Both compare in constant time and refuse anything older than 300 seconds. Without that check anyone who learns your address can send you an invented event.

What this does not do

  • The local API reaches only the copy of ARMANOS on that one computer. There is no cloud route that opens a browser window for you, and a server key starts nothing: it only marks a profile busy and writes the log.
  • The local token is one token for the whole API. It has no scopes, no expiry and no per-route rights, and regenerating it stops every script you have at once.
  • The local API has no rate limit of its own. It trusts the machine it runs on, which is why the page check and the token gate carry the whole load there.
  • Server keys cannot create keys or webhooks, and any route with no declared scope is closed to them. The recycle bin, the fingerprint preview, proxy rotation and checks, and flow editing are all password only.
  • Webhook delivery is at least once, and a call carries only the fields left after redaction. Your receiver has to drop repeats by id and fetch the object itself when it needs more than the event.

How to check

Every claim on this page comes from a file you can open, and most are held by a stand that runs.

The local API refuses browser-shaped requests, and /status sits behind that check too
apps/desktop/src/lib/local-api.js · apps/desktop/test/local-api-gate.js
The unprotected app-level debug port is opened only for the built-in engine
apps/desktop/src/main/main.js · apps/desktop/test/cdp-port-only-when-needed.js
The token is random per installation and the API starts out disabled
apps/desktop/src/lib/api-config.js · apps/desktop/src/main/main.js
Server keys are a paid tool, and every scope guards a real route
apps/server/src/api-keys/api-keys.service.ts · apps/server/test/ключи-платные.js · apps/server/test/права-ключей-живые.js
Webhooks are signed, cannot be aimed inside our network, and record their failures
apps/server/src/webhooks/webhook-dispatch.service.ts · apps/server/test/webhooks.js · apps/web/test/webhooks-web.js

Turn it on and call it today

The API screen is in every build. Install the app, switch the API on, and point your script at 127.0.0.1.