# Kelly Wire

The live socket: how to connect, authenticate and resume, the control messages, and the ticket and single-topic stream operations.

## Connect
| Server | URL | What it is |
| --- | --- | --- |
| Kelly Wire | `wss://wire.askkelly.ai/v1` | Kelly Wire. Browsers connect from an allowed origin: first-party tickets accept https://askkelly.ai, https://www.askkelly.ai, card tickets the card origins and *.claudemcpcontent.com. A ticket from any other origin closes with 4013. |
| Server-sent events | `https://api.askkelly.ai/v1/stream` | The SSE alternative on the API host, for widgets and cards whose frames cannot open a socket. |

## The protocol
Live topics over one WebSocket at wss://wire.askkelly.ai/v1, subprotocol `kelly.v1`. Every frame is one JSON object. Data messages (snapshot and delta) use one envelope, `{op, type, topic, seq, source_ts, emit_ts, source, data}`; control messages carry `op` and their own members. Clients ignore unknown `op` and `type` values and unknown members.

Authenticate with a single-use ticket of 60 seconds or less in the first message, within 5 seconds; a browser may offer it as a second subprotocol instead. Long-lived keys never go in a URL. The credential that minted the ticket decides the surface, never the host.

Each topic sends a snapshot with its last_seq, then deltas at last_seq + 1. Subscribing to a topic already held sends its snapshot again. Gap rule: Any sequence jump or checksum mismatch on a topic means: drop that topic's state and fetch a new snapshot, then apply deltas from its last_seq + 1.

The server pings every 20 seconds; two missed pings mean the connection is dead. A resume within 120 seconds sends the session ID, the resume token and the last seq per topic, needs no new ticket and replays missed deltas; topics outside the window get resume_failed and a fresh snapshot. A connection lives 24 hours at most and ends with a drain notice 30 seconds ahead.

Close codes: 4000–4099 Fatal for this request: do not reconnect unchanged.; 4100–4199 Back off: reconnect after at least 1 second, with exponential backoff and jitter.; 4200–4299 Reconnect now and resume immediately..

Every snapshot carries expected_latency (seconds, basis and source) and liveness, and a feed message says when either changes. Nothing claims to be faster than its feed.

Per-book prices reach signed-in first-party tickets only (D4); no edge travels on a shared topic (D21); every member passes the rights register field by field. Off the site, topics carry engine-computed values only.

