---
name: linear-oauth-app
description: Creates or signs in to a Linear workspace and registers an OAuth2 application in Linear's workspace API settings to obtain a client ID and client secret — with redirect URIs, the comma-delimited scope list, private vs public distribution, the actor=user vs actor=app decision, 24-hour access tokens with rotating refresh tokens, app-level webhooks, and a safe credential handoff. Use when asked to get Linear OAuth credentials, create a Linear OAuth application or agent, pick Linear scopes, enable client credentials tokens, rotate a Linear client secret or webhook signing secret, or debug a Linear authorization that returns 401 after a day, cannot install into another workspace, or is blocked by workspace app approvals. For any other vendor's developer portal, use that vendor's skill instead.
---

# Linear OAuth2 App Registration

Get a working Linear OAuth2 client — a workspace to own it, an OAuth application, redirect URIs, a scope set, client
ID and client secret — for a platform that connects many customers' Linear workspaces.

Linear has no separate developer portal. An OAuth application is a row in an ordinary workspace's admin settings, which
makes four things expensive to get wrong, and none of them is the form:

1. **Who owns it.** The app lives inside a Linear workspace and **every admin of that workspace can see and edit it**.
   Linear's own recommendation is to create a dedicated workspace for the app. Put it in the company workspace and
   every admin — now and in future — holds your client secret (§2).
2. **Distribution.** A new app defaults to **private**: usable only in the workspace that created it. A multi-tenant
   connector needs **public**. Nothing about the authorize URL tells you this is wrong; other workspaces simply cannot
   complete the flow (§3).
3. **Tokens now expire.** Linear migrated **all** OAuth apps to short-lived tokens on **1 April 2026**. Access tokens
   last ~24 hours and refresh tokens **rotate on every use**. Any client written against the old "Linear tokens don't
   expire" behaviour works perfectly for a day and then fails everywhere at once (§7).
4. **`actor`.** One query parameter decides whether the app acts as each authorizing user or as a single workspace-wide
   app identity — and the app-identity mode requires a workspace **admin** to install and **cannot request `admin`
   scope**. It is bound to the authorization, so it is not something you change later without re-authorizing (§6).

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **App name**, icon, description, homepage URL | Name is 2–80 chars and **must not contain "Linear"** or `http://`/`https://` (§3) |
| **Developer name** (person or company) | Required field, 2–80 chars (§3) |
| **Which Linear workspace owns the app** | Every admin of it gets access — prefer a dedicated workspace (§2) |
| **Distribution: private or public** | Public is required for other workspaces to install (§3) |
| **Redirect URIs** | Every callback host your platform serves (§4) |
| **Which operations the connector performs** | Decides the scope set; read / write / issues:create / comments:create (§5) |
| **Actor mode: user or app** | Per-user attribution, or one workspace-wide app identity (§6) |
| **Webhooks wanted?** | Enabled on the app itself; auto-creates a webhook per authorizing org (§8) |
| **Client credentials tokens wanted?** | Separate toggle, server-to-server only, 30-day tokens (§7) |
| **Integration Directory listing wanted?** | Optional, reviewed by humans, company-built apps only (§9) |

## Quick Start

1. Confirm a **new** app is needed — a new client ID orphans every existing customer connection (§1).
2. Sign in as a **workspace admin**, ideally of a workspace created just to own this app (§2).
3. **Settings → Administration → API → Applications → New application**, or go straight to
   `https://linear.app/settings/api/applications/new` (§3).
4. Set **distribution to public** if other workspaces must install it (§3).
5. Add **every** redirect URI, exactly (§4).
6. Decide the scope set; scopes are sent **comma-separated**, and `read` is always present (§5).
7. Decide `actor=user` vs `actor=app` before shipping the authorize URL (§6).
8. Capture client ID and client secret; plan for **24-hour tokens and rotating refresh tokens** (§7).
9. Enable app webhooks and pick resource types if you need push events (§8).
10. Verify from a **second, unrelated workspace**, as a **non-admin** if customers will be (§10).
11. Hand the credentials over — never commit them (§11).

## Platform state (verified 2026-09-20 — re-verify before trusting)

