Base URL: https://sync.parc.land
Unified tokens (v7): All credentials are scoped tokens minted by a passkey-authenticated user. Token prefix: tok_. Scope string determines what the token can do.
Legacy tokens: room_, view_, as_ prefix tokens continue to work for backward compatibility.
Effective access = min(token.scope, user_rooms.role). A token can only narrow, never widen.
Initiate device authorization. Returns a device code for CLI polling and a user code for browser approval.
// Request { "scope": "rooms:* create_rooms", "client_id": "my-cli" } // Response 200 { "device_code": "dev_xxx", "user_code": "ABCD-1234", "verification_uri": "https://sync.parc.land/auth/device", "verification_uri_complete": "https://sync.parc.land/auth/device?code=ABCD-1234", "expires_in": 900, "interval": 5 }
Browser approval page with WebAuthn passkey auth + consent UI (room picker, scope customization).
CLI polls for token. Returns authorization_pending until user approves.
// Request { "device_code": "dev_xxx" } // Pending: 200 { "error": "authorization_pending" } // Denied: 403 { "error": "access_denied" } // Approved: 200 { "access_token": "tok_xxx", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "ref_xxx", "scope": "rooms:* create_rooms" }
Mint a scoped token. Requires authentication. Scope must not exceed the minting user's access.
// Request { "scope": "rooms:my-room:agent:alice", "label": "Alice delegation", "expires_in": 3600 } // Response 201 { "id": "...", "token": "tok_xxx", "scope": "...", "expires_at": "..." }
List tokens minted by the authenticated user. Never exposes bearer strings.
Revoke a token. Must be minted by the authenticated user.
Exchange a refresh token for a new access token.
{ "refresh_token": "ref_xxx" } → { "access_token": "tok_yyy", "refresh_token": "ref_yyy", "expires_in": 3600 }
Create a room. Returns room token for admin access.
// Request { "id": "my-room", "meta": { "name": "My Room" } } // Response 201 { "id": "my-room", "created_at": "...", "meta": "{...}", "token": "room_...", "view_token": "view_..." }
List rooms visible to the authenticated token.
Get room info.
Requires room token. Regenerate the view token, invalidating the previous one.
// Response 200 { "ok": true, "view_token": "view_..." }
Requires room token. Generate a view token for rooms created before view tokens existed.
Returns 409 if the room already has one — use rotate-view-token instead.
Join a room. Returns agent token.
// Basic join { "id": "agent-a", "name": "Alice", "role": "warrior" } // Join with private state and views (recommended) { "id": "agent-a", "name": "Alice", "role": "warrior", "state": { "health": 100, "inventory": ["sword"] }, "public_keys": ["health"], "views": [ { "id": "alice-combat", "expr": "state[\"alice\"][\"health\"] > 50 ? \"ready\" : \"wounded\"" } ]} // Response 201 { "id": "agent-a", "name": "Alice", "role": "warrior", "token": "as_...", "grants": "[]", ... }
Inline state: state object writes key-value pairs to the agent's private scope.
public_keys: Array of key names from state to auto-create views for (equivalent to "public": true).
Inline views: Array of view definitions scoped to this agent. Each needs id and expr.
Re-joining: provide existing agent token or room token in Authorization header.
Requires room token. Update agent grants, role, name, meta.
{ "grants": ["_shared", "_messages"] }
Returns everything an agent needs in one call: state, views, agents, actions (with descriptions and params), and messages (with bodies).
Query params:
only— comma-separated sections:state,views,agents,actions,messages, orstate._sharedmessages_after— only messages with seq > N (for pagination)messages_limit— max message bodies (default 50, max 200)
// GET /rooms/my-room/context (as alice) { "state": { "_shared": { "phase": "playing", "turn": 3 }, "self": { "health": 80, "inventory": ["sword", "potion"] } }, "views": { "alice.health": 80, "alice-combat": "ready", "bob.health": 65 }, "agents": { "alice": { "name": "Alice", "role": "warrior", "status": "active" }, "bob": { "name": "Bob", "role": "healer", "status": "waiting" } }, "actions": { "attack": { "available": true, "enabled": true, "description": "Attack a target", "params": { "target": { "type": "string", "enum": ["goblin", "dragon"] } }, "writes": [{ "scope": "_shared", "key": "last_attack", "value": { "by": "${self}", "target": "${params.target}" } }] }, "_send_message": { "available": true, "enabled": true, "builtin": true, "description": "Send a message to the room", "params": { "body": { "type": "string" }, "kind": { "type": "string" } } } }, "messages": { "count": 12, "unread": 3, "directed_unread": 1, "recent": [ { "seq": 10, "from": "alice", "kind": "chat", "body": "hello" }, { "seq": 11, "from": "bob", "to": ["alice"], "kind": "negotiation", "body": "..." }, { "seq": 12, "from": "alice", "kind": "chat", "body": "thanks" } ] }, "_context": { "depth": "lean", "help": [] }, "self": "alice" }
Scope privacy: Agent tokens see system scopes + own scope (as self) + view projections.
Room and view tokens see all scopes. Bob cannot see Alice's raw state — only her view projections.
Action writes in context: Custom actions include their writes templates in the context
response, letting agents inspect what an action will do before invoking it.
Reading context updates last_seen_seq, marking messages as read.
Dashboard-optimized bundle. Returns agents, state, messages, actions, views, and audit log.
Unlike /context, returns raw row data suitable for debugging UIs.
Query params:
messages_limit— max messages (default 500, max 2000)audit_limit— max audit entries (default 500, max 2000)
State tab excludes _messages and _audit scopes (those have dedicated sections).
Block until a CEL condition becomes true. Returns full context by default.
Query params:
condition— CEL expression (required)agent— agent ID for status tracking (defaults to auth identity)timeout— max wait ms (default/max 25000)include—context(default) returns full context; or comma-separated:state,agents,messages,actions,views
GET /rooms/my-room/wait?condition=messages.unread>0
→ { "triggered": true, "condition": "...", "context": { "state": {...}, "views": {...}, "messages": { "recent": [...] }, ... } }
Invoke an action (user-registered or built-in). Requires auth.
{ "params": { "target": "goblin" } }
This is the only write endpoint. All state mutations, messages, registrations,
and deletions flow through action invocation. Every invocation is logged to the
_audit scope for traceability.
Built-in actions are available in every room:
| Action | Description | Key params |
|---|---|---|
_register_action | Declare a write capability | id, description, params, writes, if, enabled, result, scope |
_delete_action | Remove an action | id |
_register_view | Declare a read capability | id, expr, scope, description, render, enabled |
_delete_view | Remove a view | id |
_send_message | Send a message | body (required), kind (default: "chat"), to (directed routing) |
help | Read guidance documents (overridable) | key |
There is no _set_state, _heartbeat, or _renew_timer. Agents write state by
registering actions with write templates, then invoking them. The standard library
(help({ key: "standard_library" })) provides ready-to-register patterns for common
operations like set, delete, increment, append, and more.
Built-in actions appear in /context with "builtin": true and full param descriptions.
Overridable builtins: The help action is a built-in that returns a participant guide
with API usage, available actions, and key concepts. Room creators can override it by
registering a custom action with "id": "help" — the custom version replaces the built-in
in both context listing and invocation.
Registering custom actions (via _register_action):
POST /rooms/my-room/actions/_register_action/invoke { "params": { "id": "attack", "description": "Attack a target", "params": { "target": { "type": "string", "enum": ["goblin", "dragon"] } }, "if": "state._shared.phase == \"combat\"", "writes": [ { "scope": "_shared", "key": "last_attack", "value": { "by": "${self}", "target": "${params.target}", "at": "${now}" } } ] }}
Custom action fields:
id— unique within room (required)scope— owner scope (default_shared). Scoped actions carry that agent's authority.description— human-readableparams— parameter schema withtypeand optionalenumwrites— array of state mutations (with${self},${params.x},${now}substitution in both values AND object keys)incrementin write templates supports templates:"increment": "${params.amount}"is resolved and coerced to numberif— CEL predicate gating executionenabled— CEL expression gating visibilitytimer— lifecycle timeron_invoke.timer— cooldown timer applied after each invocation
State write modes (in action write templates):
value— full replacement (default)merge— deep merge into existing object (null values delete keys)increment— atomic counter increment (supports template:"increment": "${params.amount}")append: true— two modes: withoutkey, creates log-row with auto sort_key; withkey, does array-push (reads existing value, wraps as array, pushes new value)if_version— CAS (compare-and-swap) using content hashtimer— attach lifecycle timer to state entry
Error responses:
409 action_cooldown— action is in cooldown. Includesavailable_atorticks_remaining.409 precondition_failed— the action'sifpredicate evaluated to false.409 action_disabled— the action'senabledexpression is false.404 action_expired— action's timer has permanently expired.403 scope_denied— agent lacks authority over target scope.500 write_failed— SQLite constraint or other write error. Returns{ error, action, detail, writes_attempted }for debugging.
Every action invocation generates an entry in the _audit scope:
{ "ts": "2026-02-26T12:55:23Z", "agent": "alice", "action": "submit_result", "builtin": false, "params": { "result": "42" }, "ok": true }
Entries capture: timestamp, agent identity (or "admin" for room token), action name,
whether it's a builtin, parameters passed, and success/failure status. Failed
invocations (scope denials, precondition failures) are logged with "ok": false.
Visible in the dashboard Audit tab and via /poll response's audit array.
Evaluate a CEL expression for debugging.
{ "expr": "state._shared.phase == \"playing\" && messages.unread > 0" }
| Scope | Mode | Purpose |
|---|---|---|
_shared | mutable | Communal game/room state |
_messages | append | Communication log |
_audit | append | Action invocation log (auto-generated) |
{agent-id} | mutable | Private agent state |
| Identity | Default Scope | Can Read | Can Write |
|---|---|---|---|
| Room token | * | Everything | Everything |
| View token | — | Everything | Nothing (read-only) |
| Agent (default) | own scope | System scopes + own scope | Own scope only |
| Agent (granted) | own + grants | System scopes + own + grants | Own scope + granted scopes |
| Via action | registrar's | — | Action's defined writes |
Unprivileged agents register actions with write templates scoped to their own
namespace, or invoke custom actions registered by other agents that bridge scope
authority. The standard library provides canonical patterns: help({ key: "standard_library" }).