# Frames and replies

Every call over the socket is one JSON frame in and one JSON frame out.

## The request frame

```json
{ "fun": "/app/phones/list", "msgid": "1", "data": {} }
```

- **`fun`** names the operation. Every fun starts with `/app/`.
- **`msgid`** is yours to choose, and it is **required**. The reply echoes it back. Send a
  frame without it and you get `400`.
- **`data`** holds the fun's parameters. Omit it, or send `{}`, for a fun that takes none.

Phone funs take the phone's `slot` in `data`. Get it from `/app/phones/list` or from the
`phones` list in the [connect reply](/websocket/connecting#1-get-a-socket-url).

## The success envelope

A successful call replies with the same status and body the matching REST endpoint returns:

```json
{ "fun": "/app/phones/list", "msgid": "1", "status": 200, "data": [ ... ] }
```

`status` is `200` for a read, `201` for something created, `202` for work accepted and queued
on a phone, `204` for a delete (with `data` as `null`).

## The error envelope

A failed call replies with `message` in place of `data`, and adds `errors` when the failure
was a validation failure:

```json
{
  "fun": "/app/phones/input",
  "msgid": "2",
  "status": 422,
  "message": "The slot field is required.",
  "errors": { "slot": ["The slot field is required."] }
}
```

`errors` maps each rejected field to its messages. Read `message` for the summary.

## Match on msgid, not on order

Calls run independently. With several in flight, replies can come back in any order, and
[events](/websocket/events) arrive in between. Keep a table of the ids you are waiting on and
match each reply on `msgid`.

A frame with no `msgid` at all is an event, not a reply.

```json
→ { "fun": "/app/phones/ocr",  "msgid": "1", "data": { "slot": "b3f..." } }
→ { "fun": "/app/phones/list", "msgid": "2", "data": {} }
← { "fun": "/app/phones/list", "msgid": "2", "status": 200, "data": [ ... ] }
← { "event": "run", "data": { ... } }
← { "fun": "/app/phones/ocr",  "msgid": "1", "status": 200, "data": { "text": "..." } }
```

## Status codes

| Status | What caused it |
|---|---|
| `400` | The frame is malformed: no `msgid`, a `data` that is not an object, or a phone fun with no `slot`. |
| `403` | Your token lacks the ability this fun needs, the API token behind the connection was revoked or expired, or the `slot` names a phone you cannot view or control. |
| `404` | The `fun` does not exist, or the record you named does not exist or is not yours. |
| `409` | A conflict: cancelling a submission that already finished, or deleting one that is still in flight. |
| `422` | Validation failed. Read `errors` for the fields. |
| `429` | You passed the screen-read cap. Screen reads are limited to 60 a minute, and `message` says how many seconds to wait. |
| `502` | A phone could not be reached, or a screen read could not be completed. |
| `503` | The capability is not ready yet. Agent runs return this when the phone agent is not available. |
| `500` | Something unexpected went wrong. Retry, and quote the call if it persists. |

`401` never appears in a fun reply. It belongs to the REST call that mints the socket URL: a
missing or invalid API token stops you before you connect. See
[Connecting](/websocket/connecting#2-connect-within-10-minutes).

A `404` on a record you believe exists usually means it is not yours. Accounts and
submissions are scoped to your account, so "not found" and "not yours" read the same.

## Limits

Frames are not counted against the REST rate limit. The one cap that applies is on screen
reads: 60 a minute per account, shared between `/app/phones/ocr`, the REST screen-read
endpoint and the MCP tool. Over that you get `429`.

## Next

- [Phones](/websocket/phones): the phone funs and their `data` fields.
- [Accounts](/websocket/accounts), [Submissions](/websocket/submissions) and [Billing](/websocket/billing): the rest of the funs.
- [Events](/websocket/events): frames that arrive without a `msgid`.