- **The docs moved.** `developers.linear.app` now 301-redirects to **`https://linear.app/developers`**. Old deep links
  such as `developers.linear.app/docs/oauth/authentication` land on the developers home page, not the OAuth page.
- **All OAuth apps were migrated to the refresh-token system on 1 April 2026.** Newly created apps issued refresh
  tokens by default from **1 October 2025**; existing apps had until 1 April 2026. Access tokens now return
  `expires_in: 86399` (~24 hours) with a `refresh_token`. Refresh returns a **new access token and a new refresh
  token**. There is a **30-minute grace period** in which the original refresh request can be replayed to recover a
  rotated refresh token lost to a network error.
- **The documented scope list is:** `read`, `write`, `issues:create`, `comments:create`, `timeSchedule:write`,
  `admin`. Agent/app scopes are documented separately: `app:assignable`, `app:mentionable`, `customer:read`,
  `customer:write`, `initiative:read`, `initiative:write`. Scopes are **comma-separated**, and `read` "will always be
  present".
- **`actor=app` supersedes the deprecated `actor=application`.** Installing with `actor=app` is a **workspace-scoped**
  install and **requires admin permissions**. Apps using `actor=app` **cannot also request `admin` scope**.
- **Agent APIs are an explicit Developer Preview** — Linear states functionality may change before general
  availability. Do not build a customer commitment on them without saying so.
- **PKCE is supported** (`code_challenge` / `code_challenge_method`, `plain` or `S256`) and is optional; the plain
  confidential-client exchange with `client_secret` still works.
- **`client_credentials` is supported** but must be toggled on per app. Those tokens are **app-actor tokens**, valid
  **30 days**, with **no refresh token**, capped at **1000 active tokens** per app, all of which must share the same
  scopes — requesting different scopes revokes the rest.
- **Secrets can be rotated in place** (Settings → API → **Rotate secret**). Rotating the **client secret** immediately
  invalidates client-credentials tokens but **does not** revoke existing user access/refresh tokens. Rotating the
  **webhook signing secret** takes effect immediately.
- **OAuth app manifests exist**: the creation page accepts pre-fill query parameters (`distribution`,
  `oauth.redirect_uris`, `webhook.resourceTypes`, …) or a whole JSON manifest, schema version `1.0.0`, published at
  `https://linear.app/.well-known/oauth-app-manifest.schema.json`. Redirect URIs are capped at **32**.
- **Rate limits are per user (or app user) per hour**, not per app: OAuth apps get **5,000 requests/hour** and
  **2,000,000 complexity points/hour**; a single query may not exceed **10,000 complexity points**. Linear
  **dynamically increases** limits for workspace-level apps using actor authorization, based on the workspace's paid
  seat count.
- **Third-party app approvals** are available on **any paid plan** and are off by default. **On Free plans every user
  is an Admin**, which is why a free test workspace cannot reproduce a customer's non-admin experience.
- Unverified: Linear documents `distribution` as a creation-time field with default `private`, but does not state
  whether it can be flipped to `public` later. Check the app's settings page before assuming either way.

If the settings page does not look like this, stop and report what you actually see rather than clicking on.

## 1. Decide: reuse the existing app, or register a new one

A new application means a **new client ID, and every existing customer authorization is bound to the old one** — every
customer re-authorizes. Reuse the existing app for: adding a redirect URI, changing the scope set, rotating a leaked
secret, enabling webhooks or client credentials, or diagnosing an authorization failure.

Register a **new** app only when the user explicitly wants one: a replacement for a compromised app, a separate app for
a different product, a deliberate environment split, or — the Linear-specific case — because the existing app sits in a
workspace whose admin list can no longer be trusted with the secret, or because it was created private and cannot be
made public. Note that scope changes are also customer-visible: a token carries only the scopes granted when it was
issued, so broadening scopes re-authorizes everyone. Say which path you are taking before you touch the settings page.

## 2. Account and workspace — this is the decision people skip

- Sign in at `https://linear.app/login`, or sign up at `https://linear.app/signup`.
- **You must be a workspace admin.** Linear states plainly that admin permissions are necessary to even *view*
  Settings → Administration → API, which is where OAuth applications and webhooks live.