## Tickets and single-topic streams
| Method | Path | Title | Access | Price | MCP tool |
| --- | --- | --- | --- | --- | --- |
| POST | [`/v1/wire/tickets`](#get-wire-ticket) | Get a Kelly Wire ticket | public | [object Object] | none |
| GET | [`/v1/stream/{topic}`](#stream-topic) | Stream one topic as server-sent events | public | [object Object] | none |

`POST /v1/wire/tickets`

## Get a Kelly Wire ticket
public, no key needed · [object Object] credits, not charged during the beta · status live

Mints a single-use ticket that lives 60 seconds, for the socket (wss://wire.askkelly.ai/v1, sent in the first message) or for one stream (GET /v1/stream/{topic}?ticket=). Needs an API key with the wire:connect scope, or an OAuth token. Stream tickets name their topics; socket tickets may name some (bound to them) or none. Each topic is still checked against the plan when it is opened.

### Parameters
Body, `application/json`:

- `transport` string  
  ws for the socket (wss://…/v1), sse for GET /v1/stream/{topic}. A ticket works on the transport it was minted for only.  
  One of: `ws`, `sse` (open: new values may be added).  
  default "ws"
- `topics` array of string · nullable  
  Topics the ticket is bound to, such as game:gm_… or market:mkt_…; required for sse. At most the plan's topic limit. Omit on a socket ticket to subscribe to any topic the plan allows.  
  max items 5000
- `mode` string · nullable  
  test serves the recorded Sunday (sample data) to any credential; live is refused for a test-mode key. Omitted: the credential's own mode.  
  One of: `live`, `test` (open: new values may be added).

### Response
`201` The ticket. It is shown once; responses are never cached.

- `object` const · required  
  Always `wire_ticket`.  
  Always `"wire_ticket"`.  
  default "wire_ticket"
- `ticket` string · required  
  The ticket (wtkt_…); single use, 60 seconds or less.  
  pattern ^wtkt_[0123456789abcdefghjkmnpqrstvwxyz]{20}$
- `expires_at` string · required  
  When the ticket stops working, unused.  
  format date-time
- `topics` array of string · required · nullable  
  The topics it is bound to; null for any topic the plan allows.
- `transport` string · required  
  The transport it works on.  
  One of: `ws`, `sse` (open: new values may be added).
- `mode` string · required  
  live data, or test: the recorded Sunday.  
  One of: `live`, `test` (open: new values may be added).
- `url` string · required  
  Where to connect: the socket's URL, or the stream's URL (its {topic} filled in when the ticket names one topic). It never contains the ticket.

### Errors
[`invalid_request`](/problems/invalid_request) · [`credential_in_url`](/problems/credential_in_url) · [`unsupported_api_version`](/problems/unsupported_api_version) · [`unauthenticated`](/problems/unauthenticated) · [`method_not_allowed`](/problems/method_not_allowed) · [`rate_limited`](/problems/rate_limited) · [`internal_error`](/problems/internal_error) · [`service_unavailable`](/problems/service_unavailable) · [`quota_exhausted`](/problems/quota_exhausted) · [`insufficient_scope`](/problems/insufficient_scope) · [`not_entitled`](/problems/not_entitled) · [`payload_too_large`](/problems/payload_too_large) · [`too_many_topics`](/problems/too_many_topics)

### Example
```http
POST https://api.askkelly.ai/v1/wire/tickets
Authorization: Bearer $KELLY_API_KEY
Content-Type: application/json

{
  "transport": "sse",
  "topics": [
    "game:gm_hwxy9tewzag7fee8hb01"
  ],
  "mode": "test"
}
```

```json
{
  "type": "https://docs.askkelly.ai/problems/insufficient_scope",
  "title": "Insufficient scope",
  "status": 403,
  "code": "insufficient_scope",
  "detail": "This needs the wire:connect scope.",
  "param": "wire:connect",
  "doc_url": "https://docs.askkelly.ai/problems/insufficient_scope",
  "request_id": "req_ecea15c85392623ab2018a24",
  "retryable": false,
  "display_message": "This app needs one more permission to do that."
}
```

```http
POST https://api.askkelly.ai/v1/wire/tickets
Authorization: Bearer $KELLY_API_KEY
Content-Type: application/json

{
  "transport": "sse",
  "topics": [
    "game:gm_hwxy9tewzag7fee8hb01"
  ],
  "mode": "test"
}
```

```json
{
  "object": "wire_ticket",
  "ticket": "wtkt_nj7ftwgg0hbac03jg4wb",
  "expires_at": "2026-10-04T17:31:00Z",
  "topics": [
    "game:gm_hwxy9tewzag7fee8hb01"
  ],
  "transport": "sse",
  "mode": "test",
  "url": "https://api.askkelly.ai/v1/stream/game:gm_hwxy9tewzag7fee8hb01"
}
```

`GET /v1/stream/{topic}`

## Stream one topic as server-sent events
public, no key needed · [object Object] credits, not charged during the beta · status live

One topic as server-sent events (STR-21), for widgets, simple clients and cards whose host blocks sockets. Each event's `id` is the topic's seq: first `event: snapshot` (the topic's state, its last seq, expected latency and liveness), then one event per change, named by its type (game.state, market.update, market.suspended, ...), each seq the previous + 1. Send `Last-Event-ID` to resume: missed changes replay while they are within the 120-second window, else a fresh snapshot comes first. A gap is answered with a fresh snapshot. A comment line pings every 20 seconds; `event: drain` comes 30 seconds before the stream's 24-hour limit, and `event: error` (a catalogue code) when the topic is no longer available, after which the stream ends. Off askkelly.ai streams carry engine-computed values only. Each open stream costs wire_topic_hour credits when it opens and each hour it stays open.

### Parameters
- `topic` string · path · required  
  The topic, such as game:gm_… or market:mkt_… (the Wire topics in the AsyncAPI document).  
  pattern ^[a-z]+:[A-Za-z0-9_\-/:]+$ · max length 200

- `ticket` string · query  
  A ticket minted for sse that names this topic (POST /v1/wire/tickets): single use, 60 seconds or less (STR-24). The one credential a URL may carry; never an API key. Omit it on askkelly.ai, where the session is used.  
  pattern ^wtkt_[0123456789abcdefghjkmnpqrstvwxyz]{20}$
- `last_event_id` string · query  
  The same as Last-Event-ID, for a new EventSource, which can't set the header; the header wins when both are sent.  
  pattern ^[0-9]{1,18}$

- `Last-Event-ID` string · header  
  The last event id received (a seq), to resume after a reconnect.  
  pattern ^[0-9]{1,18}$

### Response
`200` The stream. Events: snapshot, one per change (named by its type), drain, error and warning; comment lines are pings.

`text/event-stream`: Server-sent events; each data line is one Kelly Wire envelope.

### Errors
[`invalid_request`](/problems/invalid_request) · [`credential_in_url`](/problems/credential_in_url) · [`unsupported_api_version`](/problems/unsupported_api_version) · [`unauthenticated`](/problems/unauthenticated) · [`method_not_allowed`](/problems/method_not_allowed) · [`rate_limited`](/problems/rate_limited) · [`internal_error`](/problems/internal_error) · [`service_unavailable`](/problems/service_unavailable) · [`quota_exhausted`](/problems/quota_exhausted) · [`insufficient_scope`](/problems/insufficient_scope) · [`not_entitled`](/problems/not_entitled) · [`not_found`](/problems/not_found) · [`too_many_topics`](/problems/too_many_topics)

## Control messages
The socket itself: authentication, subscriptions, heartbeats, drains and resumes. Connect to wss://wire.askkelly.ai/v1 with the subprotocol kelly.v1. Every topic below is subscribed to by name on this one socket.

### `auth`
You send. The first message: a ticket minted over authenticated REST (POST /v1/wire/tickets with a key or token, POST /me/wire/tickets on askkelly.ai, or get_wire_ticket for a card), within the authentication deadline (STR-6). Browsers may instead offer the ticket as a second subprotocol, kelly.ticket.{ticket}. Sent again mid-session with a new ticket and a request_id, it refreshes the credential in-band (STR-22) and is answered by an ack. Long-lived keys never go in a URL.

- `op` const · required  
  Always `auth`.  
  Always `"auth"`.
- `ticket` string · required  
  The ticket (wtkt_…), single-use, living 60 seconds or less.
- `request_id` string  
  Only on an in-band refresh; echoed in the ack.

### `hello`
Kelly sends. Sent after authentication or a resume: the heartbeat interval, the session ID and resume token, the resume window, when the connection's lifetime ends and the published limits.

- `op` const · required  
  Always `hello`.  
  Always `"hello"`.
- `session_id` string · required  
  Handle for resuming this session (wses_…).
- `resume_token` string · required  
  Secret that proves a resume comes from this session; a new one in every hello.
- `heartbeat_seconds` integer · required  
  Seconds between server pings.  
  min 0
- `resume_seconds` integer · required  
  Seconds of per-topic history kept for a resume.  
  min 0
- `expires_at` string · required  
  RFC 3339 time the 24-hour lifetime ends; a drain notice comes first.  
  format date-time
- `limits` any · required  
  topics, inbound_per_second and max_frame_bytes for this connection (STR-14).
- `mode` string · required  
  live, or test for test-mode credentials (the recorded Sunday).  
  One of: `live`, `test` (open: new values may be added).
- `rg_line` string · required  
  The cost line (COMP-1, D19): 21+ and the helpline, from brand.toml [cost_line]. Show it with anything built on a wagering topic; every wagering topic's snapshot carries it too.

### `subscribe`
You send. Adds topics. Entitlement is checked per topic (STR-7). Subscribing to a topic already held sends its snapshot again (a resync).

- `op` const · required  
  Always `subscribe`.  
  Always `"subscribe"`.
- `request_id` string  
  Echoed in the ack.
- `topics` array of string · required  
  Topics to add.

### `unsubscribe`
You send. Removes topics.

- `op` const · required  
  Always `unsubscribe`.  
  Always `"unsubscribe"`.
- `request_id` string  
  Echoed in the ack.
- `topics` array of string · required  
  Topics to remove.

### `list_subscriptions`
You send. Asks for the current topic set.

- `op` const · required  
  Always `list_subscriptions`.  
  Always `"list_subscriptions"`.
- `request_id` string  
  Echoed in the ack.

### `ack`
Kelly sends. Answers subscribe, unsubscribe, list_subscriptions and an in-band auth, echoing the full current topic set (STR-8).

- `op` const · required  
  Always `ack`.  
  Always `"ack"`.
- `request_id` string  
  The request this answers.
- `topics` array of string · required  
  The full current topic set.

### `feed`
Kelly sends. A topic's expected latency or liveness changed: a poller paused when its budget ran out (STR-5), or no update arrived within the topic's limit, so it is now stale.

- `op` const · required  
  Always `feed`.  
  Always `"feed"`.
- `topic` string · required  
  The topic.
- `expected_latency` any  
  seconds, basis and source.
- `liveness` any  
  state, as_of and limit_seconds.

### `error`
Kelly sends. A per-topic or per-request error with a catalogue code, such as not_entitled (STR-7, STR-22).

- `op` const · required  
  Always `error`.  
  Always `"error"`.
- `code` string · required  
  A code from the error catalogue.
- `detail` string  
  Prose; may change.
- `topic` string  
  The topic it concerns, if any.
- `request_id` string  
  The request it answers.
- `retryable` boolean  
  Whether retrying can succeed.

### `warning`
Kelly sends. The connection is falling behind: read faster or drop topics, or it closes with 4108 (STR-13).

- `op` const · required  
  Always `warning`.  
  Always `"warning"`.
- `code` string · required  
  slow_consumer.
- `detail` string  
  Prose; may change.

### `ping`
Kelly sends. Heartbeat: answer with pong. Two missed pings mean the connection is dead (STR-12).

- `op` const · required  
  Always `ping`.  
  Always `"ping"`.
- `ts` string · required  
  RFC 3339 server time.

### `pong`
You send. Answers a ping.

- `op` const · required  
  Always `pong`.  
  Always `"pong"`.
- `ts` string  
  The ping's ts, echoed.

### `drain`
Kelly sends. The server will close this connection: open a new one and resume before the deadline (STR-16, STR-23).

- `op` const · required  
  Always `drain`.  
  Always `"drain"`.
- `deadline_at` string · required  
  RFC 3339 time the connection closes.  
  format date-time

### `resume`
You send. The first message of a reconnect, instead of auth: the session ID, its resume token and the last seq seen per topic (STR-11). Inside the resume window it needs no new ticket.

- `op` const · required  
  Always `resume`.  
  Always `"resume"`.
- `session_id` string · required  
  From hello.
- `resume_token` string · required  
  From the latest hello.
- `last_seq` object · required  
  Topic → last seq seen.

### `resume_failed`
Kelly sends. Some or all topics fell outside the replay window: drop their state; a snapshot for each follows (STR-11). When the session itself has expired, the connection then closes with 4011 and the client needs a new ticket.

- `op` const · required  
  Always `resume_failed`.  
  Always `"resume_failed"`.
- `topics` array of string · required  
  Topics that need a new snapshot.

Generated from Kelly Wire 2026-10-05 and Kelly API 2026-10-05.

---

Generated from Kelly Wire 2026-10-05 and Kelly API 2026-10-05

Support: support@askkelly.ai · Security and disclosure: https://docs.askkelly.ai/security

Information is the edge. Odds are information, not advice · 21+ · Get help
