Summary and recommendation
Linear's user management API is GraphQL-only, served from a single endpoint at https://api.linear.app/graphql. Authentication supports personal API keys (passed as a bare Authorization header) or OAuth 2.0 Bearer tokens; admin-scope operations - inviting members, updating roles, suspending users - require the admin OAuth scope explicitly.
SCIM 2.0 is available as a parallel interface at https://api.linear.app/scim/v2, gated to Enterprise plan with SAML SSO enabled. The SCIM token is distinct from personal API keys and OAuth tokens and must be generated separately in Settings > Security & Access > SCIM.
Rate limiting is complexity-based, not request-count-based: each token is allocated 1,500 complexity points per hour. Response headers X-Complexity-Budget-Remaining and X-Complexity-Budget-Reset expose current budget state. A single deeply nested query can exhaust the budget faster than many shallow queries.
API quick reference
| Has user API | Yes |
| Auth method | Personal API key (Bearer token) or OAuth 2.0 |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Enterprise |
Authentication
Auth method: Personal API key (Bearer token) or OAuth 2.0
Setup steps
- For personal API key: Navigate to Linear Settings > API > Personal API keys, generate a key, and pass it as 'Authorization:
' header. - For OAuth 2.0: Register an application at https://linear.app/settings/api/applications/new to obtain client_id and client_secret.
- Redirect users to https://linear.app/oauth/authorize with required query params: response_type=code, client_id, redirect_uri, scope, state.
- Exchange the returned authorization code for an access token via POST to https://api.linear.app/oauth/token.
- Use the access token as 'Authorization: Bearer
' on all subsequent API requests.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| read | Read access to all resources the user can access. | Listing users, teams, and organization data. |
| write | Write access to create and update resources. | Creating or updating user-related data via mutations. |
| admin | Administrative access including user management operations. | Inviting members, updating roles, removing users from organization. |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | String (UUID) | Unique identifier for the user. | system-generated | immutable | Used as node ID in all GraphQL references. |
| name | String | Full display name of the user. | required | updatable | |
| String | Primary email address of the user. | required | updatable | Must be unique within the organization. | |
| displayName | String | User's preferred display name. | optional | updatable | |
| avatarUrl | String (URL) | URL of the user's avatar image. | optional | updatable | |
| active | Boolean | Whether the user account is active. | system-set | read-only via GraphQL; managed via SCIM or admin actions | Deactivated users cannot log in. |
| admin | Boolean | Whether the user has workspace admin privileges. | optional | updatable by admin | |
| guest | Boolean | Whether the user is a guest (limited access). | system-set | read-only | |
| createdAt | DateTime | Timestamp when the user was created. | system-generated | immutable | |
| updatedAt | DateTime | Timestamp of last update to the user record. | system-generated | system-updated | |
| lastSeen | DateTime | Timestamp of the user's last activity. | null | system-updated | |
| timezone | String | User's configured timezone (IANA format). | optional | updatable | |
| url | String (URL) | Linear profile URL for the user. | system-generated | immutable | |
| teams | TeamConnection | Teams the user is a member of. | assigned separately | managed via team membership mutations | Returned as a paginated connection. |
| organization | Organization | The organization the user belongs to. | system-set | immutable |
Core endpoints
List all users in organization
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Returns all workspace members. Guests and deactivated users may be included; filter by 'active' field as needed.
Request example
query {
users(first: 50) {
nodes { id name email active admin }
pageInfo { hasNextPage endCursor }
}
}
Response example
{
"data": {
"users": {
"nodes": [{"id":"abc123","name":"Jane Doe","email":"jane@example.com","active":true,"admin":false}],
"pageInfo": {"hasNextPage":false,"endCursor":"cursor_xyz"}
}
}
}
Get single user by ID
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Use 'viewer' query instead to fetch the authenticated user's own record.
Request example
query {
user(id: "abc123") {
id name email active admin lastSeen
}
}
Response example
{
"data": {
"user": {"id":"abc123","name":"Jane Doe","email":"jane@example.com","active":true,"admin":false,"lastSeen":"2024-01-15T10:00:00Z"}
}
}
Get authenticated user (viewer)
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Useful for verifying token identity and org context at session start.
Request example
query {
viewer {
id name email admin organization { id name }
}
}
Response example
{
"data": {
"viewer": {"id":"abc123","name":"Jane Doe","email":"jane@example.com","admin":true,"organization":{"id":"org1","name":"Acme"}}
}
}
Invite user to organization
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Sends an email invitation; user is not active until they accept. Role options are 'member' or 'admin'. Requires 'admin' scope.
Request example
mutation {
organizationInviteCreate(input: {
email: "newuser@example.com"
role: member
}) {
success
organizationInvite { id email }
}
}
Response example
{
"data": {
"organizationInviteCreate": {
"success": true,
"organizationInvite": {"id":"inv_xyz","email":"newuser@example.com"}
}
}
}
Update user (admin flag, display name)
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Only workspace admins can update other users. Users can update their own profile fields (name, timezone, etc.).
Request example
mutation {
userUpdate(id: "abc123", input: { admin: true }) {
success
user { id name admin }
}
}
Response example
{
"data": {
"userUpdate": {
"success": true,
"user": {"id":"abc123","name":"Jane Doe","admin":true}
}
}
}
Deactivate (suspend) user
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Suspended users cannot log in but their data is retained. Requires admin scope. Reversible via 'userUnsuspend' mutation.
Request example
mutation {
userSuspend(id: "abc123") {
success
}
}
Response example
{
"data": {
"userSuspend": {"success": true}
}
}
Add user to team
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: User must already be an organization member. Team membership is separate from org membership.
Request example
mutation {
teamMembershipCreate(input: {
teamId: "team_abc"
userId: "abc123"
}) {
success
teamMembership { id }
}
}
Response example
{
"data": {
"teamMembershipCreate": {
"success": true,
"teamMembership": {"id":"mem_xyz"}
}
}
}
List pending organization invites
- Method: POST
- URL:
https://api.linear.app/graphql - Watch out for: Only returns invites that have not yet been accepted. Requires admin scope.
Request example
query {
organizationInvites {
nodes { id email createdAt }
}
}
Response example
{
"data": {
"organizationInvites": {
"nodes": [{"id":"inv_xyz","email":"newuser@example.com","createdAt":"2024-01-10T08:00:00Z"}]
}
}
}
Rate limits, pagination, and events
- Rate limits: Linear enforces complexity-based rate limiting on GraphQL requests. Each query has a complexity cost; requests exceeding the limit are rejected with HTTP 429.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: Response headers include 'X-Complexity-Budget-Remaining' and 'X-Complexity-Budget-Reset'. Complexity cost per query varies by depth and number of requested fields. Pagination reduces per-request complexity.
- Pagination method: cursor
- Default page size: 50
- Max page size: 250
- Pagination pointer: after (cursor), first (page size)
| Plan | Limit | Concurrent |
|---|---|---|
| All plans (API key) | 1,500 complexity points per hour | 0 |
| All plans (OAuth) | 1,500 complexity points per hour per OAuth token | 0 |
- Webhooks available: Yes
- Webhook notes: Linear supports webhooks configured per-team or workspace-wide via Settings > API > Webhooks or via the 'webhookCreate' GraphQL mutation. Payloads are delivered via HTTP POST to a configured URL.
- Alternative event strategy: Poll the 'users' GraphQL query with 'updatedAt' filter for change detection if webhooks are not feasible.
- Webhook events: User, Issue, IssueLabel, Comment, Cycle, Project, Reaction, Team, OrganizationInvite
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Enterprise
Endpoint: https://api.linear.app/scim/v2
Supported operations: GET /Users – list users, GET /Users/{id} – get user by ID, POST /Users – provision new user, PUT /Users/{id} – replace user attributes, PATCH /Users/{id} – update user attributes (including deactivation), GET /Groups – list groups (teams), POST /Groups – create group, PUT /Groups/{id} – replace group, PATCH /Groups/{id} – update group membership, DELETE /Groups/{id} – delete group
Limitations:
- Requires Enterprise plan with SAML SSO enabled as a prerequisite.
- SCIM-provisioned users are only billed after their first login (as of Aug 2025).
- Supported IdPs with documented integration: Okta, Microsoft Entra ID (Azure AD), OneLogin.
- Google Workspace SCIM integration is not officially supported.
- DELETE /Users is not supported; deactivation is performed via PATCH with 'active: false'.
- SCIM token is generated in Linear Settings > Security & Access > SCIM and must be passed as Bearer token.
Common scenarios
Three integration patterns cover the primary lifecycle operations.
Provisioning via SCIM (Enterprise): Push users from Okta, Entra ID, or OneLogin using POST /Users against the SCIM base URL. Linear creates a pending user record on receipt; the user is not billed and does not appear as active until their first SSO login. SAML must be configured before SCIM provisioning will succeed.
Auditing active members via GraphQL: Query users with first: 250, paginate using pageInfo.endCursor until hasNextPage is false, filter by active: true, and cross-reference the lastSeen field to surface inactive accounts. For identity graph construction, join the id, email, teams, and lastSeen fields to map each user's access footprint across workspace and team boundaries. This is the only programmatic path to activity data - there is no dedicated audit endpoint.
Offboarding via GraphQL: Resolve the user's Linear UUID via a users query filtered by email, then call userSuspend(id: "
Provision a new employee via SCIM (Enterprise)
- Ensure Enterprise plan is active and SAML SSO is configured.
- Generate a SCIM token in Linear Settings > Security & Access > SCIM.
- Configure your IdP (Okta/Entra/OneLogin) with the SCIM base URL 'https://api.linear.app/scim/v2' and the SCIM Bearer token.
- Push the new user from the IdP; Linear receives POST /Users and creates a pending user record.
- User is not billed until they complete their first login via SSO.
Watch out for: If SAML is not enabled, SCIM provisioning will fail. SCIM-created users without a first login do not count toward billing.
Audit all active workspace members via GraphQL
- Authenticate with a personal API key or OAuth token with 'read' scope.
- Send paginated 'users' query with 'first: 250' and iterate using 'pageInfo.endCursor' until 'hasNextPage' is false.
- Filter results by 'active: true' to exclude suspended users.
- Cross-reference 'lastSeen' timestamps to identify inactive accounts.
Watch out for: Max page size is 250. Large organizations may require multiple paginated requests. Complexity budget may be exhausted if many fields are requested per user.
Offboard a departing employee via GraphQL
- Authenticate with an admin OAuth token or personal API key with 'admin' scope.
- Resolve the user's Linear ID via 'users' query filtered by email.
- Call 'userSuspend(id: "
")' mutation to deactivate the account. - Optionally reassign open issues using 'issueUpdate' mutations to a new assignee.
- If on Enterprise with SCIM, trigger deactivation from the IdP via PATCH /Users/{id} with 'active: false' for automated offboarding.
Watch out for: Suspension is reversible via 'userUnsuspend'. There is no permanent user deletion via the GraphQL API. SCIM deactivation via IdP is preferred for automated offboarding on Enterprise.
Why building this yourself is a trap
The primary integration trap is conflating the three distinct credential types: personal API keys, OAuth tokens, and the SCIM Bearer token.
Each has a separate issuance path and scope model; using a personal API key for SCIM endpoints or an OAuth token without admin scope for user mutations will produce authorization errors that are not always clearly surfaced in the response.
A second trap is the pending-invite gap: calling the invite mutation creates a pending record, not an active user. The invited user does not appear in users query results until they accept - any downstream system that polls for new users immediately after an invite call will miss them.
Build explicit handling for this latency or poll organizationInvites separately to track pending state.
Finally, the complexity-based rate limit behaves differently from request-count limits. Requesting deeply nested fields - for example, teams with nested members inside a users query - can exhaust the 1,500-point hourly budget in a small number of calls. Use pagination with first: 250 and request only necessary fields to manage budget consumption predictably.
Automate Linear workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.