API Reference

Flint Task exposes two APIs over the same data: a GraphQL endpoint (the richer one, and the one most clients should use) and a REST API. The REST endpoints live under https://flinttask.com/api and exchange JSON.

Authentication

Use a personal API key (create one under Security & access) or a token from /auth/login, then send it on each request. An API key goes in bare; a login token carries the Bearer prefix:

curl https://flinttask.com/api/issues?limit=20 \
  -H "Authorization: flint_<key>"

curl https://flinttask.com/api/issues?limit=20 \
  -H "Authorization: Bearer <token>"

Authentication

Email + password authentication. Login or registration returns a JWT (valid 7 days). Send it as `Authorization: Bearer <token>` on every authenticated request.

POST/auth/register Public

Create an account (role: member) and return a JWT.

POST/auth/login Public

Exchange email + password for a JWT.

GET/auth/me Bearer token

Return the authenticated user.

POST/auth/change-password Bearer token

Change your own password.

Workspace config

Public, read-only workspace configuration — enabled features and branding.

GET/config Public

Enabled feature flags + workspace branding.

Issues

The core work items. List supports filtering (teamId, stateId, assigneeId, projectId, cycleId, priority), full-text search (q) and pagination (limit ≤ 250, offset).

GET/issues Bearer token

List issues. Returns { issues, pageInfo: { total, limit, offset } }.

Response

200 OK
{
  "issues": [ { "id", "identifier", "title", "stateId", "priority", … } ],
  "pageInfo": { "total": 142, "limit": 50, "offset": 0 }
}
POST/issues Bearer token

Create an issue (auto-assigns identifier, e.g. CLA-42).

Request

{
  "title": "Fix flaky cycle filter",
  "teamId": "t_cla",          // optional, defaults to first team
  "priority": 1,               // 0 none … 1 urgent … 4 low
  "assigneeId": "<user id>",
  "projectId": "<project id>",
  "labelIds": ["l_bug"]
}

Response

201 Created
{ "issue": { "id", "identifier": "CLA-42", "title", "stateId",
             "priority", "labelIds": [...], "createdAt", … } }
GET/issues/:id Bearer token

Get an issue by id or identifier (e.g. CLA-42).

PATCH/issues/:id Bearer token

Update title, state, priority, assignee, project, cycle, labels, …

DELETE/issues/:id Bearer token

Delete an issue and its comments.

GET/issues/:id/comments Bearer token

List comments on an issue.

POST/issues/:id/comments Bearer token

Add a comment to an issue.

Projects

Organizational containers for work, with status, health, lead and target date.

GET/projects Bearer token

List projects.

POST/projects Bearer token

Create a project.

GET/projects/:id Bearer token

Get a project.

PATCH/projects/:id Bearer token

Update a project.

DELETE/projects/:id Bearer token

Delete a project.

Cycles

Time-boxed iterations belonging to a team. Filter the list by teamId.

GET/cycles Bearer token

List cycles (optionally ?teamId=).

POST/cycles Bearer token

Create a cycle (auto-numbers within the team).

PATCH/cycles/:id Bearer token

Update a cycle.

DELETE/cycles/:id Bearer token

Delete a cycle.

Reference data

Read-only collections used to resolve ids on issues and projects.

GET/teams Bearer token

List teams.

GET/workflow-states Bearer token

List workflow states (optionally ?teamId=).

GET/labels Bearer token

List labels.

GET/users Bearer token

List workspace members.

GraphQL

The full domain is also served as a GraphQL API at https://flinttask.com/graphql. It is the richer of the two — one round trip can fetch an issue with its state, assignee, labels and comments — and it is schema-compatible with Linear's, so a client written against their reference works here by changing only the host.

Authentication

Send a personal API key as the bare Authorization value, or a login token with a Bearer prefix.

curl https://flinttask.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: <your API key>" \
  --data '{"query":"{ viewer { id name email } }"}'

Pagination

