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
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
- Codes are 6 digits, single-use, 10-minute TTL, one active per account.
- New personalities appear everywhere. Create a character on any device and every linked device sees it automatically — characters are account-scoped, not device-scoped.
- Memory sharing is inherent. It comes from the
(external_id, character)model, not from this endpoint — linking just decides which account a device resolves to. - Recommended UX: keep the "active companion" at the account level, so switching personality on one device switches it everywhere.