Help
Help
Application Settings & Devices
The Application Settings API is a generic per-user, per-app store for a client application's own settings, plus a device registry so those settings can follow the account across machines. It is the server side of the "Applications" feature: a native or web client (the first consumer is the Visual Introspection macOS app) reads and writes an opaque settings document, registers the machines it runs on, and resolves what a brand-new machine should start from on first run.
Every endpoint is authorized by Authorization: Bearer <sync-token> (or the web session cookie) and scoped to the authenticated user. No subscription is required. A token can only read and write its own user's documents; cross-user access is not possible.
Scopes: account vs device
Settings live in one of two scopes, both using the same document shape and the same optimistic-concurrency (compare-and-swap) rules:
- Account scope (shared): one document per app that follows the user across every machine. Path:
/api/user/app-settings/{appKey}. - Device scope (per-machine): one document per registered device, pinned to that machine. Path:
/api/user/app-settings/{appKey}/devices/{deviceId}/settings. A device must be registered before its device-scoped document can be written.
The document is opaque to the server: it is stored and returned verbatim, byte for byte. Keys are never dropped, renamed, or reordered, even if a key is named like a secret (token, apiKey, and so on). The server does not interpret the payload and never logs it.
The settings document
Every read (and every successful write) returns a settings document with this shape:
| Field | Type | Description |
|---|---|---|
appKey | string | The application namespace this document belongs to. |
scope | string | "account" for the shared document, "device" for a per-machine document. |
deviceId | string | null | The device this document is pinned to; null for account scope. |
version | integer | Monotonic version, starts at 1 on the first successful write and increments by 1 per write. Used for compare-and-swap. |
updatedAt | string (ISO 8601) | When the document was last written. |
schemaVersion | integer | Client-owned schema version, echoed back as sent. Defaults to 1 when omitted on write. |
settings | object | The opaque, client-owned JSON object. Round-trips byte for byte. |
Validation and limits
| Rule | Value | On violation |
|---|---|---|
appKey format | ^[a-z0-9][a-z0-9-]{0,63}$ (lowercase alphanumeric + hyphen, 1 to 64 chars, must start alphanumeric) | 400 |
deviceId format | ^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$ (alphanumeric plus . _ : -, 8 to 128 chars, must start alphanumeric) | 400 |
settings size cap | 64 KB (65536 bytes), measured as the UTF-8 byte length of the serialized settings object only, not the whole request envelope | 413 |
settings type | Must be a JSON object (not an array, string, or null) | 400 |
| Write rate limit | ~60 writes per minute per user per app (shared across all write methods on the same appKey) | 429 |
Reads are not rate-limited. The rate-limit bucket is keyed per user and per appKey, so writes to the account document, device documents, and the device registry for one app all draw from the same ~60/min budget.
Optimistic concurrency (compare-and-swap)
Writes to a settings document use body-baseVersion compare-and-swap so concurrent writers cannot silently overwrite each other:
- First-ever write: send
baseVersion: 0. The server creates the document atversion: 1. - Update: send
baseVersionequal to the document's currentversion. The server writes and incrementsversion. - Stale write: if
baseVersiondoes not match (including sending a non-zerobaseVersionwhen no document exists yet), the server returns409with the full current document undercurrent, so the client can rebase and retry.
Error responses
Errors return the standard InterlinedList shape: a human-readable error string plus a stable machine-readable code.
{ "error": "Not found", "code": "not_found" }
| Status | code | Condition |
|---|---|---|
| 400 | bad_request | Invalid appKey, invalid deviceId, unparseable JSON body, or a body that fails validation (bad baseVersion, settings not an object, and so on). |
| 401 | unauthorized | No valid sync token or session. |
| 404 | not_found | No document exists at that scope, the device is not registered (device settings PUT), the device does not exist (device PATCH/DELETE), or bootstrap resolved to none. |
| 409 | version_conflict | baseVersion did not match; the body includes current (the full current document, or null when a create raced). |
| 413 | payload_too_large | Serialized settings exceeds 64 KB. |
| 429 | rate_limited | Over ~60 writes/min. Includes RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and Retry-After headers. |
| 500 | internal_error | Unexpected server error. |
Endpoint summary
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/user/app-settings/{appKey} | Session or Bearer | Read the account (shared) settings document. |
| PUT | /api/user/app-settings/{appKey} | Session or Bearer | Create or update the account document (compare-and-swap). |
| DELETE | /api/user/app-settings/{appKey} | Session or Bearer | Delete the account document. |
| GET | /api/user/app-settings/{appKey}/devices/{deviceId}/settings | Session or Bearer | Read a device's settings document. |
| PUT | /api/user/app-settings/{appKey}/devices/{deviceId}/settings | Session or Bearer | Create or update a device's settings document (device must be registered). |
| GET | /api/user/app-settings/{appKey}/devices | Session or Bearer | List registered devices for this user and app. |
| POST | /api/user/app-settings/{appKey}/devices | Session or Bearer | Register or refresh a device. First device becomes the default. |
| PATCH | /api/user/app-settings/{appKey}/devices/{deviceId} | Session or Bearer | Rename a device and/or promote it to default. |
| DELETE | /api/user/app-settings/{appKey}/devices/{deviceId} | Session or Bearer | Deregister a device and delete its device settings. |
| GET | /api/user/app-settings/{appKey}/bootstrap?deviceId=... | Session or Bearer | Resolve what a fresh machine should seed from. |
Account-scoped settings
GET /api/user/app-settings/{appKey}
Auth required: yes Description: Read the account (shared) settings document.
Path parameters
| Param | Type | Description |
|---|---|---|
appKey | string | Application namespace (validated against the appKey format). |
Response 200 OK
{
"appKey": "visual-introspection",
"scope": "account",
"deviceId": null,
"version": 7,
"updatedAt": "2026-08-15T18:04:22.000Z",
"schemaVersion": 1,
"settings": { "theme": "dark", "recentProjects": ["/a", "/b"] }
}
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey. |
| 401 | Not authenticated. |
| 404 | Nothing synced yet for this app. |
PUT /api/user/app-settings/{appKey}
Auth required: yes
Description: Create or update the account (shared) settings document using body-baseVersion compare-and-swap.
Path parameters
| Param | Type | Description |
|---|---|---|
appKey | string | Application namespace. |
Request body
{
"baseVersion": 7,
"schemaVersion": 1,
"settings": { "theme": "dark", "recentProjects": ["/a", "/b"] }
}
| Field | Type | Required | Description |
|---|---|---|---|
baseVersion | integer | yes | The version the write is based on. 0 on the first-ever write. |
schemaVersion | integer | no | Client schema version, echoed back. Defaults to 1. Must be an integer >= 0. |
settings | object | yes | Opaque JSON object. Stored and returned verbatim. Must be <= 64 KB serialized. |
Response 200 OK: the newly stored document (with version incremented):
{
"appKey": "visual-introspection",
"scope": "account",
"deviceId": null,
"version": 8,
"updatedAt": "2026-08-15T18:10:00.000Z",
"schemaVersion": 1,
"settings": { "theme": "dark", "recentProjects": ["/a", "/b"] }
}
Response 409 Conflict (baseVersion did not match):
{
"error": "version_conflict",
"code": "version_conflict",
"current": {
"appKey": "visual-introspection",
"scope": "account",
"deviceId": null,
"version": 9,
"updatedAt": "2026-08-15T18:12:00.000Z",
"schemaVersion": 1,
"settings": { "theme": "light" }
}
}
current is the full current document. It is null in the rare case where a concurrent create won the race and the current row could not be re-read.
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey, unparseable JSON, or invalid body (baseVersion not an integer >= 0, settings not an object). |
| 401 | Not authenticated. |
| 409 | baseVersion did not match; body includes current. |
| 413 | Serialized settings exceeds 64 KB. |
| 429 | Over the write rate limit. |
DELETE /api/user/app-settings/{appKey}
Auth required: yes Description: Delete the account (shared) settings document. Idempotent.
Response 200 OK
{ "deleted": true }
deleted is true when a document was removed, false when there was nothing to delete.
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey. |
| 401 | Not authenticated. |
| 429 | Over the write rate limit. |
Device-scoped settings
GET /api/user/app-settings/{appKey}/devices/{deviceId}/settings
Auth required: yes Description: Read a device's settings document.
Path parameters
| Param | Type | Description |
|---|---|---|
appKey | string | Application namespace. |
deviceId | string | Client-stable device id (validated against the deviceId format). |
Response 200 OK
{
"appKey": "visual-introspection",
"scope": "device",
"deviceId": "macbook-pro-9F2C1A7B4D",
"version": 3,
"updatedAt": "2026-08-15T18:04:22.000Z",
"schemaVersion": 1,
"settings": { "windowLayout": "split" }
}
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey or deviceId. |
| 401 | Not authenticated. |
| 404 | No device-scoped document exists for this device. |
PUT /api/user/app-settings/{appKey}/devices/{deviceId}/settings
Auth required: yes
Description: Create or update a device's settings document using body-baseVersion compare-and-swap. The device must be registered first (see register a device); a write to an unregistered device returns 404.
Request body (identical to the account PUT):
{
"baseVersion": 3,
"schemaVersion": 1,
"settings": { "windowLayout": "split" }
}
Response 200 OK: the newly stored document with version incremented and scope: "device".
Response 409 Conflict: same shape as the account PUT, with current carrying the current device document.
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey/deviceId, unparseable JSON, or invalid body. |
| 401 | Not authenticated. |
| 404 | The device is not registered. |
| 409 | baseVersion did not match; body includes current. |
| 413 | Serialized settings exceeds 64 KB. |
| 429 | Over the write rate limit. |
Device registry
Devices are the machines an app runs on. Each is identified by a client-stable deviceId that the client generates once and keeps (for example in the Keychain). The first device registered for an app becomes the default ("main workstation") automatically; its settings seed a brand-new machine on first run (see bootstrap).
GET /api/user/app-settings/{appKey}/devices
Auth required: yes Description: List the registered devices for this user and app, newest-seen first.
Response 200 OK
{
"devices": [
{
"deviceId": "macbook-pro-9F2C1A7B4D",
"deviceName": "Studio iMac",
"platform": "macos",
"isDefault": true,
"lastSeenAt": "2026-08-15T18:04:22.000Z",
"appVersion": "1.2.0",
"osVersion": "15.5",
"hasDeviceSettings": true
}
]
}
| Field | Type | Description |
|---|---|---|
deviceId | string | Client-stable device id. |
deviceName | string | Human-friendly name shown in the UI. |
platform | string | One of macos, ios, android, windows, linux, web, other. |
isDefault | boolean | Whether this is the default ("main workstation"). |
lastSeenAt | string (ISO 8601) | When the device last registered or refreshed. |
appVersion | string | null | Client app version reported at registration. |
osVersion | string | null | OS version reported at registration. |
hasDeviceSettings | boolean | Whether this device has its own device-scoped settings document. |
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey. |
| 401 | Not authenticated. |
POST /api/user/app-settings/{appKey}/devices
Auth required: yes
Description: Register a new device or refresh an existing one (keyed on deviceId). Registering an existing deviceId refreshes its name, platform, versions, and last-seen time while keeping its default flag. The first device registered for the app becomes the default automatically.
Request body
{
"deviceId": "macbook-pro-9F2C1A7B4D",
"deviceName": "Studio iMac",
"platform": "macos",
"appVersion": "1.2.0",
"osVersion": "15.5",
"appDisplayName": "Visual Introspection"
}
| Field | Type | Required | Description |
|---|---|---|---|
deviceId | string | yes | Client-stable id (validated against the deviceId format). |
deviceName | string | yes | Display name, 1 to 120 characters (trimmed). |
platform | string | yes | One of macos, ios, android, windows, linux, web, other. |
appVersion | string | no | Client app version, up to 120 characters. |
osVersion | string | no | OS version, up to 120 characters. |
appDisplayName | string | no | Friendly app name, up to 120 characters. Used only to seed the shared app catalog entry the first time this appKey is seen; ignored afterward. |
Response 200 OK
{
"device": {
"deviceId": "macbook-pro-9F2C1A7B4D",
"deviceName": "Studio iMac",
"platform": "macos",
"isDefault": true,
"lastSeenAt": "2026-08-15T18:04:22.000Z",
"appVersion": "1.2.0",
"osVersion": "15.5"
}
}
The write response does not include hasDeviceSettings (only the list endpoint does).
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey, unparseable JSON, or invalid body (bad deviceId, missing/oversized deviceName, unknown platform). |
| 401 | Not authenticated. |
| 429 | Over the write rate limit. |
PATCH /api/user/app-settings/{appKey}/devices/{deviceId}
Auth required: yes
Description: Rename a device and/or promote it to the default ("main workstation"). At least one of deviceName or isDefault must be present.
Request body
{ "deviceName": "Studio iMac", "isDefault": true }
| Field | Type | Required | Description |
|---|---|---|---|
deviceName | string | no | New display name, 1 to 120 characters (trimmed). |
isDefault | boolean | no | true promotes this device to default and demotes the previous default atomically. |
Response 200 OK: the updated device (same shape as the POST response device object).
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey/deviceId, unparseable JSON, or a body with neither deviceName nor isDefault. |
| 401 | Not authenticated. |
| 404 | No such device. |
| 429 | Over the write rate limit. |
DELETE /api/user/app-settings/{appKey}/devices/{deviceId}
Auth required: yes Description: Deregister a device and delete its device-scoped settings document. If the removed device was the default, the most recently seen remaining device is promoted to default automatically.
Response 200 OK
{ "deleted": true, "promotedDeviceId": "mac-mini-3B7E2F9A1C" }
| Field | Type | Description |
|---|---|---|
deleted | boolean | Always true when the device existed and was removed. |
promotedDeviceId | string | null | The device promoted to default in its place, or null if the removed device was not the default or no devices remain. |
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey or deviceId. |
| 401 | Not authenticated. |
| 404 | No such device. |
| 429 | Over the write rate limit. |
First-run bootstrap
GET /api/user/app-settings/{appKey}/bootstrap?deviceId={deviceId}
Auth required: yes
Description: Resolve the settings a fresh machine should start from, with provenance. The client passes the new machine's deviceId and receives the resolved document plus a source describing where it came from, so it can seed itself and then PUT its own device document.
Query parameters
| Param | Type | Required | Description |
|---|---|---|---|
deviceId | string | yes | The new machine's client-stable id (validated against the deviceId format). |
Resolution runs in priority order and returns the first that exists:
self: this device already has its own device document.default-device: the default ("main workstation") device's document.account: the shared account document.none: nothing to seed from (returns404).
Response 200 OK: the resolved settings document with a source field merged in at the top level (the document fields are not nested under a doc key):
{
"source": "default-device",
"appKey": "visual-introspection",
"scope": "device",
"deviceId": "mac-mini-3B7E2F9A1C",
"version": 5,
"updatedAt": "2026-08-15T18:04:22.000Z",
"schemaVersion": 1,
"settings": { "theme": "dark" },
"defaultDeviceId": "mac-mini-3B7E2F9A1C",
"defaultDeviceName": "Mac mini"
}
For source: "default-device", the response also includes defaultDeviceId and defaultDeviceName (the source machine). For source: "self" and source: "account", only source plus the document fields are returned. Note that scope and deviceId reflect the source document (for default-device they belong to the default machine, not the requesting deviceId).
Response 404 Not Found (nothing to seed from):
{ "source": "none" }
Error responses
| Status | Condition |
|---|---|
| 400 | Invalid appKey or missing/invalid deviceId. |
| 401 | Not authenticated. |
| 404 | Nothing to seed from ({ "source": "none" }). |
Related
- Applications (Help Center): the user-facing guide to the Settings → Applications section.
- Authentication & OAuth: how to obtain and send the Bearer sync token.
- API overview: base URL, authentication at a glance, and response conventions.
- API explorer: try these endpoints live with the interactive Swagger console. </content>