Every list is a connection. Read it as nodes for the common case, or as edges when you need cursors. Connections accept first, last, after, before, orderBy (createdAt · updatedAt), includeArchived and filter. The first 50 results come back when no arguments are given.

query {
  issues(first: 50, orderBy: updatedAt) {
    edges { node { id identifier title } cursor }
    pageInfo { hasNextPage endCursor }
  }
}

Filtering

Filters are comparator objects. Every field takes eq · neq · in · nin; numbers and dates add lt · lte · gt · gte; strings add contains · containsIgnoreCase · startsWith · endsWith and their negations; optional fields add null. Fields combine with AND; use or for alternatives. Relations nest, and many-to-many relations take every / some.

query {
  issues(filter: {
    state: { type: { eq: "started" } }
    assignee: { email: { eq: "[email protected]" } }
    labels: { name: { eq: "Bug" } }
    or: [{ priority: { eq: 1 } }, { dueDate: { lt: "2026-09-01" } }]
  }) {
    nodes { identifier title priorityLabel state { name } }
  }
}

Search

searchIssues, searchProjects and searchDocuments return a search payload rather than a plain connection: each node is the whole record plus metadata, which holds the matched text trimmed to snippetSize characters (default 100) around the hit. Results come back most-relevant-first unless you pass orderBy. Issues match on identifier, title, description and — unless includeComments: false — comment bodies, and accept the same filter as issues. All three take teamId to search one team, and the usual pagination arguments.

query {
  searchIssues(term: "rate limit", first: 20, snippetSize: 120) {
    totalCount
    nodes { identifier title metadata state { name } }
    pageInfo { hasNextPage endCursor }
  }
}

Relations, milestones and attachments

Each of the three reads from the root as well as through its issue or project: attachment / attachments, issueRelation / issueRelations, and projectMilestone / projectMilestones. issueRelations takes no filter; the other two do. A relation's type is one of blocks · duplicate · related · similar, and either id may be a uuid or a human identifier. A milestone answers status (unstarted · next · overdue · done) and progress from the issues in it. A key limited to some teams sees only relations with both ends in reach, and only milestones whose project it can read.

mutation {
  issueRelationCreate(input: {
    issueId: "FLI-42", relatedIssueId: "FLI-57", type: blocks
  }) {
    success
    issueRelation { id type issue { identifier } relatedIssue { identifier } }
  }
}

Mutations

Each mutation returns a payload carrying success, lastSyncId and the affected record. Deletes return entityId — except documentDelete, which trashes rather than removes and hands back the entity instead; documentUnarchive restores it.

mutation IssueCreate($input: IssueCreateInput!) {
  issueCreate(input: $input) {
    lastSyncId
    success
    issue { id identifier title url branchName }
  }
}

# variables
{ "input": { "title": "Rate-limit the public API", "teamId": "<team id or key>", "priority": 2 } }

Available: issueCreate · issueUpdate · issueDelete · issueArchive · issueUnarchive · commentCreate · commentUpdate · commentDelete · projectCreate · projectUpdate · projectDelete · cycleCreate · cycleUpdate · issueLabelCreate · issueLabelUpdate · issueLabelDelete · attachmentCreate · attachmentUpdate · attachmentDelete · issueRelationCreate · issueRelationUpdate · issueRelationDelete · projectMilestoneCreate · projectMilestoneUpdate · projectMilestoneDelete · documentCreate · documentUpdate · documentDelete · documentUnarchive · fileUpload. Introspection is enabled, so codegen and GraphQL IDEs work against the endpoint directly.

Uploading files

Uploads are two steps: ask for a URL, then PUT the bytes to it. The returned assetUrl is what you reference from a description, a comment or an attachment. Files are capped at 25 MB.

mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) {
  fileUpload(contentType: $contentType, filename: $filename, size: $size) {
    success
    uploadFile { assetUrl uploadUrl headers { key value } }
  }
}

# then, with the headers the payload asked for:
curl -X PUT "<uploadUrl>" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: image/png" \
  --data-binary @screenshot.png

