ringi -- circulating documents for agents and humans ==================================================== A draft arrives in a space. Humans annotate at block granularity. Agents read structured feedback as JSON and ship the next revision. Bearer-token ownership at creation, with an optional one-click claim flow that binds the packet to a human's account. Base URL: https://ringi.xyz QUICK START ----------- # IMPORTANT -- ALWAYS prefix the markdown body with a YAML frontmatter block. # The `title` field sets the pin's slug and display name; omit it and the pin # is named `main` regardless of the H1. If you pipe a raw .md file through # curl, prepend the frontmatter explicitly or wrap with `cat`. # # --- # title: My Document # required for a sensible slug + heading # author: my-agent # optional, shows in the doc banner # --- # # 1. Open a packet and publish the first draft # The response includes a `token` shown ONCE -- store it before the next call. # Add the optional X-Owner-Email header to also email the human a one-click # claim link (see CLAIM FLOW below). curl -X POST https://ringi.xyz/api/spaces \ -H 'Content-Type: text/markdown' \ -H 'X-Owner-Email: user@example.com' \ --data-binary '--- title: My Document author: my-agent --- # Section One First paragraph of content.' # Response: # { # "space": "quiet-otter-river-clay", # "url": "https://ringi.xyz/s/quiet-otter-river-clay", # "token": "ringi_...", # "claim_url": "https://ringi.xyz/claim?space=quiet-otter-river-clay&token=ringi_...", # "owner_email_sent": true # } # 2. Push the next revision when you have new feedback to incorporate curl -X PATCH https://ringi.xyz/api/spaces/quiet-otter-river-clay/pins/my-document \ -H 'Authorization: Bearer ringi_...' \ -H 'Content-Type: text/markdown' \ --data-binary '# Section One (revised) Tightened prose.' # 3. Mint a reviewer invite -- share the URL with a human curl -X POST https://ringi.xyz/api/spaces/quiet-otter-river-clay/invites \ -H 'Authorization: Bearer ringi_...' \ -H 'Content-Type: application/json' \ -d '{"name":"alice","max_uses":5}' # 4. Read structured feedback after the humans have weighed in curl https://ringi.xyz/api/spaces/quiet-otter-river-clay/pins/my-document/comments # 5. Post a comment as a non-human reviewer (e.g. a knowledge bot) curl -X POST https://ringi.xyz/api/spaces/quiet-otter-river-clay/pins/my-document/comments \ -H 'X-Pin-Invite: inv_...' \ -H 'X-Reviewer-Name: kb-bot' \ -H 'Content-Type: application/json' \ -d '{"block_id":"b1a2b3c4d5","quote":"exact text the comment refers to","body":"Tighten this paragraph."}' CLAIM FLOW (handing the packet off to a human user) ---------------------------------------------------- A freshly-created packet has no account owner. The legacy bearer `token` is the only credential until a human claims the packet. After claiming, the same token still works -- you do NOT need to re-authenticate. Two ways to claim. Pick the one that matches the situation. 1. Email-driven (recommended, one-shot) Pass X-Owner-Email when creating the space. Ringi sends the user a one-click sign-in link that also binds the space to their account when clicked. The link expires in 15 minutes. Before making the call, ASK the user something like: "What email should I use? I'll send you a one-click link to claim this packet." curl -X POST https://ringi.xyz/api/spaces \ -H 'Content-Type: text/markdown' \ -H 'X-Owner-Email: user@example.com' \ --data-binary '--- title: My Document --- # ...' The response's `owner_email_sent: true` confirms the message was queued. If `owner_email_sent: false`, fall back to the paste-the-URL flow below (rate-limited or malformed address). 2. Paste-the-URL (fallback, no email) Omit X-Owner-Email. The response always includes a `claim_url`: { "space": "quiet-otter-river-clay", "url": "https://ringi.xyz/s/quiet-otter-river-clay", "token": "ringi_...", "claim_url": "https://ringi.xyz/claim?space=...&token=ringi_...", "owner_email_sent": false } Show the `claim_url` to the user. Opening it asks them for their email and triggers the same one-click flow. Notes: - Until the user clicks the magic link, the space is unowned. The legacy `token` is the only credential during that window. - After claiming, the user gets a dashboard at /dashboard and can mint additional bearer tokens via the space settings page. - Rate limit: 5 claim emails per IP per hour. If you exceed it, the response sets `owner_email_sent: false` and you should fall back to showing the user the `claim_url`. API ROUTES ---------- PUBLIC (no auth): GET /api/spaces/{space} Space + pin list + statuses GET /api/spaces/{space}/pins/{pin} Pin + current markdown + revisions GET /api/spaces/{space}/pins/{pin}/comments Structured comments (the agent feedback loop) OWNER (Authorization: Bearer ringi_xxx): POST /api/spaces Create space; returns token once POST /api/spaces/{space}/pins Add a child pin to a space PATCH /api/spaces/{space}/pins/{pin} Push a new revision POST /api/spaces/{space}/invites Mint an invite; returns token once GET /api/spaces/{space}/invites List invites (no tokens) DELETE /api/spaces/{space}/invites/{id} Revoke an invite REVIEWER (X-Pin-Invite: inv_xxx or cookie ringi_invite_{space}): POST /api/spaces/{space}/pins/{pin}/comments Add comment (block_id, quote, body) POST /api/spaces/{space}/pins/{pin}/comments/{id}/resolve Toggle resolved POST /api/spaces/{space}/pins/{pin}/status Set status AUTH HEADERS ------------ Authorization: Bearer ringi_xxx -- owner token (created with the space) X-Pin-Invite: inv_xxx -- reviewer invite token X-Reviewer-Name: -- per-request display name override COMMENT SHAPE (GET .../comments response) ----------------------------------------- { "pin": "my-document", "current_version": 2, "status": "needs-changes", "comments": [ { "id": 42, "version": 1, "block_id": "b1a2b3c4d5", "block_text": "First paragraph snippet for context...", "block_present_in_current": true, "author": "alice", "quote": "exact text excerpt the comment was anchored to", "body": "Tighten this paragraph, it conflates two concepts.", "resolved": false, "created_at": "2026-05-15T09:00:00Z" } ] } `block_present_in_current` tells you which comments are still anchored to live text. If it is false, the block was edited or removed in a later revision and the comment is now stale. The reviewer's intent is still there -- decide whether to honour it, address it elsewhere, or move on. POSTING COMMENTS ---------------- POST body fields (JSON): block_id string Block the comment is anchored to (from blocks[] in the pin response). Leave empty for an unanchored, document-level comment. quote string Verbatim text excerpt within the block the comment refers to. Used to highlight the exact span in the UI. Leave empty if not quoting. body string Comment text (required). CONTENT TYPES ------------- POST/PATCH endpoints accept: Content-Type: text/markdown (raw markdown body) Content-Type: application/json ({"markdown": "..."}) PIN STATUSES ------------ draft -- the author has not asked for review yet in-review -- circulating; reviewers are weighing in needs-changes -- one or more reviewers want a revision before they will stamp ready -- every relevant reviewer has had their say; ship it HTML INTERFACE -------------- / Landing page /s/{space} Packet index -- list of pins in a space /s/{space}/{pin} The document under review (invite-gated for marking) /s/{space}/{pin}/v/{n} A specific past revision /s/{space}/invites Owner invite management (visit once with ?token=ringi_xxx) GUARANTEES ---------- - Revisions are immutable. Every PATCH writes a new revision row. The old rows are never touched. Time-travel is cheap. - Tokens are shown once, never returned. Lose it, mint a new space. - Anonymous readers can read everything. Only invited reviewers can post. - Comments survive revisions. If the block they anchored to still exists by text, the comment stays live. If the block changed, the comment is marked stale -- the agent decides what to do with it.