docs

Collaboration

One-to-one

A one-to-one (direct) space is shared by exactly two identities and derived deterministically from both account keys. Both peers compute the same space id, the same immutable ACL with both as writers, and the same read key — there is no invite token and no owner handshake.

Endpoints

Method Path Purpose
POST /v1/spaces/one-to-one open (or accept, or un-decline) a 1-1 by peer identity → 201 SpaceInfo
POST /v1/spaces/one-to-one/register-incoming seed a pending row from an out-of-band signal → 204
POST /v1/spaces/:spaceId/one-to-one/accept accept a pending row → 200 SpaceInfo
POST /v1/spaces/:spaceId/one-to-one/decline decline, synced and sticky → 204

The peer's identity is the id from their GET /v1/account, exchanged out of band — a link, a QR code, a shared space's members list.

Reach out

curl -s -X POST http://127.0.0.1:7001/v1/spaces/one-to-one \
  -d '{"otherIdentity": "A5k…"}'
# → 201 SpaceInfo  (status "active", spaceType "any.onetoone")
any one-to-one start <otherIdentity>      # aliases: any 1-1, any direct

The space is derived and activated immediately — implicit self-approval. The call is idempotent, and it overrides an earlier decline of the same peer. Classify direct chats in any list on spaceType: "any.onetoone".

Status Code When
400 request.missing_field otherIdentity empty
400 request.invalid_field undecodable identity, or your own (details.reason: "self")

The state machine

Because the ACL is immutable there is nothing to accept cryptographically. "Approve incoming" is a local gate on whether this device materializes and syncs the derived space, surfaced as space statuses:

            peer reaches out
                  │
   inbox notifier / register-incoming
                  ▼
      one_to_one_pending  (device-local row, nothing downloaded)
        │                 │
     accept            decline
        ▼                 ▼
      active      one_to_one_declined  (synced, sticky)
                          │
                  POST /v1/spaces/one-to-one {otherIdentity}
                          ▼
                        active
Status Meaning
one_to_one_pending someone reached out; row is device-local, no storage materialized
active materialized and syncing
one_to_one_declined suppressed on every device of the account; never auto-resurfaces

Incoming. The other side learns about a request in one of two ways: automatically, through the coordinator inbox notifier the server starts when a coordinator is configured; or out of band, by the app calling register-incoming with the peer's identity and an optional display hint. Either way a pending row appears.

curl -s -X POST http://127.0.0.1:7001/v1/spaces/one-to-one/register-incoming \
  -d '{"peerIdentity": "A5k…", "displayHint": {"name": "Alice", "iconCid": "…"}}'
# → 204, idempotent — no-op when a row already exists
any one-to-one register <peerIdentity> [--name ...] [--description ...] [--icon-cid CID]

Accept / decline by the pending row's space id — the server reads the peer identity off the row, so you never re-derive it:

curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/one-to-one/accept    # → 200 SpaceInfo
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/one-to-one/decline   # → 204
any one-to-one accept  <spaceId>
any one-to-one decline <spaceId>

Accept is equivalent to re-running the open call with the peer's identity and is idempotent. Decline writes a synced sticky marker; only an explicit later POST /v1/spaces/one-to-one with that identity un-declines.

Why it matters. Nothing is downloaded before you accept. A pending row is just an id and a display hint; reading it never pulls the space ciphertext, so an unsolicited request costs you no storage and leaks nothing about whether you looked.

Discovery

There is no bespoke inbox endpoint. Pending requests are rows in the space list, hidden from the active-only default like deleted spaces:

curl -s "http://127.0.0.1:7001/v1/spaces?status=one_to_one_pending"        # snapshot
curl -s -N -X POST http://127.0.0.1:7001/v1/spaces/query/subscribe \
  -d '{"dataset": "spaces"}'                                                # live — filter rows on status
any one-to-one pending

The pending row carries the peer's display hint (name / iconCid) so a UI can render "Alice wants to chat" without syncing anything. GET /v1/spaces/:id on a pending row serves the row info only — it never materializes a non-active space.

After activation

Everything else is a regular space: both members are writers, so create objects, send chat and subscribe as usual, and read the two participants through the normal members collection. Two caveats:

  • Both participants report ownRole: "writer", never owner — the ACL owner slot is a synthetic key nobody holds. Do not gate anything on owner for any.onetoone rows.
  • A 1-1 carries no chat object of its own. Register one as a bundle with a derived root — the only install shape that cannot fork when the two sides set up while apart.

Note. Deletion is local-only. DELETE /v1/spaces/:spaceId offloads the space on this device and propagates the offload to the account's other devices, but a derived space is not node-owned and is never removed from the network; a later POST /v1/spaces/one-to-one re-derives and re-materializes it.