# Gateway

> A WebSocket / JSON gateway: the public APIs the Python SDK can call are callable from JS, C#, Go, Rust, browser pages and programs on another machine. Three roles, with a bundled JS client and browser demo page; latency is the fast lane plus about 1 ms.

Source: https://war3ai.com/en/docs/gateway/

The gateway wraps the fast lane and the pushed state in **WebSocket / JSON**. The public APIs the Python SDK can call in the [API catalog](https://war3ai.com/en/api/) are callable from JS, C#, Go, Rust, browser pages, programs on another machine and LLMs, with the same method names and parameters. Latency is the fast lane plus about 1 ms.

**Easiest: Farsight's home page, Control Center → Gateway → Start** (stop, restart, view logs and open the demo page from the same card). From the command line:

```bash
python gateway/server.py                 # ws://127.0.0.1:8870/ws (the port is ports.gateway in openwar3.json)
python gateway/server.py --open          # same as above, and opens the demo page http://127.0.0.1:8870/demo once the port is listening
python gateway/server.py --host 0.0.0.0  # for LAN use: a token is required automatically (bin/gateway/token.txt)
python gateway/server.py --allow-origin http://localhost:5173   # lets your own web page connect too
```

## Connections and roles

Connection URL: `ws://127.0.0.1:8870/ws?inst=9&role=dev` (you can use `pid=` instead of `inst=`; add `&token=` when a token is required).

| Role | Can call | Best for |
|---|---|---|
| `dev` | Everything: observe, command, game control, sandbox (changing the world with JASS), drawing UI | Local tools, [gameplay mods](https://war3ai.com/en/docs/mods/), companions |
| `player` (add `&player=N`) | Observe, command player N's units, drawing UI; **fair mode by default**, so it sees only what's within player N's vision (`&fair=0` turns it off) | A bot or LLM playing for one player |
| `observer` | Read-only (the runtime rejects its commands outright) | Spectating, commentary, data collection |

`player` doesn't get: game control such as ending the game, changing the speed or pausing; `players` and `enemy_ai_plan`, which reveal other players' hands; `canvas.image`, which makes the game process open a local file; or JASS. Queries that take a player number, such as `resources`, `tech` and `stats`, can only query its own player.

One connection is one session and takes one fast lane (the runtime has 16 in total). The gateway allows at most 12 sessions at once, leaving a few for bots, mods and Farsight. On disconnect, only what this session drew and its own hotkeys are removed; what other programs drew is left alone.

## Messages

After connecting, you first receive `hello`: the protocol version, the role, the game's process ID, and the list of methods this role can call. After that, every request carries an `id`, and the response carries the same `id`:

```json
→ {"id": 1, "op": "call", "method": "units", "args": ["me"]}
← {"id": 1, "ok": true, "result": [{"addr": 123456, "type": "hpea", "owner": 0, "x": -4480, "y": 2120, "hp": 220, "hp_max": 220}]}

→ {"id": 2, "op": "call", "method": "move", "args": [[{"unit": 123456}], 100, 200]}
→ {"id": 3, "op": "call", "method": "ui.button", "args": ["shop", "Buy potion"], "kwargs": {"screen": [40, 300]}}
→ {"id": 4, "op": "subscribe", "state": {"hz": 4, "units": "mine"}, "events": true}
← {"type": "state", ...}    {"type": "events", ...}        pushed continuously from then on
→ {"id": 5, "op": "overview"}                             one-page overview: resources, unit counts, heroes, visible enemies, production
→ {"id": 6, "op": "jass", "code": "call PingMinimapEx(0, 0, 3, 255, 0, 0, false)"}    dev only
→ {"id": 7, "op": "api"}                                  method catalog (there are also ping / unsubscribe)
```

- **Unit arguments** are written as `{"unit": addr}`, where the address is the `addr` in the unit's JSON; you can add `"handle": [lo, hi]` to check that the address hasn't been reused by another unit.
- **Method names** are the public methods of Game, plus `ui.*` (button / choice / toast / hotkey / mouse / cursor…), `canvas.*` (text / panel / bar / image / circle / path / remove…) and `jass.<function name>` (dev only).
- A remote client can't pass callback functions: clicks and hotkeys arrive through the event push, and the `ui.click` event carries `key`. See [UI & input](https://war3ai.com/en/docs/ui-input/).
- A failed call gets an error response for that call only (`ok: false` plus `error`); the connection stays open. The same goes for a message that isn't JSON.
- Event JSON fields match the [W3P protocol](https://war3ai.com/en/docs/protocol/), plus convenience fields (`spell`, `key`, `text`, `chat`, `button`, `player`, `mods`).

HTTP works too, which suits one-off calls and curl: `GET /api?role=player` lists the method catalog, and `POST /call` with `inst`, `role`, `method`, `args` and `kwargs` makes a single call. `/call` reuses sessions: when the game is restarted in a new process it switches to a new session automatically, and sessions idle for 10 minutes are closed.

## Clients

**JS** (browser or Node 22+, zero dependencies): `gateway/clients/js/openwar3.mjs`

```js
import { OpenWar3, unit } from "./openwar3.mjs";

const ow = new OpenWar3("ws://127.0.0.1:8870/ws", { inst: 9, role: "dev" });
await ow.connect();
const mine = await ow.api.units("me");
await ow.api.move(mine.slice(0, 3).map(unit), 100, 200);
await ow.api.ui.button("hi", "Click me", { screen: [40, 300] });  // a trailing plain object = keyword arguments
ow.on("event:ui.click", (e) => console.log("clicked", e.key));
await ow.subscribe({ state: { hz: 2, units: "mine" }, events: true });
```

Node 20 / 21 needs `--experimental-websocket`. The full example is in `gateway/clients/js/example.mjs`.

**Browser demo page** `http://127.0.0.1:8870/demo`: the game overview, a table of our units, placing a button in the game, and the event stream, all on one page.

**Other languages**: any WebSocket library + the JSON above is all you need; no need to touch shared memory.

**LLMs**: use the [MCP server](https://war3ai.com/en/docs/mcp/) directly; it turns the common tasks into ready-made tools.

## Measured

2026-09-25, checked item by item against a real game, 16/16 (9 for the gateway + 7 for MCP): handshake (121 methods for the dev role), `units('me')`, the one-page overview, a screen toast, placing a button; after subscribing, clicking that button in the game → `ui.click` pushed to the client; JASS; passing a bad unit reports an error for that call only; HTTP `/call` (observer role).

The JS client (Node) and the browser demo page were run too: a button placed from the web page was clicked in the game, and the page's event log received `ui.click`.

## Security

- By default it listens only on `127.0.0.1` and needs no token (same as Farsight). When `--host` isn't a local address, a token is required automatically; `--auth` requires one locally too.
- **Other websites in a browser can't connect**: every connection a browser opens carries its origin (`Origin`), and the gateway only accepts its own demo page and the URLs given with `--allow-origin`. Programs such as Python, Node and curl send no origin and connect as usual. When listening only locally, it also checks `Host`, blocking attacks that resolve an outside domain name to the local machine.
- The role is declared by the client when it connects: in local mode it's a convention, not a security boundary. For the Arena, the referee process has to decide who gets which role; see [Arena](https://war3ai.com/en/arena/).