- **Every admin of the owning workspace can access the application** — including its client secret. Linear's own
  recommendation in the Integration Directory guide is to build with OAuth and keep "a separate workspace for the
  application, which gives all admins access to the application (instead of only the application creator)." Read that
  the right way round: a dedicated workspace is how you get *deliberate* shared access instead of accidental exposure,
  and how you avoid the app dying with one employee's account.
- **A free workspace is enough** to create and test an app, and agents installed in a workspace do not count as
  billable users. But on Free plans **all users are Admins**, so you cannot rehearse the customer path where a plain
  Member hits the app-approval wall (§9). For that you need a paid test workspace.
- Record who owns the workspace, and put the owning workspace's name in the handoff summary (§11).

Hand control back for anything only a human can do: email verification, 2FA, accepting terms, being made an admin, or
submitting the directory form. Do not retry a blocked step in a loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand
the user an ordered click path with the literal values to paste — the redirect URIs from §4 and the scope list from §5
— or, better, build a **pre-filled creation link** (§3) so there is nothing to mistype. Continue once they report back
with the client ID.

## 3. Create the application

**Settings → Administration → API → Applications → New application**, or go directly to
`https://linear.app/settings/api/applications/new`.

Fields that carry more weight than they look:

| Field | Rule |
| --- | --- |
| **Application name** | 2–80 characters. **Must not contain "Linear"**, and must not contain `http://` or `https://`. In actor-app mode this is also how the app appears in mention and filter menus, so keep it short, recognisable and unique. |
| **Developer name** | 2–80 characters. Required for a complete setup. |
| **Icon URL** | Absolute HTTP(S) URL; at least 256×256px recommended. Shown during authorization. |
| **Homepage / client URI** | Absolute HTTP(S) URL. Required for a complete setup. |
| **Description** | Up to 1000 characters. Shown during authorization and in settings. |
| **Distribution** | `private` (default) or `public`. **Private means only the owning workspace can install it.** |
| **Redirect URIs** | See §4. Up to 32, unique, absolute HTTP(S). |
| **Grant types** | `authorization_code` is always required; `client_credentials` is an extra toggle (§7). |
| **Webhooks** | Enable, set the URL, and pick resource types (§8). |

**Distribution is the multi-tenant trap.** The default is private. A private app authorizes fine inside its own
workspace, so it looks healthy right up until a customer tries to connect and cannot. If other organizations must
install this, set it to public. Linear does not document whether the value can be changed after creation — check the
app's settings page rather than assuming, and if it cannot be changed, that is a new-app migration (§1), not a tweak.

### Pre-filling the form (worth using every time)

The creation page accepts a manifest, either as dot-notation query parameters or as a single URL-encoded `manifest`
JSON blob. This removes transcription errors from redirect URIs entirely — which matters, because a mistyped redirect
URI fails at the *token exchange*, not at save time. Repeat a parameter to supply array values; unsupported parameters
are ignored; if both a manifest and dot-notation parameters are present, the manifest wins.

```
https://linear.app/settings/api/applications/new
  ?distribution=public
  &developer.name=<Your+Company>
  &oauth.client_name=<App+Name>
  &oauth.client_uri=https%3A%2F%2Fexample.com
  &oauth.redirect_uris=https%3A%2F%2Fapi.unified.to%2Foauth%2Fcode
  &oauth.redirect_uris=https%3A%2F%2Fapi-eu.unified.to%2Foauth%2Fcode
  &oauth.redirect_uris=https%3A%2F%2Fapi-au.unified.to%2Foauth%2Fcode
  &oauth.redirect_uris=https%3A%2F%2Fapi-dev.unified.to%2Foauth%2Fcode
  &oauth.grant_types=authorization_code
```

The JSON manifest form is the canonical version (`schemaVersion: "1.0.0"`), validates against Linear's published JSON
Schema, and is the right thing to keep in a repo if this app is ever recreated.

## 4. Redirect URIs

Register **every** callback host your platform serves. Linear allows up to **32**, they must be unique, and they must
be absolute HTTP or HTTPS URLs. For Unified.to these are one per data center; confirm the current list with the
platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