# and attach it to an issue:
mutation { attachmentCreate(input: {
  issueId: "<id>", title: "screenshot.png", url: "<assetUrl>"
}) { success attachment { id url } } }

A one-shot REST form exists for clients that would rather not round-trip: POST /api/files with the file as the raw body, its name in an x-filename header (percent-encoded) and its type in Content-Type.

Rate limiting

Three budgets run side by side: how many requests you made, how much those requests asked for, and — on a handful of named endpoints — how often you called that one endpoint. The first two share a one-hour window. REST and GraphQL draw on the same budget: switching transport does not buy a second allowance. A session token from /auth/login is not rate limited; the limits below apply to personal API keys and to callers with no credentials at all, who are counted by IP.

AuthenticationRequests / hourComplexity / hour
API key2,5003,000,000
OAuth app5,0002,000,000
Unauthenticated600 per IP100,000 per IP

The OAuth row is what a token issued to an OAuth application is charged against. No OAuth token flow is live yet, so today only the first and third rows can be reached.

Complexity

Each property is 0.1 point, each object is 1 point, and a connection multiplies its children’s points by the pagination argument you gave it — or by the default of 50. A single query above 10,000 points is rejected however much of the hourly budget is left.

{ issues(first: 50) { nodes { id title identifier priority } pageInfo { hasNextPage } } }

  nodes      1 + (4 × 0.1)  =  1.4
  pageInfo   1 + (1 × 0.1)  =  1.1
  issues     1 + 50 × 2.5   =  126 points

Per-endpoint limits

A few endpoints carry an individual limit on their own window, charged on top of the hourly budgets rather than instead of them. Every other endpoint is unlimited and sends no X-RateLimit-Endpoint-* headers at all. For GraphQL the endpoint is the root field the document selects.

EndpointRequestsWindow
fileUpload5001 hour
searchIssues · searchProjects · searchDocuments240 each1 hour
login6015 minutes

Minting an upload URL and PUTting the bytes share fileUpload’s budget, so one upload spends two of the 500.

Headers

Every rate-limited response carries what is left of each budget that applies to it. All three Reset values are the time that window rolls over, in UTC epoch milliseconds. The complexity family is only sent by the GraphQL endpoint, since REST has no query to score; the endpoint family is only sent when the endpoint has an individual limit.

X-RateLimit-Requests-Limit: 2500
X-RateLimit-Requests-Remaining: 2496
X-RateLimit-Requests-Reset: 1787325044712

X-Complexity: 126
X-RateLimit-Complexity-Limit: 3000000
X-RateLimit-Complexity-Remaining: 2999873
X-RateLimit-Complexity-Reset: 1787325044712

X-RateLimit-Endpoint-Name: searchIssues
X-RateLimit-Endpoint-Requests-Limit: 240
X-RateLimit-Endpoint-Requests-Remaining: 239
X-RateLimit-Endpoint-Requests-Reset: 1787325044712

When you go over

GraphQL answers 400 with RATELIMITED in the error’s extensions; REST answers 429 with the same code in the body. Both send Retry-After in seconds.

{ "errors": [ { "message": "Rate limit of 2500 requests per hour exceeded",
                "extensions": { "code": "RATELIMITED", "type": "ratelimited" } } ] }

# an endpoint limit names the endpoint and its own window
{ "errors": [ { "message": "Rate limit of 240 requests per hour for searchIssues exceeded",
                "extensions": { "code": "RATELIMITED", "type": "ratelimited" } } ] }

Webhooks

A workspace admin can register endpoints (System administration → Webhooks). When an issue or comment changes, Flint Task POSTs a JSON payload to each endpoint.

POST <your endpoint>
X-Flint-Event: issue.create
X-Flint-Signature: sha256=<hex>

{ "type": "issue", "action": "create",
  "data": { "id", "identifier", "title", … },
  "createdAt": "2026-…Z" }

Verify authenticity with HMAC-SHA256(secret, rawBody) and compare to the X-Flint-Signature header.