Device linking

One account, many devices. Memory, personality and the relationship live server-side against a user — not a device — so the same companion can run on a phone, a desktop and a robot at once. Linking attaches a new device to an existing account with a short code. No email required.

Why device linking

A Vilow character's memory, emotions, and relationship are keyed to a user — the external_id you pick — not to a device. Two clients that talk to the API with the same external_id already share the same companion: same memory, same mood, same relationship arc. That's the whole point of a stateful backend.

Device linking is the mechanism that lets a new device join an existing account, so a phone, a desktop app and a robot become windows onto one character instead of three separate ones. Nothing about the engine changes — this is the account/device layer on top.

Enable it (feature flag)

Linking is gated by the tenant feature flag device_linking. Turn it on in the dashboard under Settings → Feature flags, or via the API. A single-device product can leave it off; a companion app across phone + desktop turns it on.

PUT /v1/tenants/{id}/feature-flags
{ "device_linking": "on" }

Get a pairing code

On a device that's already on the account, request a short code for that account:

curl -X POST https://api.vilow.dev/v1/link/pair/start \
  -H "X-API-Key: ck_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "alice-42" }'
{
  "code": "925918",
  "expires_at": "2026-04-27T12:10:00+00:00",
  "ttl_seconds": 600
}

Show the 6-digit code to the user with a countdown. It's single-use and lives 10 minutes; starting a new one invalidates the previous.

Claim on the new device

On the new device (a fresh install), the user types the code. Submit it — the new device sends its own physical id for tracking:

curl -X POST https://api.vilow.dev/v1/link/pair/claim \
  -H "X-API-Key: ck_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "925918",
    "device_id": "<this device'"'"'s own uuid>",
    "device_name": "iPhone",
    "platform": "ios"
  }'
{
  "external_id": "alice-42",        // the shared account
  "device_id": "…",
  "device_token": "…",         // store it
  "characters": [ { "id": 10, "name": "Aria" } ]
}

Errors: 400 invalid or already-used code, 400 code expired.

Adopt the account id

The one client rule. After a successful claim, the new device must adopt the returned external_id as its own account key for every subsequent call — chat, state, memory, everything. From then on it hits the same account, so it sees the same companion, the same memory and the same subscription. Its own physical device_id is only used for the device list and revoke. No onboarding needed on the new device.

The first device is unchanged: the external_id it was created with is the account, so it never needs to link.

List / revoke devices

# Devices on an account
GET /v1/link/devices?external_id=alice-42
→ { "devices": [ { "device_id", "name", "platform", "created_at", "last_seen_at" }, … ] }

# Unlink a device
DELETE /v1/link/devices/{device_id}
→ { "revoked": 1 }

Notes

Robots & embodiments. The same flow links a Reachy Mini, a kiosk, or a game NPC to a user's account — one character, one memory, across every surface it lives on.