`redirect_uri` is **required on the authorize call and required again, identical, in the token exchange body**. Linear
documents it as "Same redirect URI which you used in the previous step" on both the standard and PKCE exchanges. A
mismatch surfaces as a failed exchange after the user has already consented — the worst place to find it. Treat
matching as exact: register the literal strings, and do not rely on trailing-slash or subpath forgiveness.

Linear's examples include a `http://localhost:3000/oauth/callback` redirect URI, so loopback HTTP is accepted for
development. Do not leave a localhost URI on a production app.

## 5. Scopes

Scopes go in the authorize URL's `scope` parameter as a **comma-separated** list — not space-separated, which is what
most OAuth tooling emits by default. The full documented set:

| Scope | What Linear says it grants |
| --- | --- |
| `read` | Read access for the user's account. **Always present**, whether or not you ask. |
| `write` | Write access for the user's account. |
| `issues:create` | Create new issues and their attachments. |
| `comments:create` | Create new issue comments. |
| `timeSchedule:write` | Create and modify time schedules. |
| `admin` | Full access to admin-level endpoints. |
| `app:assignable` | Let the app be assigned as a **delegate** on issues and made a project member (agents, §6). |
| `app:mentionable` | Let the app be @mentioned in issues, documents and other editor surfaces (agents, §6). |
| `customer:read` / `customer:write` | Read / read-and-write customer entities (agents, §6). |
| `initiative:read` / `initiative:write` | Read / read-and-write initiative entities (agents, §6). |

Three rules worth internalising:

- **Prefer the narrow write scopes.** Linear's own guidance: "If your application only needs to create comments, use a
  more targeted scope." `issues:create` and `comments:create` exist precisely so you do not have to ask for `write`.
- **`admin` is not a convenience scope.** Linear: "You should never ask for this permission unless it's absolutely
  needed." It is also what a workspace admin looks at hardest before approving the app (§9). The one thing it uniquely
  buys is the ability to create or read webhooks via the API — which you do not need if you use app-level webhooks
  (§8). And it is **incompatible with `actor=app`**.
- **Broadening scopes re-authorizes everyone.** The token carries the scopes granted at issue time; adding a scope to
  the app registration does not upgrade tokens already out there.

> **As of 2026-09-20, Unified.to's Linear connector** requests scopes per capability rather than one fixed set: `read`
> alone for reading projects, issues, teams, people, comments and attachments and for the sign-in identity check;
> `read,write` for writing projects, teams and comments; and `read,issues:create` for creating issues. It does **not**
> request `admin`, `comments:create`, `timeSchedule:write`, or any of the agent/customer/initiative scopes. Register
> at least the union your deployment will send, and confirm the exact current set with the connector's owner — this is
> a snapshot, not a contract.

## 6. `actor=user` vs `actor=app` — pick before you ship the authorize URL

The optional `actor` parameter on the authorize URL decides whose name is on everything the token creates.

| | `actor=user` (default) | `actor=app` |
| --- | --- | --- |
| Attribution | Issues, comments and status changes are created **as the authorizing user** | Created **as the application itself** |
| Install scope | Per user | **Workspace-scoped** |
| Who can install | Any member who can authorize apps | **Admin permissions required** |
| `admin` scope | Allowed | **Not allowed — the two are mutually exclusive** |
| Typical use | Multi-tenant connectors where each person connects their own account | Agents and service accounts |
| Identity | The human | An app user, with the app's name and icon, that does not count as a billable user |

For a platform that connects many customers' workspaces **on behalf of individual people**, `actor=user` is the right
default and is what you get by sending no `actor` at all. Choose `actor=app` only when the product is genuinely one
workspace-wide identity — an agent that gets delegated issues, or a service account for automation. Note the knock-ons:
you need an admin at every customer to install it, you can never add `admin` scope, and with `app:assignable`,
**assigning an issue to the app sets it as the issue's *delegate*, not its assignee**, so humans keep ownership.

`actor` is tied to the authorization and its access token, so switching modes is a re-authorization for every customer,
not a configuration change. `actor=application` is the deprecated spelling; existing uses may keep working, but write
`actor=app`.

In app-actor mode, store the app's per-workspace user id (the `viewer.id` returned by the token) alongside the token —
that is how you tell your own app's activity apart across workspaces.

