The ticket API lets your own systems file a card onto an Everframe board directly — a failing CI run, a cron job, a Slack bot, anything that is not a human clicking around the dashboard.
curl -X POST https://everframe.dev/api/v1/tickets \
-H "Authorization: Bearer $EVERFRAME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Checkout crashes on iOS 18"}'
Get a token
Settings → API tokens → Create. An organisation admin role is required for every token operation — create, list, rename, re-scope, revoke.
Each token has:
- A name, up to 80 characters. This becomes the ticket’s author in Everframe,
on the card and in activity history, so name it after the system using it
(
ci-bot,support-triage), not a person. Tokens are deliberately left out of the@mentionlist — there is nobody behind one to notify. - A scope: all projects, or a chosen list. Narrowing to only the projects an integration actually needs limits the blast radius if the token leaks. A CI job that files against one project has no business holding a token that reaches every other one.
The token is shown once. Everframe stores only a hash and cannot show it again. Lose it and you revoke it and create a new one.
An API token is not an SDK key. An SDK key ships inside your application — a web bundle or a decompiled binary yields it — which is why the endpoint it authenticates only ever accepts reports. An API token can create tickets in your projects. Never embed one in an application you ship: it belongs in CI secrets or a server environment.
Authenticate
Authorization: Bearer evf_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Any problem with the credential — missing, malformed, unknown, or revoked —
returns 401 { "error": "invalid_api_token" }, deliberately without saying
which. Telling a caller a token is “revoked” rather than “invalid” would confirm
it once existed. Revocation takes effect immediately.
Create a ticket
POST /api/v1/tickets
| Field | Type | Notes |
|---|---|---|
title | string, required | 1–200 characters, plain text. |
description | string | Up to 20 000 characters, plain text. |
projectId | uuid | Defaults to the token’s only reachable project; required once it reaches more than one. |
boardId | uuid | Defaults to the project’s first board. |
columnId | uuid | Defaults to the board’s leftmost column. |
priority | string | none, low, medium, high. Defaults to none. |
assigneeUserId | uuid, nullable | Must be a member of the organisation. |
agentRepositoryId | uuid, nullable | Must be a repository already bound to the project. |
startsAt | string, nullable | YYYY-MM-DD. |
dueAt | string, nullable | YYYY-MM-DD. |
projectId, boardId and columnId resolve in order. Omit the project and the
token must reach exactly one — the request fails rather than guessing once it
reaches more than one. Omit the board and you get the project’s first; omit the
column and you get that board’s leftmost.
Assigning to the agent starts a run. If
assigneeUserIdnames the organisation’s AI agent, Everframe enqueues an autonomous run against the ticket — the same as assigning a card to the agent in the dashboard. It consumes credits and may open a pull request. That is what makesagentRepositoryIduseful, since it selects which bound repository the run works against. If you only want a card on the board, do not setassigneeUserIdto the agent.
On success, 201:
{
"id": "<uuid>",
"title": "Checkout crashes on iOS 18",
"projectId": "<uuid>",
"boardId": "<uuid>",
"columnId": "<uuid>",
"createdAt": "<ISO 8601>",
"url": "<dashboard deep link>"
}
url is a deep link straight to the new card — print it in CI output so a
person can click through.
Idempotency
A CI job that retries after a timeout must not file the same ticket twice. Send
an Idempotency-Key header derived from something already unique to the run:
-H "Idempotency-Key: ci-run-${GITHUB_RUN_ID}"
Reusing a key replays the original ticket — the same 201 and body —
rather than creating a new one, so do not reuse one expecting it to have
expired. The key is checked before the body is validated, so a retry whose body
was mangled in transit still replays rather than failing with 400. Keys are
scoped to the token that sent them.
The guarantee is for sequential retries, which is what a timeout-and-retry actually is. Two requests carrying the same key genuinely in flight at once can both pass the check before either has recorded it, and both create a ticket; from then on the key replays whichever won. If you fan out ticket creation concurrently, do not rely on the key alone to deduplicate.
Errors
| Status | Error | Meaning |
|---|---|---|
400 | invalid_input | Body failed validation, including an unrecognised field. |
400 | project_required | The token reaches more than one project; name one. |
400 | invalid_date_range | startsAt is after dueAt. |
400 | not_a_member | assigneeUserId is not a member of the organisation. |
400 | invalid_agent_repository | agentRepositoryId is not bound to the project. |
400 | invalid_idempotency_key | Empty, or longer than 255 characters. |
401 | invalid_api_token | Missing, malformed, unknown, or revoked. |
403 | insufficient_scope | The token lacks tickets:write; the body names it in required. |
404 | project_not_found | No such project, the token cannot reach it, or projectId was omitted and the token reaches none. |
404 | board_not_found | No such board, or it belongs to another project. |
404 | column_not_found | No such column, or it belongs to another board. |
429 | — | Over 60 requests/minute for this token. |
project_not_found deliberately covers both “does not exist” and “not yours” —
the same response either way, so a token cannot be used to map an organisation
it has no access to by comparing error codes.
Limits
| Limit | Value |
|---|---|
| Rate limit, per token | 60 requests / minute |
| Attachments | Not supported in v1 |
Versioning
Within /api/v1, fields may be added to requests and responses without
notice; existing fields will not be removed or change meaning. Do not validate
responses against a strict shape that rejects unknown fields — a client that
does will break on a purely additive change.