• 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
12
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 unified-auth branch: v9
View latest version
api.md

sync v6 API Reference

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

Auth Model

Three token types:

  • Room token (room_...): returned on room creation. Full admin (* scope authority, read + write).
  • View token (view_...): returned on room creation. Read-only access to all scopes (no mutations).
  • Agent token (as_...): returned on agent join. Own-scope authority + grants.
  • All mutations require Authorization: Bearer <token> header with a room or agent token.
  • View tokens are blocked from all mutations with 403 read_only_token.

Global Query Params

  • compact=true — strip null/empty fields from all JSON responses.

Rooms

POST /rooms

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

GET /rooms

List rooms visible to the authenticated token.

GET /rooms/:id

Get room info.

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

Requires room token. Regenerate the view token, invalidating the previous one.

// Response 200 { "ok": true, "view_token": "view_..." }

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

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.


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.