> **As of 2026-09-20, Unified.to's Linear connector** sends `client_id`, `redirect_uri`, `response_type`, `state` and
> a comma-delimited `scope` on the authorize call, and **does not send `actor`** — so it runs in the default per-user
> mode, and actions are attributed to the person who connected. It also **does not send `prompt=consent`** and **does
> not use PKCE** (it is a confidential client with a secret). Confirm the current parameter set with the connector's
> owner.

### `prompt=consent` and the multi-workspace problem

`prompt=consent` forces the consent screen every time, "even if all scopes were previously granted". Linear calls out
the reason it matters: it "can be useful if you want to give users the opportunity to connect **multiple workspaces**."
A user who belongs to two Linear workspaces and has already authorized one will otherwise be sent straight back with a
token for that same workspace, with no chance to pick the other. If your product lets one person connect more than one
Linear workspace, you want this parameter, and it is a client-side change, not an app-registration one.

## 7. Capture the credentials, and plan for expiry

From the application's settings page in Settings → Administration → API:

- **Client ID** and **client secret**
- The **webhook signing secret**, if webhooks are enabled (§8)
- Endpoints, fixed and identical for everyone:
  - authorize — `https://linear.app/oauth/authorize`
  - token / refresh / client credentials — `https://api.linear.app/oauth/token`
  - revoke — `https://api.linear.app/oauth/revoke`
  - GraphQL API — `https://api.linear.app/graphql`; file storage — `https://uploads.linear.app`

**The token exchange is form-encoded**: `POST https://api.linear.app/oauth/token` with
`Content-Type: application/x-www-form-urlencoded` and `code`, `redirect_uri`, `client_id`, `client_secret`,
`grant_type=authorization_code` in the body. PKCE swaps `client_secret` for `code_verifier` (the secret becomes
optional). The response is JSON: `access_token`, `token_type: "Bearer"`, `expires_in: 86399`, `scope`, `refresh_token`.

**Refresh** is the same endpoint and content type, with `refresh_token` and `grant_type=refresh_token`. Credentials go
either in the body or as HTTP Basic (`Authorization: Basic base64(client_id:client_secret)`) — Linear accepts both,
unlike vendors that insist on one. A PKCE-issued token can refresh with `client_id` alone.

Three things that bite:

1. **Refresh tokens rotate.** Every refresh returns a *new* refresh token. Store the new pair from every response or
   the second refresh fails. The **30-minute grace period** exists exactly for the case where you lost the response to
   a network error: replay the original request within 30 minutes to get the new refresh token back.
2. **Tokens expire now, and did not used to.** Any code, doc or blog post predating **1 April 2026** that says Linear
   tokens are effectively permanent is stale. A connector that never refreshes will pass every test on day one and 401
   everywhere on day two.
3. **`scope` shape varies by app age.** Apps created **before 1 December 2023** get `scope` back as an **array of
   strings**; newer apps get a space-joined string. If you are adopting an old app, check which you have.

**Revocation**: `POST https://api.linear.app/oauth/revoke` with the credential in the `token` form field, optionally
`token_type_hint` (`access_token` or `refresh_token`). 200 = revoked, 400 = could not revoke (e.g. already revoked),
401 = could not authenticate. The legacy `access_token`/`refresh_token` form fields still work for compatibility but
must not be combined with `token`. Customers can also revoke from their side — an authorization can disappear with no
warning beyond the next 401 (and, if you subscribe, an `OAuthApp` revoked webhook, §8).

**Rotating the client secret** is available in place under Settings → API → **Rotate secret**, with a separate control
for the webhook signing secret. Linear is specific about the blast radius: rotating the client secret **immediately
invalidates client-credentials tokens** but **does not revoke access or refresh tokens already granted** — those keep
working and pick up the new secret at their next refresh. Revoke those separately if that is what you meant. Never
rotate without explicit go-ahead.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be rotated, then move on.

> **As of 2026-09-20, Unified.to's Linear connector** exchanges the code with a form-encoded POST carrying
> `client_id`, `client_secret`, `redirect_uri`, `code` and `grant_type` **in the body** (not HTTP Basic), refreshes
> with `refresh_token`, `grant_type`, `client_id` and `client_secret` the same way, and sends the result as a
> `Bearer` token. It also supports a **personal API key** as an alternative to OAuth for single-workspace customers —
> note that a workspace admin can block Members from creating personal API keys, so that path is not always available.
> It paginates with Linear's cursor and **caps page size at 50** deliberately, because larger pages blow the query
> complexity budget. Confirm all of this with the connector's owner rather than trusting the snapshot.

