For Developers
# List every project this key can see.
curl -s https://unityfreak.com/api/public/v1/projects \
-H "X-API-Key: $UNITYFREAK_API_KEY"Every endpoint on this page lives under one origin — there is no separate API host.
https://unityfreak.com/api/public/v1Spec version 2026-09-11. The machine-readable description of everything below is served, unauthenticated, from /api/public/v1/openapi.json.
Mint a key under Settings → API Keys, give it the scopes it needs, and send it with every request. Three header forms carry the same token — use whichever your HTTP client makes easiest:
X-API-Key: <token>
Authorization: Bearer <token>
Authorization: token <token>X-API-Key takes precedence whenever more than one is present. The third form matches GitHub's classic-PAT scheme, so tooling ported from GitHub works unchanged. Requests here are authenticated by key alone: browser cookies are never accepted on this namespace, so a call made from a logged-in browser behaves exactly like one from a server.
A key carries one or more scopes, and every endpoint below names the one it requires. A write scope implies its read counterpart, and the older broad api scope satisfies all four, so a key minted before these existed keeps working unchanged.
projects:readRead projects, their labels, sprints, epics and custom-field definitions. A list endpoint returns only the projects this key can see.
Also satisfied by: projects:write, api
projects:writeCreate and update projects, create labels, sprints and epics, and set an epic’s custom-field values.
Also satisfied by: api
tasks:readRead tasks, their comments and their custom-field values, across every visible project.
Also satisfied by: tasks:write, api
tasks:writeCreate, update and delete tasks, post comments, add and remove labels, and set a task’s custom-field values.
Also satisfied by: api
A write scope needs a plan with API write access; a key that asks for one without it is refused at mint time rather than failing later on a live request.
Instead of a global scope, a key can be granted access to named projects only. The grammar is:
project:<KEY>:read
project:<KEY>:writeA key holding only project:ACME:write can create and edit tasks in the ACME project and nothing else: GET /projects returns just that one project, and a task, sprint or epic belonging to any other project answers 404. Grants are made from the project picker under Settings → API Keys; a project's key or its id both work as the middle segment.
List endpoints are cursor-paginated. Pass limit to size a page, then follow meta.nextCursor until meta.hasMore is false. A cursor encodes its own sort position, so pages stay stable while rows are written underneath you — never construct one yourself, and never assume a numeric offset.
curl -s "https://unityfreak.com/api/public/v1/projects/ACME/tasks?limit=50&cursor=$NEXT" \
-H "X-API-Key: $UNITYFREAK_API_KEY"Every GET returns a weak ETag. Send it back as If-None-Match and an unchanged resource answers 304 Not Modified with no body. A 304 still counts as one request against your hourly quota — it saves bandwidth and parsing, never quota.
On the unsafe methods the same tag works the other way round: send it as If-Match and the write applies only if nobody has changed the resource since you read it, otherwise it answers 412 and nothing is modified.
Send an Idempotency-Key header on any POST and a retry carrying the same key and an identical body replays the first response — marked Idempotency-Replayed: true — instead of creating a second record. The same key sent with a different body, or to a different route, is rejected rather than silently doing the wrong thing; a duplicate that arrives while the first is still in flight is told to retry shortly.
Limits are per key, per hour, and follow the key owner's plan:
| Plan | Requests per hour |
|---|---|
| Free | 1,000 |
| Pro | 5,000 |
| Team | Unlimited |
| Enterprise | Unlimited |
A limited key gets RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset on every response, and 429 once the window is spent. A key on an unlimited plan is never counted and receives no RateLimit-* headers at all — their absence is how "no ceiling" is represented, because no legal header value means unlimited. Do not read a missing RateLimit-Remaining as zero. An individual key may also carry a lower ceiling of its own, which can only narrow the plan's, never raise it.
"Unlimited" describes the per-key hourly ceiling only. Every request to this namespace — including one that never carries a valid key — also passes a coarse, unauthenticated 100 requests per minute, per IP address limiter first, before your key is even validated. That limiter is shared across every integration calling from the same address, replies with 429 and { error: { code: "RATE_LIMITED" } } rather than this API's own RATE_LIMIT_EXCEEDED, and is not affected by your plan. A Team/Enterprise key is unlimited per key, but still subject to this per-IP ceiling.
GET /tasks returns tasks across every project a key can see and accepts an updatedSince filter — this is how you keep an external system in step without running a webhook receiver.
Ordinary updatedSince listing filters on task updatedAt and its boundary is inclusive: page through a cycle, then retain the largest updatedAt you consumed and de-duplicate on id at that boundary. An empty page carries no new checkpoint — keep the updatedSince you already have; there is nothing new to advance to. Trigger polling (trigger=created or trigger=updated) instead reads the immutable feed ordered by revision, filters by inclusive publishedAt, and returns each item’s revision/changeKind/publishedAt metadata. Follow meta.nextCursor until meta.hasMore is false; only that terminal response includes X-UF-Polling-Checkpoint, which begins the next immutable window — unlike ordinary listing, this checkpoint always advances to the end of the polled window even when the page was empty, because it is bounded by feed revision, not by which rows were returned. Do not use a clock value such as HTTP Date. Delivery is at-least-once and never silently lossy. meta deliberately contains only nextCursor/hasMore, nothing time-varying, so this endpoint can actually 304 on an unchanged page.
# $CHECKPOINT is the largest updatedAt you saw in the last full cycle.
curl -s "https://unityfreak.com/api/public/v1/tasks?updatedSince=$CHECKPOINT&limit=100" \
-H "X-API-Key: $UNITYFREAK_API_KEY"Every non-2xx response carries the same envelope — { "error": { "code", "message", "status" } } — so a client can branch on error.code without parsing prose.
| Status | Meaning |
|---|---|
| 304 | Not Modified — the ETag supplied in If-None-Match still matches the current body. A 304 still counts against the calling key's hourly rate-limit quota; it only saves response bandwidth, never quota. |
| 400 | The request failed validation. |
| 401 | Missing, invalid or expired API key. |
| 403 | The key lacks the scope required for this endpoint, or its owner cannot access the targeted resource. |
| 404 | The resource was not found, or this key cannot see it. |
| 409 | IDEMPOTENT_REQUEST_IN_PROGRESS — a request with this Idempotency-Key is already being processed. Retry shortly. |
| 412 | PRECONDITION_FAILED — the ETag supplied in If-Match no longer matches the current resource. Nothing was changed; re-read the resource and retry. |
| 422 | IDEMPOTENCY_KEY_REUSED — the Idempotency-Key header was already used for a request with a different target URL or body; nothing was created or replayed. |
| 429 | Rate limit exceeded. RateLimit-* response headers report the ceiling and remaining budget for a limited key. They are ABSENT — never the literal "unlimited" — on a Team/Enterprise key with no hourly ceiling, since draft-7 has no legal value meaning "unlimited"; omitting the headers is the correct representation of that state. "Unlimited" describes the per-key hourly ceiling only: every request to this namespace, including one that never carries a valid key, also passes a coarse, unauthenticated 100-requests-per-minute-per-IP-address limit first — before the key is even validated — which a Team/Enterprise key does not exempt it from. That per-IP limit responds with { "error": { "code": "RATE_LIMITED" } }, not this endpoint's own RATE_LIMIT_EXCEEDED. |
| 503 | The API is temporarily in maintenance mode. Retry after the Retry-After header elapses. |
Generated from the OpenAPI document, so this is the API rather than a description of it. Paths are relative to https://unityfreak.com/api/public/v1.
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | / | Public | Namespace root — the API version, spec version and a documentation link. Unauthenticated. |
| GET | /openapi.json | Public | This OpenAPI 3.0 document, describing every endpoint in this namespace. Unauthenticated. |
# The namespace root is unauthenticated — a one-call connectivity check.
curl -s https://unityfreak.com/api/public/v1/| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /projects/{projectKey}/boards | projects:read | List a project's kanban boards. |
| GET | /projects/{projectKey}/columns | projects:read | List a project's board columns. |
| GET | /projects/{projectKey}/automation-options | api | List current, key-authorized project setup choices for an automation connector. |
| GET | /projects | projects:read | List projects visible to the authenticating key. |
| POST | /projects | projects:write | Create a project. |
| GET | /projects/{projectKey} | projects:read | Get a single project by key or id. |
| PATCH | /projects/{projectKey} | projects:write | Update a project. |
| GET | /projects/{projectKey}/tasks | tasks:read | List a project's tasks. |
| POST | /projects/{projectKey}/tasks | tasks:write | Create a task in a project. |
| GET | /projects/{projectKey}/labels | projects:read | List a project's labels. |
| POST | /projects/{projectKey}/labels | projects:write | Create a label on a project. |
| GET | /projects/{projectKey}/sprints | projects:read | List a project's sprints. |
| POST | /projects/{projectKey}/sprints | projects:write | Create a sprint on a project. |
| GET | /projects/{projectKey}/epics | projects:read | List a project's epics. |
| POST | /projects/{projectKey}/epics | projects:write | Create an epic on a project. |
| GET | /projects/{projectKey}/custom-fields | projects:read | List a project's custom field definitions. |
| GET | /projects/{projectKey}/documents | projects:read | List a project's documents (without body content). |
| GET | /projects/{projectKey}/documents/{documentId} | projects:read | Get a single project document, including its body. |
# List every project this key can see.
curl -s https://unityfreak.com/api/public/v1/projects \
-H "X-API-Key: $UNITYFREAK_API_KEY"| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /tasks | tasks:read | List tasks across every project the key can see, optionally since a timestamp. |
| GET | /tasks/{taskKey} | tasks:read | Get a single task by key or id. |
| PATCH | /tasks/{taskKey} | tasks:write | Update a task. |
| DELETE | /tasks/{taskKey} | tasks:write | Delete a task. |
| GET | /tasks/{taskKey}/comments | tasks:read | List a task's comments. |
| POST | /tasks/{taskKey}/comments | tasks:write | Post a comment on a task. |
| POST | /tasks/{taskKey}/labels | tasks:write | Assign a label to a task. |
| DELETE | /tasks/{taskKey}/labels/{labelId} | tasks:write | Remove a label from a task. |
| GET | /tasks/{taskKey}/custom-fields | tasks:read | Get a task's custom field values. |
| PUT | /tasks/{taskKey}/custom-fields | tasks:write | Set a task's custom field values (bulk). |
# Create a task. Safe to retry: the same Idempotency-Key never creates twice.
curl -s -X POST https://unityfreak.com/api/public/v1/projects/ACME/tasks \
-H "X-API-Key: $UNITYFREAK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"title":"Fix login redirect"}'| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /sprints/{sprintId} | projects:read | Get a single sprint by id. |
# List a project's sprints.
curl -s https://unityfreak.com/api/public/v1/projects/ACME/sprints \
-H "X-API-Key: $UNITYFREAK_API_KEY"| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /epics/{epicId} | projects:read | Get a single epic by id. |
| PATCH | /epics/{epicId} | projects:write | Update an epic. |
| GET | /epics/{epicId}/custom-fields | projects:read | Get an epic's custom field values. |
| PUT | /epics/{epicId}/custom-fields | projects:write | Set an epic's custom field values (bulk). |
# Read an epic by id.
curl -s https://unityfreak.com/api/public/v1/epics/$EPIC_ID \
-H "X-API-Key: $UNITYFREAK_API_KEY"| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /me | api | Identify the authenticating key and its owning user. |
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| POST | /auth/revoke | api | Revoke the authenticating key itself (self-revocation). |
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| POST | /oauth/device/code | Public | Start an RFC 8628 device authorization flow for the CLI. |
| POST | /oauth/token | Public | Poll for (or receive) the device-flow access token. |
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /mcp | api | Open a remote MCP session over Server-Sent Events. Requires a valid API key; grants no PM scope of its own — each proxied tool call is authorized against the scope its own underlying route requires. |
| POST | /mcp/messages | Public | Send a JSON-RPC message to an already-open MCP session (identified by the `sessionId` query parameter established by GET /mcp). Carries no API key of its own — the session it targets was already authenticated when it was opened. |
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /automation/capabilities | api | Test an API-key connection and return only its safe effective capabilities. |
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /hooks | hooks:read | List hooks owned by this key. |
| POST | /hooks | hooks:write | Register a managed automation hook. |
| GET | /hooks/samples | hooks:read | List marked synthetic hook samples. |
| GET | /hooks/{id} | hooks:read | Get an owned hook. |
| DELETE | /hooks/{id} | hooks:write | Delete an owned hook. |
The full OpenAPI 3.0 document — request and response schemas, every status code, every header — is public and needs no key. Point a generator at it rather than hand-writing a client.