# Writing a client

Use this if you are not on Python or TypeScript, or you would rather not take a dependency.
Everything the SDKs do is these frames: mint a `socket_url` over REST as in
[Connecting](/websocket/connecting), open a WebSocket to it, send a
[frame](/websocket/frames), and match the reply on its `msgid`.

No SDK. Python needs one dependency (`pip install websockets`), and TypeScript needs none,
since Node 22 ships both `fetch` and `WebSocket`.

<CodeTabs syncKey="lang">

```python title="Python"
import json
import os
import urllib.request
from typing import Any

from websockets.sync.client import connect

TOKEN = os.environ["ZEROBULL_API_TOKEN"]


def controller_session() -> dict[str, Any]:
    """Mint a socket URL with the REST endpoint."""
    request = urllib.request.Request(
        "https://0bull.net/api/v1/phone-controller",
        headers={"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"},
    )
    with urllib.request.urlopen(request, timeout=30) as response:
        data: dict[str, Any] = json.load(response)
        return data


session = controller_session()
print("farm_online:", session["farm_online"])

with connect(session["socket_url"], ping_interval=20) as socket:
    socket.send(json.dumps({"fun": "/app/phones/list", "msgid": "1", "data": {}}))
    for message in socket:
        frame: dict[str, Any] = json.loads(message)
        if frame.get("msgid") != "1":
            continue
        if frame["status"] != 200:
            raise SystemExit(f"{frame['status']}: {frame['message']}")
        for phone in frame["data"]:
            print(phone["slot"], phone["name"], phone["can_control"])
        break
```

```ts title="TypeScript"
const token = process.env.ZEROBULL_API_TOKEN;
if (!token) throw new Error("Set ZEROBULL_API_TOKEN");

interface Phone {
  slot: string;
  name: string;
  can_control: boolean;
}

interface ControllerSession {
  phones: Phone[];
  socket_url: string;
  farm_online: boolean;
}

const response = await fetch("https://0bull.net/api/v1/phone-controller", {
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
});
if (!response.ok) throw new Error(`Could not mint a socket URL: ${response.status}`);
const session = (await response.json()) as ControllerSession;
console.log("farm_online:", session.farm_online);

const socket = new WebSocket(session.socket_url);
await new Promise<void>((resolve, reject) => {
  socket.addEventListener("open", () => resolve(), { once: true });
  socket.addEventListener("error", () => reject(new Error("Socket failed to open")), {
    once: true,
  });
});

socket.addEventListener("message", (message: MessageEvent) => {
  const frame = JSON.parse(String(message.data)) as {
    msgid?: string;
    status?: number;
    message?: string;
    data?: Phone[];
  };
  if (frame.msgid !== "1") return;
  if (frame.status !== 200) throw new Error(`${frame.status}: ${frame.message}`);
  for (const phone of frame.data ?? []) {
    console.log(phone.slot, phone.name, phone.can_control);
  }
  socket.close();
});

socket.send(JSON.stringify({ fun: "/app/phones/list", msgid: "1", data: {} }));
```

</CodeTabs>

Frames without your `msgid` are [events](/websocket/events), so skip them while you wait for
a reply.

## Next

- [Frames and replies](/websocket/frames): every field, status and error shape.
- [Events](/websocket/events): the pushes that arrive without being asked for.