### Client credentials tokens (only if a user flow is genuinely impossible)

For CI and scheduled automation, Linear supports `grant_type=client_credentials` with a required comma-separated
`scope`. You must **toggle client credentials on for the app** first; otherwise the response is
`"Client does not support the client_credentials grant type"`. The token is an **app-actor token** with access to all
public teams in the workspace, valid **30 days**, **with no refresh token** — fetch a new one when you get a 401, and
Linear asks you to request one per run rather than treating it as a long-lived API key. Up to **1000** can be active in
parallel **provided they all carry the same scopes**; requesting different scopes revokes every existing app-actor
token. Mixing grant types breaks this: if you first mint an app-actor token some other way, you cannot then hold
several in parallel. Team access for the app user is editable by workspace admins at any time. This is not a substitute
for the user OAuth flow in a multi-tenant connector.

## 8. Webhooks — app-level, not API-level

Linear has two different webhook mechanisms and confusing them costs a scope you did not need:

- **App-level webhooks (what a multi-tenant connector wants).** Configure a webhook URL and resource types **on the
  OAuth application**. Then, "each time a new organization authorizes the given application, a webhook will be created
  for that organization that posts to the provided webhook URL." No per-customer setup, no `admin` scope. When the app
  is de-authorized from an organization, an **`OAuthApp` revoked** event is sent, carrying `oauthClientId` and
  `organizationId` — the cleanest signal that a customer disconnected.
- **API-created webhooks.** The `webhookCreate` mutation. Linear: "Only workspace admins, or OAuth applications with
  the `admin` scope, can create or read webhooks." If you find yourself wanting `admin` for webhooks, you probably
  wanted app-level webhooks instead.

Practicalities:

- The endpoint must be a **publicly accessible HTTPS, non-localhost URL**, and must not be a loopback host, a
  private-network host, or on `linear.app`.
- Respond **200 within 5 seconds**. Failures retry **3 times** with backoff at **1 minute, 1 hour, 6 hours**; a URL
  that stays unresponsive **may be disabled by Linear and must be re-enabled manually**.
- Verify the **`Linear-Signature`** header: hex HMAC-SHA256 of the **raw** body with the webhook signing secret.
  Restringifying parsed JSON changes the bytes and breaks the signature. Also check `webhookTimestamp` is within a
  minute to block replays. Linear additionally publishes outbound IPs on its security page if you want an allowlist.
- Resource types selectable on an app include `AgentSessionEvent`, `AppUserNotification`, `Attachment`, `Comment`,
  `Customer`, `CustomerNeed`, `Cycle`, `Document`, `Initiative`, `InitiativeUpdate`, `Issue`, `IssueLabel`,
  `IssueSLA`, `OAuthAuthorization`, `PermissionChange`, `Project`, `ProjectLabel`, `ProjectUpdate`, `Reaction`,
  `Release`, `ReleaseNote`, `User`.
- **Linear explicitly discourages polling** the API for updates and points at webhooks instead — which matters,
  because polling burns the per-user request and complexity budgets in §10.

For agents, enabling webhooks and selecting **Agent session events** is not optional — it is how the app learns it was
mentioned or delegated an issue. Linear also suggests **Inbox notifications** and **Permission changes**; the latter
fires a `PermissionChange` webhook when an admin changes or revokes the app's team access.

> **As of 2026-09-20, Unified.to's Linear connector** detects changes by **polling** rather than by registering an
> app-level webhook URL. If that is still true, nothing on the app registration needs a webhook URL — but it is worth
> raising with the connector's owner, because app-level webhooks are the documented, rate-limit-friendly path and
> would also deliver de-authorization notices.

## 9. Approvals, distribution and the Integration Directory

Three separate gates, often confused:

1. **Distribution (yours).** Private vs public on the app itself (§3). Public is what makes the app installable by
   other workspaces at all. There is no Linear review to make an app public.
