Skip to content

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.

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.

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.

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.

[
  {
    "id": "shr_ab12cd",
    "reportContentId": "rct_q2v3",
    "token": "Hs9Kp2mNv8rT3wYbL6dQx7",
    "hasPasscode": true,
    "expiresAt": "2026-07-28T00:00:00.000Z",
    "revokedAt": null,
    "viewCount": 4,
    "lastViewedAt": "2026-06-28T11:00:00.000Z",
    "createdAt": "2026-06-28T09:45:00.000Z"
  }
]

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.
curl -X POST "https://<your-instance>/api/v1/reports/rep_q2/shares" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "passcode": "sunflower", "expiresInDays": 30 }'

Returns 201 Created.

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.

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.

curl -X POST "https://<your-instance>/api/v1/report-shares/shr_ab12cd/revoke" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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.

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.

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.

curl -X POST "https://<your-instance>/share/Hs9Kp2mNv8rT3wYbL6dQx7" \
  --data-urlencode "passcode=sunflower"

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.

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 /api/v1/report-contents/:id/discussion scope: read

Returns the version’s body-level reactions and its comments (each with their own reactions), plus sharedfalse for a version that has never been sent (the UI hides the panel then).

curl "https://<your-instance>/api/v1/report-contents/rct_q2v3/discussion" \
  -H "Authorization: Bearer spantail_pat_yourtoken"

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).
curl -X POST "https://<your-instance>/api/v1/report-contents/rct_q2v3/comments" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Looks great — shipping it." }'

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 /api/v1/report-contents/:id/comments/:commentId scope: write

Author only. Returns 204 No Content.

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).
curl -X PUT "https://<your-instance>/api/v1/report-contents/rct_q2v3/reactions" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "emoji": "rocket" }'

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.

curl -X PUT "https://<your-instance>/api/v1/report-contents/rct_q2v3/comments/cmt_7x/reactions" \
  -H "Authorization: Bearer spantail_pat_yourtoken" \
  -H "Content-Type: application/json" \
  -d '{ "emoji": "heart" }'