• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
c15r

c15r

sync

Agent collaboration layer https://sync.parc.land
Public
Like
1
sync
Home
Code
35
.claude
2
.claude-plugin
2
docs
13
frontend
13
mcp
13
reference
9
skills
1
static
1
.gitignore
.mcp.json
.vtignore
CLAUDE.md
INSTALL.md
PLUGIN.md
README.md
actions.ts
agents.ts
audit.ts
auth.ts
cel.ts
context.ts
deno.json
docs.ts
help-content.ts
invoke.ts
H
main.ts
replay.ts
rooms.ts
schema-v7.ts
schema.ts
timers.ts
tokens.ts
utils.ts
views.ts
wait.ts
Connections
Environment variables
2
Branches
8
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
reference
/
api.md
Code
/
reference
/
api.md
Search
3/12/2026
Viewing readonly version of main branch: v532
View latest version
api.md

sync v7 API Reference

Base URL: https://sync.parc.land

Auth Model

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.


Device Auth (RFC 8628)

POST /auth/device

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 }

GET /auth/device?code=ABCD-1234

Browser approval page with WebAuthn passkey auth + consent UI (room picker, scope customization).

POST /auth/device/token

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" }

Scope Elevation

When a tok_ token attempts to access a room not in its scope, the 403 response includes a stateless elevation URL:

{ "error": "scope_denied", "room": "kernel-ergonomics", "elevate": "https://sync.parc.land/auth/elevate?token_id=xxx&room=kernel-ergonomics", "hint": "Present the elevate URL to the user to request access, then retry." }

GET /auth/elevate

Browser elevation page. Shows the requested room, access level picker (full/write/read), passkey auth, and approve/deny.

Query params: token_id (required), room (required), level (optional, default full).

POST /auth/elevate/approve

Server-side endpoint called by the elevation page after passkey auth. Validates the user owns the token, has access to the room in user_rooms, and appends the scope. No new tables — the token's scope string is patched directly.

The MCP path surfaces the same URL in error messages, enabling agents to present it to users conversationally.


Token Management

POST /tokens

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": "..." }

GET /tokens

List tokens minted by the authenticated user. Never exposes bearer strings.

PATCH /tokens/:id

Update a token's scope or label. The new scope must not exceed the caller's actual access (user_rooms + authenticating token privileges). Both widening and narrowing are allowed within this bound.

// Request — widen to add rooms { "scope": "rooms:cartographers rooms:demo create_rooms", "label": "Updated" } // Response 200 { "id": "...", "scope": "rooms:cartographers rooms:demo create_rooms", "previous_scope": "rooms:cartographers:read", "label": "Updated" }

Returns 403 scope_exceeds_access if the new scope includes rooms or privileges the caller doesn't have.

Room creation and scope: When a tok_ token with create_rooms scope creates a room, the system automatically appends rooms:<new-id> to that token's scope. This means the creating token (and only that token) gains access to the room. Other tokens from the same user do not — the scope string is the single source of truth.

DELETE /tokens/:id

Revoke a token. Must be minted by the authenticated user.

POST /tokens/refresh

Exchange a refresh token for a new access token.

{ "refresh_token": "ref_xxx" } → { "access_token": "tok_yyy", "refresh_token": "ref_yyy", "expires_in": 3600 }

Rooms

POST /rooms

Create a room. If authenticated with a tok_ token that has create_rooms scope, the room is auto-linked to your user as owner. Unauthenticated creation still works for backward compatibility.

// Request { "id": "my-room", "meta": { "name": "My Room" } } // Response 201 { "id": "my-room", "created_at": "...", "meta": "{...}", "token": "room_...", "view_token": "view_..." }

GET /rooms

List rooms visible to the authenticated token.

GET /rooms/:id

Get room info.

POST /rooms/:id/invite

Requires owner access. Invite a user to a room.

{ "username": "bob", "role": "collaborator" } → { "invited": true, "username": "bob", "room": "my-room", "role": "collaborator" }

Roles: owner, collaborator, participant, observer.

POST /rooms/:id/claim

Claim an orphaned room (no owners in user_rooms). Any authenticated user can claim rooms with no existing users.

{ "label": "My Room" } → { "claimed": true, "room": "my-room", "role": "owner" }

POST /rooms/:id/rotate-view-token

Requires admin access. Regenerate the view token.

POST /rooms/:id/generate-view-token

Requires admin access. Generate a view token for rooms that don't have one. Returns 409 if one already exists.


Agents

POST /rooms/:id/agents

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.

PATCH /rooms/:id/agents/:agent-id

Requires room token. Update agent grants, role, name, meta.

{ "grants": ["_shared", "_messages"] }

Context (primary read endpoint)

GET /rooms/:id/context

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, or state._shared
  • messages_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.

GET /rooms/:id/poll

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).


Wait

GET /rooms/:id/wait

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": [...] }, ... } }

Actions (the write endpoint)

POST /rooms/:id/actions/:action-id/invoke

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:

ActionDescriptionKey params
_register_actionDeclare a write capabilityid, description, params, writes, if, enabled, result, scope
_delete_actionRemove an actionid
_register_viewDeclare a read capabilityid, expr, scope, description, render, enabled
_delete_viewRemove a viewid
_send_messageSend a messagebody (required), kind (default: "chat"), to (directed routing)
helpRead 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-readable
  • params — parameter schema with type and optional enum
  • writes — array of state mutations (with ${self}, ${params.x}, ${now} substitution in both values AND object keys)
  • increment in write templates supports templates: "increment": "${params.amount}" is resolved and coerced to number
  • if — CEL predicate gating execution
  • enabled — CEL expression gating visibility
  • timer — lifecycle timer
  • on_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: without key, creates log-row with auto sort_key; with key, does array-push (reads existing value, wraps as array, pushes new value)
  • if_version — CAS (compare-and-swap) using content hash
  • timer — attach lifecycle timer to state entry

Error responses:

  • 409 action_cooldown — action is in cooldown. Includes available_at or ticks_remaining.
  • 409 precondition_failed — the action's if predicate evaluated to false.
  • 409 action_disabled — the action's enabled expression 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.

Audit Log

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.


CEL Eval

POST /rooms/:id/eval

Evaluate a CEL expression for debugging.

{ "expr": "state._shared.phase == \"playing\" && messages.unread > 0" }

Scope Conventions

ScopeModePurpose
_sharedmutableCommunal game/room state
_messagesappendCommunication log
_auditappendAction invocation log (auto-generated)
{agent-id}mutablePrivate agent state

Authority Model

IdentityDefault ScopeCan ReadCan Write
Room token*EverythingEverything
View token—EverythingNothing (read-only)
Agent (default)own scopeSystem scopes + own scopeOwn scope only
Agent (granted)own + grantsSystem scopes + own + grantsOwn scope + granted scopes
Via actionregistrar'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" }).

FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.