2. **Third-party app approvals (the customer's).** Available on **any paid plan** and off by default. A workspace owner
   (Enterprise) or admin (other paid plans) turns it on at **Settings → Administration → Security**. Once on, a Member
   who tries to install your app gets a "request approval" screen instead, with an optional reason; the admins get an
   email and an in-app notification; approved and denied apps are listed under **Applications**, where an admin can
   also **revoke authorization for an already-installed application** or flip a previous decision. If an app was
   denied before, the requester is told, along with the reason. This is entirely customer-side — you cannot register
   your way out of it, and it is the most common cause of "the connect button did nothing" from enterprise customers.
3. **Integration Directory listing (optional).** A public app works through its OAuth flow without ever being listed.
   If a listing is wanted: fill out Linear's submission form and send assets to `integrations@linear.app` — a colour
   icon and a monochrome white icon at 320×320, and one to three showcase images at 1600×1000. Linear says it accepts
   integrations it "think[s] are useful to the community and are built by formal companies" and "generally do[es] not
   accept scripts, low-effort/vibe coded or apps built by hobbyists." Listing changes and delisting go to the same
   address. Do not promise a customer a directory listing before it is accepted, and do not invent compliance, data
   retention or volume claims on the form — ask.

## 10. Verify end-to-end

Testing in the workspace that owns the app proves very little — you are an admin there, and on a Free plan so is
everyone else. Test the path a customer takes:

1. Authorize from a **second, unrelated workspace**, through your platform's real connect flow, as a **non-admin
   Member** if customers will be. On a paid test workspace, turn **third-party app approvals on** once and confirm your
   product handles the approval-pending state sanely.
2. Confirm the token response carries `expires_in` (~86399) and a `refresh_token`.
3. Force a **refresh**, then force a **second refresh using the token returned by the first**. That is the test that
   catches a client ignoring rotation — and the one that would have caught the 1 April 2026 migration.
4. Fast-forward past 24 hours (or expire the stored token deliberately) and confirm a real read still works.
5. Call `query { viewer { id name } }` on `https://api.linear.app/graphql` with `Authorization: Bearer <token>`. In
   `actor=app` mode, **store `viewer.id` per workspace** — it is how you identify your own app later.
6. Exercise one write in each mode you support and check the attribution in the Linear UI matches what you promised
   (the user, or the app).
7. If webhooks are on, authorize from the second workspace and confirm a webhook was auto-created for that org and
   that your signature check passes against the **raw** body.
8. Deliberately **revoke** from the customer side (admin → Applications → revoke) and confirm your platform notices,
   via the `OAuthApp` revoked webhook or a clean 401 path.
9. Watch `X-RateLimit-*` and `X-Complexity` response headers on a realistic sync and confirm page sizes keep single
   queries well under **10,000** complexity points.

| Symptom | Cause |
| --- | --- |
| Everything works for a day, then 401 everywhere | Access tokens expire in ~24h since the 1 April 2026 migration; the client is not refreshing (§7) |
| First refresh succeeds, second fails | Rotated `refresh_token` not stored; replay the original request within the 30-minute grace window (§7) |
| Token exchange fails right after consent | `redirect_uri` missing from the exchange body, or not byte-identical to the authorize value (§4) |
| `invalid_client` / auth failure at exchange | Wrong client secret, or the secret was rotated and not redeployed (§7) |
| Another workspace cannot install the app | Distribution is **private** (§3) |
| Install fails only for non-admins | `actor=app` install is workspace-scoped and needs admin (§6) |
| Authorize screen offers "request approval" instead of connecting | Customer has third-party app approvals on (§9) |
| App cannot request `admin` scope | `actor=app` and `admin` are mutually exclusive (§6) |
| `invalid_scope`-style rejection with a valid-looking scope list | Scopes were **space**-separated; Linear wants commas (§5) |
| User can only ever connect one of their workspaces | `prompt=consent` not sent (§6) |
| Cannot see Settings → Administration → API at all | Not a workspace admin (§2) |
| `"Client does not support the client_credentials grant type"` | The client-credentials toggle is off on the app (§7) |
| All app-actor tokens suddenly stop working | A client-credentials token was requested with different scopes, or the client secret was rotated (§7) |
| HTTP **400** with `"code": "RATELIMITED"` in `errors` | Rate limit, not a malformed query — GraphQL returns 400 (§10) |
| Query rejected outright regardless of retries | Single-query complexity over **10,000** points; reduce page size (§10) |
| Webhook signature never matches | Signature computed over restringified JSON instead of the raw body (§8) |
| Webhooks stopped arriving and never resumed | Repeated non-200/slow responses got the webhook disabled; re-enable manually (§8) |
| `webhookCreate` returns a permission error | API-created webhooks need workspace admin or `admin` scope; use app-level webhooks (§8) |
| `scope` comes back as an array, breaking the parser | App created before 1 December 2023 (§7) |
| Files return 401 when downloaded | `uploads.linear.app` needs the same `Authorization: Bearer` header, or a signed URL via `public-file-urls-expire-in` (§8 refs) |

## 11. Hand off — never commit the secret

- **Do not** write the client secret, the webhook signing secret, an access token, a refresh token or a personal API
  key into source control, a test, a fixture, a committed `.env`, a ticket, a PR body, or a chat channel. Values go to
  the user, for their secret store or console. If one leaks, rotate it from the app's settings page and understand what
  that does and does not revoke (§7).
- If a code change is needed (a redirect host, comma-delimited scopes, `prompt=consent`, refresh-token rotation),
  keep it secret-free and say what the human must set out of band.
- Close with: application name, **the workspace that owns it and therefore which admins can read the secret**,
  distribution (private/public), the client ID, where the secret was delivered, the authorize/token/revoke endpoints,
  the exact scope string the client will send, the actor mode, whether client credentials and webhooks are enabled,
  the webhook resource types, whether a directory listing was submitted, and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the user wants a single-workspace setup and a **personal API key** is
the right answer (that run produces no OAuth credentials at all); the owning workspace is the company workspace, because
every admin gains the secret; distribution needs to change and the settings page does not offer it; `actor=app` is on
the table, because it needs an admin at every customer, forecloses `admin` scope, and changes attribution for everyone;
a scope needs adding, because that re-authorizes every existing customer; a customer's admin must approve or has denied
the app (customer-side, not registerable); the Integration Directory form asks for compliance, legal, data-retention or
volume claims; agent/preview APIs would become a customer commitment; or the settings page does not match the
**Platform state** section above.

## References

- Developer docs home — https://linear.app/developers
- OAuth 2.0 authentication (flow, scopes, refresh, revoke, client credentials, secret rotation) — https://linear.app/developers/oauth-2-0-authentication
- OAuth actor authorization — https://linear.app/developers/oauth-actor-authorization
- OAuth application manifests (fields, limits, pre-fill URLs, JSON schema) — https://linear.app/developers/oauth-app-manifests
- OAuth app manifest JSON Schema — https://linear.app/.well-known/oauth-app-manifest.schema.json
- Agents: getting started (actor=app, app:assignable / app:mentionable, customer and initiative scopes) — https://linear.app/developers/agents
- Developing the agent interaction — https://linear.app/developers/agent-interaction
- File storage authentication — https://linear.app/developers/file-storage-authentication
- Webhooks (app-level webhooks, signing, retries, resource types) — https://linear.app/developers/webhooks
- Rate limiting and query complexity — https://linear.app/developers/rate-limiting
- Deprecations policy — https://linear.app/developers/deprecations
- GraphQL API getting started — https://linear.app/developers/graphql
- Pagination — https://linear.app/developers/pagination
- Integration Directory (submission, assets, acceptance bar) — https://linear.app/developers/integration-directory
- Admin docs: API and webhooks (where OAuth apps live; API-key controls) — https://linear.app/docs/api-and-webhooks
- Admin docs: third-party app approvals — https://linear.app/docs/third-party-application-approvals
- Admin docs: members and roles — https://linear.app/docs/members-roles
- Admin docs: security and access — https://linear.app/docs/security-and-access
- Security page (incl. outbound IP addresses) — https://linear.app/security
- Changelog — https://linear.app/changelog
- Create an OAuth application — https://linear.app/settings/api/applications/new
- Workspace API settings — https://linear.app/settings/api
- Integration directory (customer-facing) — https://linear.app/integrations
