Skip to content
Everframe Docs
Documentation

Ticket API

Create a ticket from CI, a script, or a bot — without opening the dashboard.

Updated

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 @mention list — 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
FieldTypeNotes
titlestring, required1–200 characters, plain text.
descriptionstringUp to 20 000 characters, plain text.
projectIduuidDefaults to the token’s only reachable project; required once it reaches more than one.
boardIduuidDefaults to the project’s first board.
columnIduuidDefaults to the board’s leftmost column.
prioritystringnone, low, medium, high. Defaults to none.
assigneeUserIduuid, nullableMust be a member of the organisation.
agentRepositoryIduuid, nullableMust be a repository already bound to the project.
startsAtstring, nullableYYYY-MM-DD.
dueAtstring, nullableYYYY-MM-DD.
attachmentIdsuuid[]Up to 10 ids from POST /api/v1/attachments. Claimed with the ticket.

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 assigneeUserId names 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 makes agentRepositoryId useful, since it selects which bound repository the run works against. If you only want a card on the board, do not set assigneeUserId to 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.

Upload a file

POST /api/v1/attachments
Content-Type: multipart/form-data

Upload one file, then reference its id from a ticket or a comment. Send the file as a single multipart part (any field name); the request carries exactly one file. The token needs tickets:write. projectId is an optional query parameter here, with the same rules as for creating a ticket.

On success, 201:

{
  "id": "<uuid>",
  "filename": "shot.png",
  "contentType": "image/png",
  "sizeBytes": 48213
}

An upload is unclaimed until a ticket or comment uses its id. It belongs to the project it was uploaded to and to the token’s principal: another token cannot claim it, and an id from a different project is rejected. Unclaimed uploads are cleaned up after about 24 hours, so claim them promptly.

Limits and accepted types:

  • 25 MB per file, one file per request.
  • PNG, JPEG, GIF, WebP, MP4, WebM, QuickTime, PDF, ZIP, JSON, CSV and plain text. The type is detected from the file’s bytes, not from the filename or the Content-Type you send. SVG, HTML and other markup are refused.
  • 60 uploads per hour per token.
  • At most 50 unclaimed uploads at a time, per token per project.

Upload errors:

StatusErrorMeaning
400invalid_inputNot a multipart request, or no file part.
400too_many_filesMore than one file.
400empty_fileThe file has no bytes.
400unsupported_typeThe detected type is not accepted.
409too_many_unclaimed_attachmentsClaim or wait out the existing unclaimed uploads first.
413file_too_largeOver 25 MB; the body carries the limit in bytes.
429—Over the hourly upload limit.
502storage_unavailableFile storage is not available; retry later.

Plus the shared 401, 403 and 404 project_not_found / 400 project_required errors from Errors.

Create a ticket with files

Upload first, then pass the ids to POST /api/v1/tickets:

ID=$(curl -s -X POST https://everframe.dev/api/v1/attachments \
  -H "Authorization: Bearer $EVERFRAME_TOKEN" \
  -F "[email protected]" | jq -r .id)

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\", \"attachmentIds\": [\"$ID\"]}"

The ids are claimed in the same transaction as the ticket. If any id cannot be claimed, the whole request fails and no ticket is created (see the errors below).

Attach files to an existing ticket

POST /api/v1/tickets/:ticket/attachments
{ "attachmentIds": ["<uuid>", "<uuid>"] }

attachmentIds is required: 1 to 10 ids from the upload endpoint. A ticket holds at most 10 attachments of its own; a request that would push it past that fails whole with too_many_attachments.

