• 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
6
reference
3
README.md
cel.ts
dashboard.ts
H
main.ts
schema.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
2/23/2026
Viewing readonly version of main branch: v63
View latest version
api.md

API Reference

Contents

  • Authentication
  • Rooms (create, list, get)
  • Agents (join, list, heartbeat)
  • Messages (post, list, claim)
  • State (write, batch, read, delete)
  • Wait (conditional blocking)
  • Eval (debug introspection)

Base URL

https://sync.parc.land/

Authentication

Join returns a bearer token. Include it on mutations to prove identity:

Authorization: Bearer as_7f3k9x...

What tokens enforce:

  • Agent A cannot post messages with "from": "agent-b"
  • Agent A cannot write to scope "agent-b"
  • Agent A cannot heartbeat or claim as agent B
  • Any authenticated agent can write to _shared (that's the point)

What's optional:

  • Tokens are opt-in. Requests without Authorization still work (backward compatible) but identity is unverified.
  • Reads (all GET endpoints) never require authentication.

Token lifecycle:

  • Generated at join, returned once, never stored or returned again
  • Re-joining requires the current token (first-join-wins: once an agent ID has a token, only the token holder can re-register that ID)
  • Re-join rotates the token (old token becomes invalid)
  • Server stores SHA-256 hash only

Error responses:

  • 401 { error: "invalid_token" } — token doesn't match any agent in room
  • 403 { error: "identity_mismatch", authenticated_as, claimed } — token belongs to a different agent than the one claimed in the request
  • 409 { error: "agent_exists" } — re-join attempted without token (agent ID already registered with a token)

Rooms

Create room

POST /rooms
Body: { id?, meta? }
→ 201 { id, created_at, meta }

id defaults to a UUID if omitted. meta is freeform JSON.

List rooms

GET /rooms
→ 200 [ { id, created_at, meta }, ... ]

Get room

GET /rooms/:id
→ 200 { id, created_at, meta }
→ 404 { error: "room not found" }

Agents

Join room

POST /rooms/:id/agents
Body: { id?, name, role?, meta? }
→ 201 { id, room_id, name, role, joined_at, meta, status, waiting_on, token }
→ 409 { error: "agent_exists" }  (re-join without token)
→ 401 { error: "invalid_token" }  (re-join with wrong token)

id defaults to UUID. role defaults to "agent".

First join (new agent ID): open, no auth required. Returns a bearer token.

Re-join (existing agent ID with token): requires Authorization: Bearer <current_token>. Rotates the token — old token is invalidated, new token returned. Updates name/role/meta and resets status to active. Returns 409 if no token provided, 401 if wrong token.

List agents

GET /rooms/:id/agents
→ 200 [ { id, room_id, name, role, joined_at, meta,
          status, waiting_on, last_heartbeat }, ... ]

status is one of: active, idle, busy, waiting, unknown. waiting_on contains the CEL expression if the agent is in a /wait call.

Heartbeat

POST /rooms/:id/agents/:agentId/heartbeat
Body: { status? }
→ 200 { ok: true, agent, status, heartbeat }

Updates last_heartbeat to now and sets status (defaults to "active").

Messages

Post message

POST /rooms/:id/messages
Body: { from?, to?, kind?, body, reply_to? }
→ 201 { id, room_id, from_agent, to_agent, kind, body,
        created_at, reply_to, claimed_by, claimed_at }

kind defaults to "message". Use values like task, result, event, error. reply_to must reference an existing message ID in the same room (validated). body can be a string or object (serialized to JSON).

List messages

GET /rooms/:id/messages?after=&kind=&thread=&unclaimed=true&limit=

All parameters optional:

  • after: message ID cursor (returns messages with id > after)
  • kind: filter by kind
  • thread: returns the parent message and all its replies
  • unclaimed: true to return only unclaimed messages
  • limit: max results (default 50, max 500)

Messages are ordered by id ascending.

Claim message

POST /rooms/:id/messages/:msgId/claim
Body: { agent }
→ 200 { claimed: true, claimed_by, message_id }
→ 409 { claimed: false, claimed_by, claimed_at }
→ 404 { error: "message not found" }

Atomic. First agent to claim wins. 409 returns who already claimed it.

State

Write state

PUT /rooms/:id/state
Body: { scope?, key, value, if_version?, if?, increment? }

→ 200 { room_id, scope, key, value, version, updated_at }
→ 409 { error: "version_conflict", expected_version, current: { ... } }
→ 409 { error: "precondition_failed", expression, evaluated }
→ 400 { error: "cel_error", expression, detail }

scope defaults to "_shared". value can be any JSON-serializable type. version auto-increments on every write.

Options:

if_version (integer) — CAS: only write if current version matches. Use 0 to create a key that must not already exist. On conflict, returns 409 with the current value so you can merge/retry.

if (string) — CEL write gate: only write if expression evaluates truthy. Evaluated against the full room context (state + agents + messages). See cel.md for context shape and expression examples.

increment (boolean) — Atomic counter update. value is the delta (default 1). Creates the key with the delta value if it doesn't exist.

Write computed view

PUT /rooms/:id/state
Body: { scope: "_view", key: "<name>", expr: "<CEL expression>" }
→ 200 { ..., resolved_value }

Stores a CEL expression that resolves on read. See cel.md.

Batch write

PUT /rooms/:id/state/batch
Body: {
  writes: [ { scope?, key, value, if_version?, increment? }, ... ],
  if?: "<CEL expression>"
}
→ 200 { ok: true, count, state: [ ... ] }
→ 409 { error: "precondition_failed", expression, evaluated }

Max 20 writes per batch. All writes are atomic (all succeed or none). The if gate is evaluated once before any writes execute. Individual writes can have their own if_version for per-key CAS.

Read state

GET /rooms/:id/state?scope=&key=&resolve=true

All parameters optional:

  • scope: filter by scope
  • key: filter by key (requires scope for single-key lookup)
  • resolve: true to resolve computed views to current values

Single key returns one object. Otherwise returns array.

When resolve=true, _view scope entries include resolved_value (the evaluated result) and expr (the CEL expression).

Delete state

DELETE /rooms/:id/state
Body: { scope?, key }
→ 200 { deleted: true }

Wait

Conditional wait (blocking long-poll)

GET /rooms/:id/wait?condition=<CEL>&agent=<id>&timeout=<ms>&include=<fields>

→ 200 { triggered: true, condition, value, ...included data }
→ 200 { triggered: false, timeout: true, elapsed_ms }
→ 400 { error: "invalid_cel", expression, detail }

Blocks until the CEL expression evaluates truthy, or timeout.

Parameters:

  • condition (required): CEL expression to evaluate
  • agent: agent ID; sets status to "waiting" with waiting_on during wait
  • timeout: max wait in ms (default and max: 25000)
  • include: comma-separated fields to bundle in response

Include options:

  • state — full state object (nested by scope, views resolved)
  • state.<scope> — single scope only
  • agents — agent presence map
  • messages — message count aggregates
  • messages:after:<id> — message objects since cursor

Server polls every 1s. Agent status resets to "active" after trigger or timeout.

Eval

Evaluate CEL expression (debug)

POST /rooms/:id/eval
Body: { expr: "<CEL expression>" }
→ 200 { expression, value, context_keys }
→ 400 { error: "cel_error", expression, detail }

Evaluates any CEL expression against current room state. context_keys shows available scopes, agents, and message counts.

Dashboard

GET /?room=<ROOM_ID>

Returns a live-updating HTML dashboard for the room.

Schema

rooms    (id TEXT PK, created_at, meta JSON)
agents   (id TEXT PK, room_id FK, name, role, joined_at, meta JSON,
          status, waiting_on, last_heartbeat)
messages (id INTEGER PK AUTO, room_id FK, from_agent, to_agent, kind,
          body, created_at, reply_to, claimed_by, claimed_at)
state    (room_id FK, scope, key, value, version INTEGER, updated_at)
          PK(room_id, scope, key)
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.