---
title: Quickstart
order: '2'
summary: Enrol two devices, send a clip from one, receive it on the other. Five minutes
  end-to-end.
path: quickstart
status: published
---

In five minutes you'll enrol two devices into a ScaiClip personal channel, publish an encrypted clip from one, and see it arrive on the other. Everything ciphered client-side; the grid holds only opaque bytes.

You need:

- A ScaiGrid endpoint (managed or self-hosted) with the ScaiClip module enabled.
- A user sgk_ key with scopes `scaiclip:read` and `scaiclip:write`, or a user JWT.
- Python 3.10+ with `pip install cbor2 pynacl httpx websockets`.

```bash
export SCAIGRID_HOST="https://scaigrid.scailabs.ai"
export SCAIGRID_API_KEY="sgk_..."
```

## 1. Register two devices

Each client generates its own X25519 KEM keypair and Ed25519 signing keypair. Private keys never leave the device.

```python
from nacl.public import PrivateKey
from nacl.signing import SigningKey
import base64, httpx

def gen_device():
    kem = PrivateKey.generate()
    sig = SigningKey.generate()
    return {
        "kem_priv": bytes(kem),
        "kem_pub":  bytes(kem.public_key),
        "sig_priv": bytes(sig),
        "sig_pub":  bytes(sig.verify_key),
    }

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

alice_a, alice_b = gen_device(), gen_device()

import os
HOST = os.environ["SCAIGRID_HOST"]
KEY  = os.environ["SCAIGRID_API_KEY"]

def enrol(dev, name):
    r = httpx.post(f"{HOST}/v1/modules/scaiclip/devices",
        headers={"Authorization": f"Bearer {KEY}"},
        json={
            "kem_pub": b64u(dev["kem_pub"]),
            "sig_pub": b64u(dev["sig_pub"]),
            "kind": "desktop",
            "platform": "linux",
            "name": name,
        })
    r.raise_for_status()
    return r.json()["data"]["device_id"]

alice_a["id"] = enrol(alice_a, "Alice Laptop")
alice_b["id"] = enrol(alice_b, "Alice Phone")
print(f"laptop: {alice_a['id']}\nphone:  {alice_b['id']}")
```

## 2. Connect both devices to the personal channel

Each device opens its own WebSocket on `/v1/modules/scaiclip/ws`. The personal channel is auto-created on first connect.

```python
import asyncio, websockets

async def connect(dev):
    ws = await websockets.connect(
        f"{HOST.replace('https', 'wss')}/v1/modules/scaiclip/ws"
        f"?token={KEY}&device_id={dev['id']}",
        subprotocols=["scaiclip/1"],
    )
    return ws
```

Every client-originated frame carries a 16-byte header (`ver | type | flags | request_id | body_length | reserved`) followed by a canonical-CBOR body. See the [SCWP reference](./reference/scwp.md) for the wire format.

## 3. Publish a clip from the phone

The phone encrypts a UTF-8 text clip with a fresh XChaCha20-Poly1305 content key, wraps that key for every enrolled device via HPKE, signs the envelope, and sends `clip.publish`.

```python
from nacl.secret import SecretBox
import secrets, cbor2, struct

TYPE_CLIP_PUBLISH = 0x10

def frame(type_, req_id, body):
    return struct.pack("<BBHII", 0x01, type_, 0, req_id, len(body)) + b"\0"*4 + body

async def publish_text(ws, dev, text):
    content_key = secrets.token_bytes(32)
    nonce = secrets.token_bytes(24)
    ciphertext = SecretBox(content_key).encrypt(text.encode(), nonce)
    envelope = {
        1: uuid7_bytes(),       # clip_id
        2: personal_channel_id, # channel_id
        4: "usr_alice",         # sender subject
        5: bytes.fromhex(dev["id"]) if len(dev["id"]) == 32 else dev["id"],
        6: int(time.time()*1000),
        8: 1,                   # type_hint: text
        9: 0,                   # size_class: small
        10: 0,                  # cflags: none
        11: 60*60,              # ttl_s: 1 hour
    }
    envelope["sig"] = sign(envelope, dev["sig_priv"])
    body = cbor2.dumps({1: envelope, 2: ciphertext}, canonical=True)
    await ws.send(frame(TYPE_CLIP_PUBLISH, 1, body))
    resp = await ws.recv()
    print("published:", resp[16:])
```

Some helpers (`uuid7_bytes`, `sign`, `personal_channel_id` resolution via `channel.list`) are omitted for brevity — see the [two-device tutorial](./tutorials/two-device-round-trip.md) for the complete runnable script.

## 4. Receive on the laptop

The laptop's socket sees `clip.new` (0x90) with the same envelope. It verifies the signature against the phone's `sig_pub` (fetched via `channel.devices`), unwraps its own copy of the content key with its KEM private key, decrypts, hits the OS clipboard.

Total round-trip: **under 250 ms warm**.

## What just happened

- **The grid never saw plaintext.** Ciphertext + wraps only.
- **The phone signed the envelope.** The laptop verified before decrypting — spoofed envelopes are rejected.
- **Both wraps were pre-installed at enrolment.** New devices trigger the [onboarding flow](./concepts/channels.md#onboarding) with `wraps.ready` push events.

## What's next

- [Core concepts](./concepts/channels.md) — the mental model.
- [Encryption & consent](./concepts/encryption.md) — the crypto in detail.
- [Two-device round trip tutorial](./tutorials/two-device-round-trip.md) — the runnable script this quickstart summarised.
- [SCWP reference](./reference/scwp.md) — every verb.