SCWP wire reference
SCWP — ScaiClip Wire Protocol — is a binary protocol over WebSocket. All client-originated messages are dispatched by an 8-bit type code. Bodies are canonical CBOR (RFC 8949 §4.2.1).
Endpoint. wss://scaigrid.scailabs.ai/v1/modules/scaiclip/ws. Sub-protocol scaiclip/1. Auth via ?token=<sgk_> query param or Authorization: Bearer <jwt> header.
Frame format. 16-byte header (little-endian) + CBOR body. See §2 of the wire spec for the layout. Every request carries a monotone request_id; the response echoes it. Grid-originated pushes use the high half (0x8000_0000+).
Client → grid verbs#
Channel verbs#
| Type | Verb | Body | Response |
|---|---|---|---|
0x01 |
channel.list |
{} |
{1: [ChannelInfo], ...} |
0x02 |
channel.subscribe |
{1: [channel_id]} |
{1: [channel_id]} |
0x03 |
channel.unsubscribe |
{1: [channel_id]} |
{1: [channel_id]} |
0x04 |
channel.sync |
{1: channel_id, 2: from_seq, 3: limit} |
{1: [Envelope], 2: {seq: ciphertext}, 3: head_seq, 4: [tombstoned_seq]} |
0x05 |
channel.create |
{1: kind, 2: name, 3: group, 4: peer, 5: policy_overrides, 6: initial_wraps} |
{1: channel_id, 2: 1} |
0x06 |
channel.devices |
{1: channel_id} |
{1: [{1: device_id, 2: subject, 3: kem_pub, 4: sig_pub, 5: online}]} |
ChannelInfo is {1: channel_id_bytes, 2: kind_enum, 4: perms_bitfield, 5: key_version, 6: {1: ttl_default_s, 2: ttl_max_s, 3: history_depth}}.
Clip verbs#
| Type | Verb | Body | Response |
|---|---|---|---|
0x10 |
clip.publish |
{1: Envelope, 2: ciphertext, 3: [WrapRecord]} (wraps required iff ONE_SHOT) |
{1: seq, 2: grid_ts, 3: ttl_effective_s, 4: replayed} |
0x11 |
clip.fetch |
{1: channel_id, 2: seq} or {3: clip_id} |
{1: Envelope, 2: ciphertext} |
0x12 |
clip.pin |
{1: channel_id, 2: seq} |
{} |
0x13 |
clip.unpin |
{1: channel_id, 2: seq} |
{} |
0x14 |
clip.delete |
{1: channel_id, 2: seq} |
{} |
0x15 |
clip.claim |
{1: channel_id, 2: seq} |
{1: WrapRecord, 2: ciphertext} |
Key wrap verbs#
| Type | Verb | Body | Response |
|---|---|---|---|
0x20 |
key.wraps.get |
{1: channel_id, 2: key_version} |
{1: WrapRecord} or E_NOT_FOUND |
0x21 |
key.wraps.put |
{1: channel_id, 2: key_version, 3: [WrapRecord]} |
{} |
0x22 |
key.rekey.complete |
{1: channel_id, 2: new_key_version} |
{} |
WrapRecord = {1: device_id, 2: hpke_enc (32 bytes), 3: wrap_blob, 4: key_version}.
Presence#
| Type | Verb | Body | Response |
|---|---|---|---|
0x30 |
presence.devices |
{} |
{1: [{1: device_id, 2: online, 3: last_seen}]} |
Scratch storage#
| Type | Verb | Body | Response |
|---|---|---|---|
0x40 |
scratch.reserve |
{1: size, 2: sha256_hex, 3: mime_hint, 4: sharing_mode, 5: intended_ttl_s} |
{1: object_id, 2: upload_url, 4: expires_at_ms, 5: quota_warning?} |
0x41 |
scratch.commit |
{1: object_id, 2: sha256_hex, 3: [WrapRecord]} |
{1: object_id, 2: committed_at_ms, 3: expires_at_ms} |
0x42 |
scratch.get |
{1: object_id} |
{1: object_id, 2: download_url, 3: size, 4: sha256_hex} |
0x43 |
scratch.list |
{1: cursor?, 2: limit?} |
{1: [{1: object_id, 2: size, 3: mime, 4: sharing_mode, 5: created_at_ms, 6: expires_at_ms}]} |
0x44 |
scratch.delete |
{1: object_id} |
{1: object_id, 2: deleted_at_ms} |
0x45 |
scratch.publish |
{1: object_id, 2: expires_at_ms, 3: download_limit, 4: password_bound} |
{1: token, 2: public_url, 3: revoke_token, 4: expires_at_ms} |
Media proxy (ref_origin)#
| Type | Verb | Body | Response |
|---|---|---|---|
0x50 |
media.fetch |
{1: channel_id, 2: seq} |
{1: MediaRef, 2: transfer_id} or E_ORIGIN_OFFLINE |
0x52 |
media.pull_stream |
{1: transfer_id, 2: chunk_index, 3: chunk_bytes, 4: is_final} (origin → grid) |
{1: "ok", 2: chunk_index, 3: is_final} |
0x53 |
media.pull_range |
`{1: transfer_id | sha256_hex, 2: chunk_index}` |
0x54 |
media.cancel |
{1: transfer_id} |
{1: "cancelled"} |
SCSG (user-side)#
| Type | Verb | Body | Response |
|---|---|---|---|
0x62 |
session.deny |
{1: grant_id, 2: reason} |
{1: "denied"} |
Grid → client push events#
| Type | Event | Body |
|---|---|---|
0x80 |
ok |
(response body per verb) |
0x81 |
error |
{1: code, 2: wire_name, 3: message, 4: detail} — see error codes |
0x90 |
clip.new |
{1: Envelope, 2: ciphertext?} (inline for small clips) |
0x91 |
clip.expired |
{1: channel_id, 2: seq, 3: reason} |
0x92 |
clip.claimed |
{1: channel_id, 2: seq, 3: claimant_subject, 4: claimant_device_id, 5: at_ms} |
0x93 |
clip.deleted |
{1: channel_id, 2: seq} |
0x94 |
rekey.required |
{1: channel_id, 2: new_key_version, 3: reason} |
0x95 |
channel.membership |
{1: channel_id, 2: added_devices?, 3: removed_devices?} |
0x96 |
wrap.request |
{1: channel_id, 2: requester_device_id, 3: requester_subject, 4: requester_kem_pub, 5: key_version} |
0x97 |
wraps.ready |
{1: channel_id, 2: target_device_id, 3: key_version} |
0xA0 |
session.grant_request |
{1: grant_id, 2: service_id, 3: reason, 4: {1: channel_ids, 2: capabilities}, 5: duration_max_s, 6: ephemeral_sig_pub, 7: consent_deadline_ms, 8: service_display_name} |
0xB0 |
presence.device_state |
{1: device_id, 2: online, 3: at_ms} |
Envelope key registry#
| Key | Meaning | Notes |
|---|---|---|
| 1 | clip_id |
UUIDv7 (client-generated) |
| 2 | channel_id |
bytes(16) |
| 3 | seq |
grid-assigned |
| 4 | sender |
text (usr_alice) |
| 5 | device_id |
bytes(16) or text (nanoid) |
| 6 | client_ts |
ms |
| 7 | grid_ts |
ms, grid-assigned |
| 8 | type_hint |
0-4 |
| 9 | size_class |
0 inline · 1 reference |
| 10 | cflags |
bit0 SENSITIVE · bit1 ONE_SHOT · bit2 PINNED |
| 11 | ttl_s |
client-requested, grid-clamps |
| 12 | key_version |
channel key at publish time |
| 13 | source_app |
optional identifier ("mobile", "browser", …) |
| 14 | payload_hash |
SHA-256 of ciphertext |
| 15 | sig |
Ed25519 over canonical CBOR of keys 1-14 |
| 20 | payload_ref |
MediaRef map (optional) |
Related#
- SCSG verb reference — the service-side sub-protocol.
- Error codes — every
E_*value and its detail shape. - Channels, envelopes, wraps, claims — the conceptual overview.
- Encryption & consent — the crypto profile.