:ticket is either the ticket’s uuid or its number (the #123 shown in the dashboard, sent as 123). The request is resolved within one project, so a token that reaches more than one project must pass ?projectId=<uuid> as a query parameter, whichever reference form you use. A token that reaches only one project never needs it. A multi-project token that omits projectId gets 400 project_required.

On success, 200, the ticket plus its attachment count:

{
  "id": "<uuid>",
  "title": "Checkout crashes on iOS 18",
  "projectId": "<uuid>",
  "boardId": "<uuid>",
  "columnId": "<uuid>",
  "createdAt": "<ISO 8601>",
  "url": "<dashboard deep link>",
  "attachmentCount": 2
}

Comment on a ticket

POST /api/v1/tickets/:ticket/comments
FieldTypeNotes
bodystring, required1–20 000 characters, plain text.
attachmentIdsuuid[]Up to 10 ids from the upload endpoint. Belong to the comment.

:ticket and projectId follow the same rules as for attaching files: a uuid or number, with projectId required for a token that reaches several projects. Unrecognised fields are rejected. The comment is authored by the token’s name and marked as written through the API. Files attached to a comment belong to the comment and do not count against the ticket’s own 10 attachments.

On success, 201:

{
  "id": "<comment uuid>",
  "ticketId": "<uuid>",
  "createdAt": "<ISO 8601>",
  "url": "<dashboard deep link to the ticket>"
}
curl -X POST "https://everframe.dev/api/v1/tickets/42/comments?projectId=$PROJECT_ID" \
  -H "Authorization: Bearer $EVERFRAME_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ci-run-${GITHUB_RUN_ID}-comment" \
  -d "{\"body\": \"Reproduced on build 812\", \"attachmentIds\": [\"$ID\"]}"

From the CLI

The everframe CLI wraps the endpoints above, so you can file a ticket or add a screenshot from a terminal without writing curl. It uploads each file, then creates the ticket, attaches, or comments in one go. Run it with npx @everframe/cli ticket ... or install @everframe/cli globally; ticket commands need CLI 1.7.0 or later. The examples below assume the everframe command is on your path.

Create a token with the tickets:write scope (see Get a token) and keep it outside your repository, in your shell profile, a password manager, or CI secrets:

export EVERFRAME_API_TOKEN=evf_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export EVERFRAME_PROJECT_ID=<project uuid>   # optional if the token reaches one project

The CLI uses the same API token as the curl examples above; it just reads it from EVERFRAME_API_TOKEN rather than $EVERFRAME_TOKEN. EVERFRAME_API_URL overrides the API base URL. The project comes from --project first, then the project uuid in a ticket link, then EVERFRAME_PROJECT_ID. A dashboard link that names the project by its slug instead of its uuid needs --project, so a link from one project can never edit a ticket in another.

# Create a ticket, optionally with screenshots
everframe ticket create --title "Checkout crashes on iOS 18" \
  --description-file notes.md --attach shot1.png shot2.png

# Attach files to an existing ticket
everframe ticket attach 42 shot1.png shot2.png

# Comment, optionally with files
everframe ticket comment 42 --body "Reproduced on build 812" --attach trace.png

<ticket> is a ticket number, a ticket uuid, or a dashboard link such as https://everframe.dev/admin/projects/<project>/boards/<board>?card=42. Text comes from --description or --description-file (create) and --body or --body-file (comment); pass only one of each pair. --attach takes one or more files and can be repeated. ticket attach takes its files as plain arguments.

Example: attach a screenshot from your Mac to ticket 42. macOS saves screenshots to the Desktop by default:

everframe ticket attach 42 ~/Desktop/"Screenshot 2026-11-04 at 10.15.00.png"

To grab a region and attach it straight away:

screencapture -i /tmp/shot.png && everframe ticket attach 42 /tmp/shot.png

On success the command prints the ticket URL, or the full response with --json. Files follow the limits below (at most 10 per command, 25 MB each, and the ticket’s own 10-attachment cap still applies); the server decides which types it accepts. Every command sends an Idempotency-Key (a random one unless you pass --idempotency-key, which takes 1 to 255 printable ASCII characters), so retrying with the same key replays the original result instead of writing twice. A rerun still uploads its files before the replay; the replay ignores them, and unused uploads are deleted after 24 hours.

Exit codeMeaning
0Done.
1The API or network failed (rate limits and server errors are retried first), or no token is set. The message names the failing step and the API error code.
2Bad input: unknown flag, unreadable file, too many files, missing title or body, an invalid idempotency key, an unrecognised ticket reference, or a slug link without --project. Nothing was sent.

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 result rather than writing again. The replay returns the same status, with the ticket or comment re-read in its current state (for example, an attach replay returns the current attachmentCount), so do not reuse one expecting it to have expired. This applies to creating a ticket, attaching files, and commenting; the three are independent, so the same key can be used once for each. 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 also holds for concurrent retries. The key is recorded in the same transaction as the write, under a lock, so two requests carrying the same key in flight at once produce one write: the second waits for the first and then replays its result.

Errors

StatusErrorMeaning
400invalid_inputBody failed validation, including an unrecognised field.
400project_requiredThe token reaches more than one project; name one.
400invalid_date_rangestartsAt is after dueAt.
400not_a_memberassigneeUserId is not a member of the organisation.
400invalid_agent_repositoryagentRepositoryId is not bound to the project.
400invalid_idempotency_keyEmpty, or longer than 255 characters.
400invalid_attachmentsAn attachmentIds entry is unknown, already claimed, in another project, or uploaded by another token. Nothing is written.
400too_many_attachmentsThe ticket would exceed 10 attachments. Nothing is written.
401invalid_api_tokenMissing, malformed, unknown, or revoked.
403insufficient_scopeThe token lacks tickets:write; the body names it in required.
404ticket_not_foundNo such ticket in the resolved project (attach and comment).
404project_not_foundNo such project, the token cannot reach it, or projectId was omitted and the token reaches none.
404board_not_foundNo such board, or it belongs to another project.
404column_not_foundNo 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

LimitValue
Rate limit, per token60 requests / minute
Upload size25 MB per file
Attachments per ticket10
Attachments per comment10
Uploads, per token60 / hour

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.