Viewers, links, and GitHub

Share tickets with viewers, set parent and child relations, add blocks, relates, and duplicates links, and store GitHub references through the SAQ REST API.

Written for
integration-developer
Roles
owner, admin, member, elevated-client-user, agent
Requires
API token with write scope for parent, links, and GitHub; Viewers and visibility preview: browser session only
Feature
tickets

Viewers are explicit access grants on one ticket; links relate tickets to each other; GitHub links are stored references to commits and pull requests (SAQ has no GitHub app and never fetches from GitHub). Editing a ticket's links needs the same permission as editing the ticket (permissions.edit on the ticket page).

POST /api/w/{slug}/tickets/{ref}/viewers

Share the ticket with people. Token: no (browser session only). Needs permissions.addViewers. Every grant is a timeline event.

Body (AddViewers): { "identityIds": ["…"] }, 1–20 identity ids. Response 200 { "added": ["…"] } with the ids that were granted (already-present viewers are omitted).

Error Status details.reason
not_found 404 unknown_person (with identityId), or ticket not visible
forbidden 403 share_forbidden (with identityId, name): that person cannot be given access

DELETE /api/w/{slug}/tickets/{ref}/viewers/{identityId}

Remove a viewer. Token: no (browser session only). The reporter can never be removed. Response 200 { "ok": true }. Errors: forbidden 403 ("The reporter can never be removed" or "You cannot remove this viewer"), not_found 404.

POST /api/w/{slug}/tickets/{ref}/visibility

Preview who will see a comment at an audience with these mentions, before posting it. Token: no (browser session only).

Body (VisibilityRequest): { "audience": "internal", "mentions": [] } (mentions at most 20). Response 200 (VisibilityPreview): audience, workspaceUsers (count), clientPeople[] (identityId, name, clientName), willGainAccess[] (same shape; mentions that become viewers), denied[] (identityId, name; mentions that would be refused).

PUT /api/w/{slug}/tickets/{ref}/parent

Set or clear the parent. Token: yes, write. Cycles are refused and a parent chain is limited to 10 edges. A parent's estimate is the sum of its children.

Body (SetParent): { "parentId": "01924c3e-8f10-7a22-b3c4-5d6e7f8a9b0d" } or { "parentId": null }.

curl -X PUT https://saq.no/api/w/acme/tickets/ACME-184/parent \
  -H 'Authorization: Bearer saq_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "parentId": "01924c3e-8f10-7a22-b3c4-5d6e7f8a9b0d" }'

Response 200 { "ok": true }.

Error Status details.reason
invalid 400 self_parent
conflict 409 own_ancestor, parent_cycle, parent_depth (with depth)
forbidden 403 Cannot edit the ticket
not_found 404 Ticket or parent not visible

POST /api/w/{slug}/tickets/{ref}/links

Add a link from this ticket to another. Token: yes, write. A blocks link means this ticket blocks the target; the target shows it under blockedBy. Blocking chains are limited to 10 edges and may not form a cycle. A link that already exists is not duplicated.

Body field (LinkInput) Type Required Meaning
targetId uuid yes The other ticket (must be visible to the caller)
kind blocks, relates, duplicates yes Link kind
curl -X POST https://saq.no/api/w/acme/tickets/ACME-184/links \
  -H 'Authorization: Bearer saq_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "targetId": "01924c3e-8f10-7a22-b3c4-5d6e7f8a9b0e", "kind": "blocks" }'

Response 201 { "id": "01924c3e-0c13-7d24-ae45-6f7a8b9c0d1e" }; id is null when the link already existed. Events are written on both tickets.

Error Status details.reason
invalid 400 self_link
conflict 409 dependency_cycle, dependency_depth (with depth)
forbidden 403 Cannot edit the ticket
not_found 404 Source or target not visible

Remove a link by its linkId (from the ticket page's links). Token: yes, write. Response 200 { "ok": true }. Errors: forbidden 403, not_found 404.

POST /api/w/{slug}/tickets/{ref}/github

Store a GitHub reference on the ticket. Token: yes, write. Exactly one of sha or pr must be given.

Body field (GithubLinkInput) Type Required Validation
repo string yes owner/name, 1–200 characters
url string yes An https://github.com/… URL, at most 500 characters
sha string or null one of 7–40 hex characters
pr positive integer or null one of Pull request number
curl -X POST https://saq.no/api/w/acme/tickets/ACME-184/github \
  -H 'Authorization: Bearer saq_YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "repo": "acme/website", "pr": 412, "url": "https://github.com/acme/website/pull/412" }'

Response 201 (GithubLink)

{
  "id": "01924c3e-0d14-7e25-bf56-7a8b9c0d1e2f",
  "repo": "acme/website",
  "sha": null,
  "pr": 412,
  "url": "https://github.com/acme/website/pull/412",
  "createdAt": "2026-09-19T10:30:00.000Z"
}
Error Status details.reason
invalid 400 repo_format, github_url, sha_format, pr_format, sha_or_pr_required (each with field)
forbidden 403 Cannot edit the ticket
not_found 404 Ticket not visible

DELETE /api/w/{slug}/tickets/{ref}/github/{linkId}

Remove a GitHub reference. Token: yes, write. Response 200 { "ok": true }. Errors: forbidden 403, not_found 404.

Every GitHub reference on tickets the caller may read, newest first, at most 500. Token: no (browser session only); the app shows it under Settings → Channels → GitHub.

Response 200 { "links": WorkspaceGithubLink[] } where each entry is a GithubLink plus ticket (id, humanId, title).