Help
Help
Sharing & Share Links
Documents and lists can be shared three ways:
- By person: grant a named, existing user access at a role, via the collaborators/watchers endpoints (see Documents and Lists).
- By link: mint a secret, tokenized share link that grants a role to anyone who opens it. This page covers share links.
- By email invite: invite a specific email address (a person who may not have an account yet) to a private resource. Unlike a share link, an invite is bound to that email: it only becomes access once a signed-in user with that verified email claims it. See Email invites below.
Both resources use one identical sharing model, so the roles, gating, and semantics below apply equally to documents and lists.
Roles
Every grant, whether by person or by link, carries one of three roles:
| Role | UI label | Can do |
|---|---|---|
watcher | Viewer | Read-only view of the resource. |
collaborator | Editor | View and edit content (list rows / document body). |
manager | Admin | Everything Editor can, plus edit the resource's settings/schema and delete it. |
The true owner always outranks every grant and is the only party who can create, list, or revoke share links and manage per-person access; even a manager cannot.
Who can do what
| Action | Auth | Gating |
|---|---|---|
| Create a share link | Session or Bearer | Owner only, and subscriber only: a free owner gets 403. |
| List a resource's share links | Session or Bearer | Owner only (404 otherwise). |
| Revoke a share link | Session or Bearer | Owner only (404 otherwise). Not subscriber-gated: a downgraded owner can always revoke access they previously granted. |
| Open (resolve) a share link | Optional (anonymous allowed) | Anyone with the token. Viewer links grant anonymous read; Editor/Admin links return read-only data plus a prompt to sign in and claim. |
| Claim a share link | Session only | Any signed-in user. Upserts a real role grant for Editor/Admin links. |
Recipients never need a subscription to view, edit, or claim; only the granting owner must subscribe to create a link.
The token is a secret
A share link's token is a 256-bit random capability (base64url-encoded). Possession of the token is the grant, so treat it like a password. Anyone who has the token has whatever access the link's role confers, so:
- Only share the link over trusted channels; never post it publicly.
- To cut off access, revoke the link (below). Revocation is immediate.
- Optionally set an expiry when creating the link so it stops working automatically.
Endpoint table
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/documents/:id/share-links | Session or Bearer | List a document's active share links. Owner only. |
| POST | /api/documents/:id/share-links | Session or Bearer | Create a document share link. Owner only. Subscriber only. |
| DELETE | /api/documents/:id/share-links/:token | Session or Bearer | Revoke a document share link. Owner only. |
| GET | /api/documents/shared/:token | Optional | Resolve a document share link for viewing. |
| POST | /api/documents/shared/:token | Session only | Claim a document Editor/Admin link as the signed-in user. |
| GET | /api/lists/:id/share-links | Session or Bearer | List a list's active share links. Owner only. |
| POST | /api/lists/:id/share-links | Session or Bearer | Create a list share link. Owner only. Subscriber only. |
| DELETE | /api/lists/:id/share-links/:token | Session or Bearer | Revoke a list share link. Owner only. |
| GET | /api/lists/shared/:token | Optional | Resolve a list share link for viewing. |
| GET | /api/lists/shared/:token/data | Optional (token) | Read-only row data for a shared list. |
| POST | /api/lists/shared/:token | Session only | Claim a list Editor/Admin link as the signed-in user. |
The document and list forms are identical apart from the resource path and the response's resource object (document vs list, documentId vs listId). The examples below use lists; swap lists/listId/list for documents/documentId/document for the document equivalents.
Creating a share link
POST /api/lists/lst_abc001/share-links
Content-Type: application/json
{ "role": "watcher", "expiresAt": null }
| Field | Type | Description |
|---|---|---|
role | string | Optional. One of watcher, collaborator, manager. Defaults to watcher. An invalid value returns 400. |
expiresAt | string | null | Optional ISO 8601 datetime after which the link stops resolving. Omit or null for a link that never expires. An unparseable value returns 400. |
Response (201):
{
"token": "xN3v…base64url…9Qk",
"url": "https://interlinedlist.com/lists/shared/xN3v…9Qk",
"role": "watcher",
"expiresAt": null
}
url is the ready-to-share landing address. A free (non-subscriber) owner receives 403 { "error": "Subscribe to share lists." } (or "Subscribe to share documents."). A caller who does not own the resource receives 404 (existence is never leaked).
Listing active links
GET /api/lists/lst_abc001/share-links
{
"shareLinks": [
{
"token": "xN3v…9Qk",
"role": "watcher",
"expiresAt": null,
"createdAt": "2025-06-11T09:00:00.000Z",
"revokedAt": null,
"url": "https://interlinedlist.com/lists/shared/xN3v…9Qk"
}
]
}
Only active (non-revoked) links are returned, newest first. Owner only: a non-owner gets 404.
Revoking a link
DELETE /api/lists/lst_abc001/share-links/xN3v…9Qk
Returns { "revoked": true } on success, or 404 if the link does not exist under that resource or you are not the owner. Revocation takes effect immediately: the token stops resolving on its next use. This endpoint is intentionally not subscriber-gated so an owner whose subscription lapsed can still shut off previously created links.
Opening (resolving) a link
GET /api/{lists|documents}/shared/:token is the only cross-user / anonymous read path. Authentication is optional:
GET /api/lists/shared/xN3v…9Qk
{
"role": "watcher",
"canClaim": false,
"needsAuth": false,
"list": {
"id": "lst_abc001",
"title": "Books to Read",
"description": "My reading backlog.",
"isPublic": false,
"updatedAt": "2025-06-11T09:00:00.000Z"
}
}
- Viewer (
watcher) links resolve for anyone, signed in or not, and grant read-only access to the resource object. - Editor / Admin (
collaborator/manager) links also return the resource read-only, plus:canClaim: truewhen the viewer is signed in: prompt them toPOSTand claim the edit grant.needsAuth: truewhen the viewer is anonymous: prompt them to sign in first.
There are no anonymous writes: an Editor/Admin link must be claimed by a signed-in user before that user can edit, so every change stays attributable to a real account.
A 404 ("Share link not found, expired, or revoked") is returned for any unknown, revoked, or expired token, or when the underlying resource has been deleted; the four cases are deliberately indistinguishable. This endpoint is rate-limited per IP to blunt token-guessing.
Reading a shared list's rows
A shared list also exposes its row data through a dedicated read-only endpoint. This is what lets a Viewer link render a private list's rows on the share landing page without any account:
GET /api/lists/shared/xN3v…9Qk/data?limit=100&offset=0
- The token is the capability: rows are served regardless of the list's
isPublicflag and with no session. - Read-only. This handler never mutates; there is no document equivalent (a document's body is returned directly by the resolve call above).
- The token is re-checked on every request, so a revoked or expired link stops serving rows immediately.
- Supports the same pagination/filter/sort query params as the authenticated
GET /api/lists/:id/dataendpoint (limit,offset,page,sort,order, plus per-column filters) and returns the same row payload shape. - Per-IP rate-limited, matching the resolve route. A
404is returned for any unknown/revoked/expired token or a deleted list.
Claiming a link
POST /api/lists/shared/xN3v…9Qk
Cookie: session=<session id list>
Claiming is authenticated by the session cookie only (not a Bearer sync token): a browser visitor clicks "Start editing" and the request carries their session. For an Editor or Admin link, this upserts a real access grant (a list watcher / document collaborator row) at the link's role for the signed-in user; they then edit through the normal authenticated endpoints. Requires authentication (401 if not signed in). Returns the resolved resource id and role:
{ "listId": "lst_abc001", "role": "collaborator" }
(For documents the key is documentId.) Claiming a Viewer link is a harmless no-op; plain viewing needs no grant. A 404 is returned if the link no longer resolves.
Per-person sharing
Granting access to a named user (rather than by link) uses the collaborators/watchers endpoints, which follow the same three roles and the same subscriber-gating on the granting owner:
- Documents:
POST/GET /api/documents/:id/collaborators,PUT/DELETE /api/documents/:id/collaborators/:userId(see Documents). - Lists:
POST/GET /api/lists/:id/watchers,PUT/DELETE /api/lists/:id/watchers/:userId(see Lists).
Adding or changing a person's role notifies that user (in-app, and by email unless the request sets notify: false).
Email invites
An email invite grants a role to a specific email address (including a person who does not have an account yet) while the resource stays private. It differs from a share link in one crucial way:
A share link is a bearer capability: whoever holds the token has access. An invite is bound to the invited email: the token only becomes access once a signed-in user whose verified email matches the invited address claims it. A forwarded invite link is useless to anyone else.
The document and list forms are identical apart from the resource path and the claim response's id key (documentId vs listId). The examples below use documents; swap documents/documentId for lists/listId for the list equivalents.
Endpoint table
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/documents/:id/invites | Session or Bearer | Create (or re-issue) an email invite. Owner only. Subscriber only. |
| GET | /api/documents/:id/invites | Session or Bearer | List a document's invites. Owner only. |
| DELETE | /api/documents/:id/invites/:token | Session or Bearer | Revoke an invite. Owner only. Not subscriber-gated. |
| GET | /api/documents/invite/:token | Optional | Resolve an invite for its landing page. |
| POST | /api/documents/invite/:token | Session only | Claim an invite as the signed-in user. |
| POST | /api/lists/:id/invites | Session or Bearer | Create (or re-issue) a list invite. Owner only. Subscriber only. |
| GET | /api/lists/:id/invites | Session or Bearer | List a list's invites. Owner only. |
| DELETE | /api/lists/:id/invites/:token | Session or Bearer | Revoke a list invite. Owner only. |
| GET | /api/lists/invite/:token | Optional | Resolve a list invite. |
| POST | /api/lists/invite/:token | Session only | Claim a list invite. |
Who can do what
| Action | Auth | Gating |
|---|---|---|
| Create an invite | Session or Bearer | Owner only, and subscriber only: a free owner gets 403. The subscription gate runs before any resource lookup, so existence never leaks to free users. |
| List invites | Session or Bearer | Owner only (404 otherwise). |
| Revoke an invite | Session or Bearer | Owner only (404 otherwise). Not subscriber-gated: a downgraded owner can always revoke. |
| Resolve an invite | Optional | Anyone; the invited email is never returned to anonymous or mismatched callers. |
| Claim an invite | Session only | Requires a verified email that matches the invited address. Accepting is free. |
Creating an invite
POST /api/documents/doc_abc001/invites
Content-Type: application/json
{ "email": "friend@example.com", "role": "collaborator", "expiresAt": null }
| Field | Type | Description |
|---|---|---|
email | string | Required. The address to invite. Validated syntactically (400 if invalid) and stored lowercased/trimmed. |
role | string | Optional. One of watcher, collaborator, manager. Defaults to watcher. An invalid value returns 400. |
expiresAt | string | null | Optional ISO 8601 datetime after which the invite stops resolving. Omit or null for no expiry. An unparseable value returns 400. |
Response (201):
{
"email": "friend@example.com",
"role": "collaborator",
"expiresAt": null,
"url": "https://interlinedlist.com/documents/invite/xN3v…9Qk"
}
url is the invite landing address; an invite email carrying it is also sent to the address (best-effort, fire-and-forget). Re-inviting the same address is idempotent: it re-issues a fresh token and resets the invite to unclaimed. A free (non-subscriber) owner receives 403 ("Subscribe to invite people to documents." / "…lists."); a non-owner receives 404 (existence is never leaked). Invite creation is per-user rate-limited (429 with Retry-After when exceeded).
Listing invites
GET /api/documents/doc_abc001/invites
{
"invites": [
{
"email": "friend@example.com",
"role": "collaborator",
"expiresAt": null,
"accepted": false,
"createdAt": "2025-06-11T09:00:00.000Z",
"token": "xN3v…9Qk"
}
]
}
Owner only: a non-owner gets 404. accepted flips to true once the invite has been claimed. The token is included so the owner can revoke a specific invite.
Revoking an invite
DELETE /api/documents/doc_abc001/invites/xN3v…9Qk
Returns { "revoked": true } on success, or 404 if no such invite exists under that resource or you are not the owner. Not subscriber-gated: an owner whose subscription lapsed can always revoke.
Resolving an invite
GET /api/{documents|lists}/invite/:token renders the invite landing page. Authentication is optional, and the response reveals only what's needed to show the correct branch, never the invited email:
{
"role": "collaborator",
"needsAuth": false,
"canClaim": true,
"wrongAccount": false,
"accepted": false,
"resourceTitle": "Q3 Planning"
}
| Field | Meaning |
|---|---|
role | The role the invite grants. |
needsAuth | true when no user is signed in → prompt to sign in / create an account. |
canClaim | true when the signed-in user's verified email matches the invited address → they may POST to claim. |
wrongAccount | true when a user is signed in but their email doesn't match → they must switch accounts. |
accepted | true if the invite has already been claimed (it still resolves, so the page can link the claimer into the resource). |
resourceTitle | The document/list title, for display. |
A 404 ("Invite not found, expired, or revoked") is returned for any unknown, revoked, or expired token, or when the underlying resource has been deleted; these cases are deliberately indistinguishable. Rate-limited per IP to blunt token-guessing.
Claiming an invite
POST /api/documents/invite/xN3v…9Qk
Cookie: session=<session id list>
Claiming is authenticated by the session cookie only (not a Bearer sync token). On success it upserts a real role grant (a document collaborator / list watcher row) at the invite's role (for every role, including watcher, because a personal invite to a private resource is the access) and marks the invite accepted (idempotent).
{ "documentId": "doc_abc001", "role": "collaborator" }
(For lists the key is listId.) Error cases:
| Status | Condition |
|---|---|
401 | Not signed in. |
403 | Email not verified yet ("Verify your email to accept this invite."). |
403 | Signed-in email doesn't match the invited address ("This invite was sent to a different email address."). |
404 | Invite no longer resolves (unknown / revoked / expired / resource deleted). |
Because a brand-new signup starts unverified, an invited person who just created an account must verify their email first, then claim. To land them back on the invite after signup, the register flow honors a sanitized same-origin returnUrl.
Related
- Documents (Help Center): the user-facing guide to sharing documents.
- Lists (Help Center): the user-facing guide to sharing lists.
- Documents and Lists: the per-person collaborator and watcher endpoints.
- API overview: base URL, authentication at a glance, and response conventions.
- API explorer: try sharing endpoints live with the interactive Swagger console.