Linear API sandbox
Point @linear/sdk at https://linear.sandboxapis.dev/graphql via the client's apiUrl option (Linear is GraphQL-only: one endpoint). Both auth forms pass: a bare API key in Authorization, or an OAuth Bearer token.
Coverage badge
Full17/17 · 100%
The share of read rows with a final answer — served and verified, a deviation, retired upstream, or a reviewed empty/refusal. Computed in the coverage manifest, copied here.
linear.sandboxapis.devServed & verified
16 / 94%
Answering with real universe data, each response checked against Linear's published spec by the conformance suite on this build.
The full read API
17
Every GraphQL entity Linear publishes, deferred long tail included, minus the rows excluded by policy. Writes are out of scope: this universe is read-only.
Not served yet
1
Each returns an explicit, provider-shaped coverage error naming the gap — never invented data.
01 / The swap
No SDK of ours, no shim, no recorded fixtures. The same client library you already use, one environment variable different.
curl -X POST "https://linear.sandboxapis.dev/graphql" -H 'content-type: application/json' -d '{"query":"{ rateLimitStatus { kind } }"}'Verified drop-in clients
@linear/sdk91.0.0The versions the conformance suite drives against this host on every build — pinned in coverage/client-pins.yaml, and watched weekly for upstream releases, because a client library moving without us is how a shipped integration breaks silently.
02 / Coverage by family
All 17 rows the coverage manifest carries for Linear, deferred long tail included and nothing capped. Open a family, or filter by path to find the exact endpoint your client calls.
Status — what a conformance test found
Mode — what kind of answer a row gets
A row with no mode shown has not been judged yet. Modes are the manifest's own words, from coverage/MODES.yaml; every empty and refuse carries a written reason and a review date before it counts as final.
Badge — where Full starts
REST + GraphQL + git rows that are not `excluded`. Write operations are NOT rows (DECISIONS 2026-09-01 decision 9): they are counted in meta.write_operations and never enter this ratio.
17 read surfaces in 1 family
SERVED 2026-09-12: a Linear Attachment is an issue-to-external-URL LINK rather than an uploaded file (the SDL's own `url` description), so the canonical row behind it is canon's `issue.closed_by_pull` edge - the same edge GitHub renders as a pull request's closing reference and GitLab as a merge request's closed issue, now with a fourth rendering and nothing new asserted. NOT canon's `attachment` table, whose only parents are ticket_comment and macro; reading it that way would have made this row a canon widening instead of a rendering. `url` points at gh.sandboxapis.dev, which is exactly what renderer-github prints as the pull's html_url, so the link resolves (invariant #5) - the VCS_ORIGIN precedent renderer-circleci already set. NO generation gate: closed_by_pull is a base-schema column, so an older pin answers FEWER attachments rather than an error. `subtitle` and `bodyData` are null because composing Linear's widget copy would be authoring it
SERVED 2026-09-12 (hello-17): IssueRelation is the Linear rendering of canon's issue_link - the same row GitLab serves at /issues/{iid}/links. Linear names both ends outright (issue and relatedIssue) and keeps ONE word for the relationship, so Issue.relations is the rows an issue is the source of and Issue.inverseRelations the rows it is the target of, and neither needs GitLab's direction-dependent vocabulary. The type is mapped to Linear's own set - canon `relates` is Linear `related`, canon `duplicates` is `duplicate` - and the mapping THROWS on a kind with no Linear spelling rather than guessing; `parent` has no member in IssueRelationType at all and carries no issue_link row; parentage is Issue.parent and Issue.children, read from hello-23's own issue_parent table (clause added 2026-10-27, decisions/2026-10-27-2306-hello-23-serving-rest.md). IssueRelation.type is String! and not the enum, copied verbatim from the pinned SDL. On an artifact without the domain every relation field answers an explicit generation error in `errors` rather than an empty connection, which would assert that nothing in this universe is linked (invariant #4's GraphQL clause)
SERVED 2026-09-14 (hello-20 F13 `providerTails`): Initiative is canon's `initiative` table - one row per StorylineKind this organization actually worked in, so the membership is TOTAL and DISJOINT (every epic has a kind, every epic is in exactly one initiative, no initiative is empty) and conformance asserts exactly that rather than a count. `Initiative.projects` are the epics of its own kind, which is the same entity renderer-linear already publishes as a Project. Canon's status enum is spelled `planned`/`active`/`completed` and Linear capitalises it; the map THROWS on a word with no Linear member rather than defaulting, and `Canceled`/`Proposed` are unreachable because canon records neither. `completedAt` is the last close among its epics and exists exactly when the status does; `updatedAt` is that instant or its creation, because nothing in this universe edits an initiative. `visibility` is the PROVIDER'S own derivation quoted from the pinned SDL (derived from its lead team, public when none is assigned) and canon assigns none. `health`/`lastUpdate` are null because an initiative's health comes from an INITIATIVE update and this universe posts updates on projects only. On an artifact without the domain the list answers an explicit generation error in `errors` rather than an empty connection, which would assert that nobody here grouped any work (invariant #4's GraphQL clause)
SERVED 2026-09-14 (founder batch A-R9: renderer work, no canon): a ProjectMilestone is canon's `milestone` seen through the epic whose issues carry it - the row gh. already publishes at /repos/{owner}/{repo}/milestones/{n} and gl. at /projects/{id}/milestones, now with a third rendering and nothing new asserted. THE PAIR IS THE ENTITY: Linear hangs a milestone off exactly one project (`ProjectMilestone.project` is non-null) while canon hangs it off a repository, so the checkpoint exists where issues carrying BOTH an epic and a milestone put it, and one canonical milestone reached through two projects is two checkpoints. `name` is `Sprint N`, the same string renderer-github's shapeMilestone prints for the same row, and BOTH instants come from the shared `ArtifactReader.milestoneTimes` accessor, so three hosts cannot date one milestone three ways (invariant #5). `progress` is the closed share of the issues the checkpoint actually holds, counted on the same two workflow states projectFacts counts, so a milestone and its project cannot disagree; `status` is measured (done when the milestone is closed or all its issues are, overdue when its date has passed with work open, unstarted when nothing has left the backlog, next otherwise). NO generation gate: `issue.epic` and `issue.milestone` are base-schema columns, so an older pin answers FEWER checkpoints - none at all in the hello-3 universe, where no issue carries both, and that empty IS the true answer there
SERVED 2026-09-14 (hello-20 F13 `providerTails`): ProjectUpdate is canon's `project_update` table - one status post per epic, by the person who opened the work, whose health is READ OFF TWO DATES and never written, so the words in `body` and the dates in the same response cannot say different things. Canon hyphenates the health and Linear camel-cases it; the map THROWS on a word with no Linear member rather than guessing. `Project.health`, `Project.healthUpdatedAt` and `Project.lastUpdate` are now read off the newest post instead of the SDK default, because a host serving health null beside a projectUpdates list carrying an at-risk post would contradict itself in one response - and those three STAY null on a pin rather than joining the error, since all are nullable, the SDL defines their null as no update has been posted, and they belong to a row that is green on every generation. `updatedAt` equals `createdAt` and `editedAt` is null (nothing here edits a post, the P20 comments precedent); `diff` is null because a diff needs an earlier post and canon writes one per project; `isStale` is false because staleness is measured against an update cadence nobody set; `commentCount`/`reactions` are empty because canon records neither on a post. The list answers an explicit generation error on an artifact without the domain, never an empty connection
refuse — Linear itself deprecated this surface: packages/conformance/specs/linear/graphql/schema.graphql:39381 marks `roadmap` and :39419 marks `roadmaps` with @deprecated(reason: `Roadmaps are deprecated, use initiatives instead.`), so mirroring it would advertise a surface Linear tells clients to stop calling (the ruled vendor-removed precedent, coverage/reviews/2026-09-01-gitlab-modes.md)
03 / What's simulated
Every Linear call resolves against the same simulated data set every other provider serves. Counted from the linear-2026-08-g12 artifact (universe generation 12):
Each line below is one assertion in the conformance suite named beside it — run on every build, over one artifact, through the real renderers. Nothing is claimed here that no expect checks.
The same issue identifier and title, the same `estimate`, the same history instants in the same order, and the same comment count and comment instants as Jira serves for that issue; per team, cycle names and `startsAt`/`endsAt` equal the Jira board's sprint names and windows.
Against Jira · parity/tracker-parity.test.ts
The history instants for an issue equal the revision instants Azure Boards serves for the same work item, and the `estimate` equals its `StoryPoints`.
Against Azure DevOps · parity/tracker-parity.test.ts
`issue.branchName` is the head branch of the pull request that closed the issue, it round-trips through `issueVcsBranchSearch`, and GitHub serves a branch under that exact name.
Against GitHub · parity/branch-parity.test.ts
`organization.createdAt`, and every team's `createdAt`, is the same instant GitHub's `/orgs/{org}`, GitLab's group and the Zendesk account's locale records return.
Against GitHub, GitLab, Zendesk · parity/org-member-parity.test.ts
The issue at hop 2 of the four-hop walk carries the same identifier, title, estimate and state Jira serves, and the `branchName` at hop 3 is the head branch of the GitHub pull request and the GitLab merge request, on the live hosts and on the frozen `-g6` pins.
Against Slack, Jira, GitHub, GitLab · parity/pinned-cross-category.test.ts
05 / Pinned snapshots
Each pin regenerates byte-identically on every request, so a test written against one never drifts. Generations are DIFFERENT universes, not versions of one — never swap a suffix expecting the same data.
linear-2026-08.snap.sandboxapis.devgeneration 1unversionednot in this generationlinear-2026-08-g10.snap.sandboxapis.devgeneration 10unversionedservedlinear-2026-08-g11.snap.sandboxapis.devgeneration 11unversionedservedlinear-2026-08-g12.snap.sandboxapis.devgeneration 12unversionedservedlinear-2026-08-g2.snap.sandboxapis.devgeneration 2unversionedservedlinear-2026-08-g3.snap.sandboxapis.devgeneration 3unversionedservedlinear-2026-08-g5.snap.sandboxapis.devgeneration 5unversionedservedlinear-2026-08-g6.snap.sandboxapis.devgeneration 6unversionedservedlinear-2026-08-g8.snap.sandboxapis.devgeneration 8unversionedservedlinear-2026-08-g9.snap.sandboxapis.devgeneration 9unversionedservedNeed an endpoint that is not served yet?
Every row Linear's manifest carries is on this page, so “not here” is an answer rather than a gap in the rendering. Tell us which path and which client, and it moves up the queue — the order is set by what people ask for.