Sharing & discussion
A report can be exposed two ways. A public share mints a capability link that anyone with the URL (and optional passcode) can open — one immutable version of the report, served by the public viewer with no Spantail account. A discussion accompanies each sent content version: its participants (the report owner and that version’s Send-to recipients) react to the body and leave threaded comments.
Public shares
Section titled “Public shares”A share references one immutable content version of the report — the current version when the owner mints from the report screen, or the delivered version when a recipient mints from a received inbox message. Versions never change and are never individually deleted, so a later report edit never changes a published page. The plaintext token is the capability — treat the URL as a secret.
Every share is managed by whoever minted it: only the creator can list or revoke their links, and the owner’s and a recipient’s lists never mix.
The share object
Section titled “The share object”| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier. |
reportContentId |
string | The immutable report version this link serves. |
token |
string | The capability token (a 22-char base64url string). The public URL is /share/<token>. |
hasPasscode |
boolean | Whether a passcode is required to view. The passcode itself never leaves the server. |
expiresAt |
string | null | ISO 8601 expiry, or null for no expiry. |
revokedAt |
string | null | ISO 8601 revocation instant, or null if active. |
viewCount |
integer | Number of times the page has been viewed. |
lastViewedAt |
string | null | ISO 8601 instant of the last view, or null. |
createdAt |
string | ISO 8601 creation instant. |
List a report’s shares
Section titled “List a report’s shares”GET /api/v1/reports/:id/shares scope: read
Returns the shares the report’s owner minted (including plaintext tokens); links minted by
delivery recipients belong to their inbox view and never appear here. Follows the report’s read
access: the owner (re-checked against current workspace membership), an instance admin, or a
workspace admin/owner of a single-workspace report. Anyone else gets 404.
Create a share
Section titled “Create a share”POST /api/v1/reports/:id/shares scope: write
Owner only (re-checked against current workspace membership). Mints a link over the report’s current immutable version. Both fields are optional; an empty body mints a no-passcode, no-expiry link.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
passcode |
string | no | Passcode required to view (4–128 chars). Stored hashed (PBKDF2). |
expiresInDays |
integer | no | Days until the link expires (1–365). Omitted means no expiry. |
Returns 201 Created.
Share a received message
Section titled “Share a received message”POST /api/v1/inbox/:id/shares scope: write
GET /api/v1/inbox/:id/shares scope: read
A Send-to recipient can mint the same kind of public link over a message
in their inbox; the link serves exactly the delivered version. Recipient only — the sender
and everyone else get 404. The received copy is the recipient’s to re-share (they can already
download it), so minting re-checks no workspace membership; the sender’s recipient validation at
send time is the dissemination gate. POST takes the same body as Create a
share; GET lists the links the caller minted on this message’s version.
Revoke a share
Section titled “Revoke a share”POST /api/v1/report-shares/:id/revoke scope: write
Creator only — the report owner or the delivery recipient who minted the link. Revocation only
reduces exposure, so it skips the workspace membership re-check. Idempotent — re-revoking keeps
the first revokedAt. A revoked link immediately stops serving its content. Returns the updated
share object.
The public viewer
Section titled “The public viewer”The public viewer serves the shared version as HTML at /share/:token — no /api/v1 prefix, no
authentication. Malformed, unknown, revoked, and expired tokens all return the same HTML 404, so
a link’s past existence is never revealed. The page is served noindex, no-store, with a strict
Content-Security-Policy.
View a shared report
Section titled “View a shared report”GET /share/:token public
Returns the report page as HTML. If the share has a passcode, returns a passcode prompt page
instead; submit the passcode with the POST form below.
Submit a passcode
Section titled “Submit a passcode”POST /share/:token public
Accepts a form-encoded passcode field. A correct passcode returns the report page; an incorrect
one returns the passcode page again with 401. Shares without a passcode return the page directly.
Discussion
Section titled “Discussion”Discussions are keyed by content version (/report-contents/:id/…), not by report: each sent
version carries its own thread, so an edited report starts a fresh discussion and recipients only
ever discuss the exact body they received. Resolve the id from reportContentId — on the
report (its current version) or on an
inbox message (the delivered version).
A version’s discussion is restricted to participants — the report owner and that version’s Send-to recipients. An instance admin (or a workspace admin/owner of a single-workspace report) who is not a participant can still read the thread under the owning report’s read access, but writing stays participant-only. Comments and reactions are only writable once the version has been shared (it has at least one delivery); an unsent version has an empty, read-only thread.
The comment object
Section titled “The comment object”| Field | Type | Description |
|---|---|---|
id |
string | Unique identifier. |
reportContentId |
string | The content version this comment belongs to. |
authorUserId |
string | null | Author, or null once their account is deleted (the frozen authorName stands alone). |
authorName |
string | Author display name, frozen at write. |
authorImageUrl |
string | null | Author’s live avatar URL, or null. |
body |
string | Markdown comment body. |
createdAt |
string | ISO 8601 creation instant. |
updatedAt |
string | ISO 8601 last-update instant. |
editable |
boolean | true when the caller authored it (drives the edit/delete menu). |
reactions |
object[] | Aggregated reactions on this comment (see below). |
A reaction summary aggregates one emoji: { "emoji": "+1", "count": 2, "reactedByMe": true, "userNames": ["Ana", "Bo"] }. Reaction emoji are stable content keys, not glyphs: +1, -1,
laugh, hooray, confused, heart, rocket, eyes.
Get the discussion
Section titled “Get the discussion”GET /api/v1/report-contents/:id/discussion scope: read
Returns the version’s body-level reactions and its comments (each with their own reactions), plus
shared — false for a version that has never been sent (the UI hides the panel then).
Add a comment
Section titled “Add a comment”POST /api/v1/report-contents/:id/comments scope: write
Participant only; the version must have been shared. Returns the created comment with 201.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
body |
string | yes | Markdown comment (1–10000 chars). |
Edit a comment
Section titled “Edit a comment”PATCH /api/v1/report-contents/:id/comments/:commentId scope: write
Author only. Same body field as create. A missing comment, a wrong version, or a non-author all
return 404 — which is never distinguished.
Delete a comment
Section titled “Delete a comment”DELETE /api/v1/report-contents/:id/comments/:commentId scope: write
Author only. Returns 204 No Content.
React to the version’s body
Section titled “React to the version’s body”PUT /api/v1/report-contents/:id/reactions scope: write
Participant only; the version must have been shared. Toggles the caller’s reaction for the given emoji on the version’s body (a second call with the same emoji removes it). Returns the body’s updated reaction summaries.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
emoji |
string | yes | One of the reaction keys (+1, -1, laugh, hooray, confused, heart, rocket, eyes). |
React to a comment
Section titled “React to a comment”PUT /api/v1/report-contents/:id/comments/:commentId/reactions scope: write
Participant only. Toggles the caller’s reaction on a single comment (validated to belong to the version). Returns that comment’s updated reaction summaries.