Linear API sandbox

Linear

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.

GraphQL · 17 entitiesMCP-ready

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.

Live host
linear.sandboxapis.dev
Pinned hosts
10 — generation 1, 2, 3, 5, 6, 8, 9, 10, 11, 12
Serving since
2026-08-04
Lifecycle
Live

Served & 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

Point your client at a different base URL.

No SDK of ours, no shim, no recorded fixtures. The same client library you already use, one environment variable different.

shell
curl -X POST "https://linear.sandboxapis.dev/graphql" -H 'content-type: application/json' -d '{"query":"{ rateLimitStatus { kind } }"}'

Verified drop-in clients

  • @linear/sdk91.0.0

The 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

Every read surface, grouped the way Linear groups it.

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

served & verified
answers with real universe data, and this build checked that response against the vendor's spec.
deviation
served and faithful to the real provider, but failing the vendored spec — usually a bug in the spec.
retired
the vendor removed the endpoint; snapshots pinned before that date still serve it.
planned / deferred
not served yet — an explicit coverage error naming the gap, never invented data.
excluded
out of the claim by policy (writes, and surfaces we refuse); not in any denominator on this page.

Mode — what kind of answer a row gets

derive
the response is computed from artifact rows that already exist
generate
canon does not carry this yet; the generator will produce it, then derive
empty
the true answer for this universe is an empty collection — reason + reviewed date required
refuse
mirror the provider's OWN refusal (e.g. its 403 for a non-admin token) — reason + reviewed date required
read-only
a write named in the manifest because clients probe it; the read-only 403 IS its final response, and it never joins the badge denominator
scratch
a write this replica ACCEPTS, into a per-key 24-hour overlay, answering with the provider's own success shape - reason + reviewed date required. The compiled artifact is unchanged and no other caller ever sees the row; a caller WITHOUT a scratch-enabled key gets the read-only refusal on the identical path. Like read-only it never joins the badge denominator (founder ruling 5, 2026-09-17: the published figure has meant READ operations served since A0.1), and unlike read-only it ANSWERS, so the generation report counts it in its own `scratch` column rather than in (excluded). Scratch never creates a pull request, a branch, a commit or a diff - those are canon structure the overlay cannot invent

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

Full
every published read row has a final answer
Deep
60% up to 100%
Partial
25% up to 60%
Preview
under 25%

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

graphql16 of 17 served & verified
GQL
attachments

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 & verified
Core
GQL
comments
served & verified
Core
GQL
cycles
served & verified
Core
GQL
issue-history
served & verified
Core
GQL
issue-relations

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 & verified
Core
GQL
issues
served & verified
Core
GQL
labels
served & verified
Core
GQL
orgs
served & verified
Core
GQL
projects
served & verified
Core
GQL
rate-limit
served & verified
Core
GQL
teams
served & verified
Core
GQL
users
served & verified
Core
GQL
workflow-states
served & verified
Core
GQL
initiatives

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 & verified
Next
GQL
project-milestones

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 & verified
Next
GQL
project-updates

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

served & verified
Next
GQL
roadmaps

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)

deferred
Long tail

03 / What's simulated

One data set, rendered in Linear's dialect.

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):

What another provider has to agree with, and the test that makes it

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.

05 / Pinned snapshots

Universes that never change, on their own hostnames.

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.

PinHostUniverse generationLinear API versionRepository files
linear-2026-08linear-2026-08.snap.sandboxapis.devgeneration 1unversionednot in this generation
linear-2026-08-g10linear-2026-08-g10.snap.sandboxapis.devgeneration 10unversionedserved
linear-2026-08-g11linear-2026-08-g11.snap.sandboxapis.devgeneration 11unversionedserved
linear-2026-08-g12linear-2026-08-g12.snap.sandboxapis.devgeneration 12unversionedserved
linear-2026-08-g2linear-2026-08-g2.snap.sandboxapis.devgeneration 2unversionedserved
linear-2026-08-g3linear-2026-08-g3.snap.sandboxapis.devgeneration 3unversionedserved
linear-2026-08-g5linear-2026-08-g5.snap.sandboxapis.devgeneration 5unversionedserved
linear-2026-08-g6linear-2026-08-g6.snap.sandboxapis.devgeneration 6unversionedserved
linear-2026-08-g8linear-2026-08-g8.snap.sandboxapis.devgeneration 8unversionedserved
linear-2026-08-g9linear-2026-08-g9.snap.sandboxapis.devgeneration 9unversionedserved

Need 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.

Request coverage