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.
/auth/register PublicCreate an account (role: member) and return a JWT.
/auth/login PublicExchange email + password for a JWT.
/auth/me Bearer tokenReturn the authenticated user.
/auth/change-password Bearer tokenChange your own password.
Workspace config
Public, read-only workspace configuration — enabled features and branding.
/config PublicEnabled 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).
/issues Bearer tokenList issues. Returns { issues, pageInfo: { total, limit, offset } }.
Response
200 OK
{
"issues": [ { "id", "identifier", "title", "stateId", "priority", … } ],
"pageInfo": { "total": 142, "limit": 50, "offset": 0 }
}/issues Bearer tokenCreate 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", … } }/issues/:id Bearer tokenGet an issue by id or identifier (e.g. CLA-42).
/issues/:id Bearer tokenUpdate title, state, priority, assignee, project, cycle, labels, …
/issues/:id Bearer tokenDelete an issue and its comments.
/issues/:id/comments Bearer tokenList comments on an issue.
/issues/:id/comments Bearer tokenAdd a comment to an issue.
Projects
Organizational containers for work, with status, health, lead and target date.
/projects Bearer tokenList projects.
/projects Bearer tokenCreate a project.
/projects/:id Bearer tokenGet a project.
/projects/:id Bearer tokenUpdate a project.
/projects/:id Bearer tokenDelete a project.
Cycles
Time-boxed iterations belonging to a team. Filter the list by teamId.
/cycles Bearer tokenList cycles (optionally ?teamId=).
/cycles Bearer tokenCreate a cycle (auto-numbers within the team).
/cycles/:id Bearer tokenUpdate a cycle.
/cycles/:id Bearer tokenDelete a cycle.
Reference data
Read-only collections used to resolve ids on issues and projects.
/teams Bearer tokenList teams.
/workflow-states Bearer tokenList workflow states (optionally ?teamId=).
/labels Bearer tokenList labels.
/users Bearer tokenList 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.
| Authentication | Requests / hour | Complexity / hour |
|---|---|---|
| API key | 2,500 | 3,000,000 |
| OAuth app | 5,000 | 2,000,000 |
| Unauthenticated | 600 per IP | 100,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 pointsPer-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.
| Endpoint | Requests | Window |
|---|---|---|
fileUpload | 500 | 1 hour |
searchIssues · searchProjects · searchDocuments | 240 each | 1 hour |
login | 60 | 15 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.