Developer guide
Build on Trama
The Trama API is the JSON API the web app runs on: workspaces, boards, tasks, comments and integrations. This guide covers signing in, the parts most integrations need and how GitHub, GitLab and GRUPIM connect. Every operation is in the OpenAPI spec.
Quickstart
The quickest way in is a personal token. It acts as you, on everything you can reach in the app, so keep it out of code you share.
Create a personal token
In SettingsAPI & MCP, choose Create token and copy it. It is shown once.Split it into a key and a secret
The token is your API key and secret joined by a dot. Send the part before the first dot asX-Api-Keyand the rest asX-Api-Secret.Call the API
The base URL ishttps://api.ontrama.com.
# Your personal token is KEY.SECRET: split it at the first dot
TOKEN="YOUR_PERSONAL_TOKEN"
export TRAMA_KEY="${TOKEN%%.*}"
export TRAMA_SECRET="${TOKEN#*.}"
# Who am I?
curl https://api.ontrama.com/me \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET"
# The boards I can see
curl https://api.ontrama.com/boards \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET"Authentication
Every endpoint except login, the health check and the OAuth server needs one of three credentials. They all act as the same user and reach the same boards; they differ in how long they last and in what they may do.
| Credential | Send it as | Lasts | Can do |
|---|---|---|---|
| Personal token | X-Api-Key + X-Api-Secret | Until you replace it | Everything you can do in the app |
| OAuth access token | Authorization: Bearer lxt_at_… | 7 days, then refreshed | Only the operations its scopes allow |
| JWT from e-mail login | Authorization: Bearer <jwt> | 24 hours | Everything you can do in the app |
Personal token
Scripts and server-side integrations should use a personal token. Create or replace it in SettingsAPI & MCP. The same token also works as a Bearer token on the MCP server, which splits it for you; the REST API only takes the two headers.
When X-Api-Key is present it is the only credential the API looks at: a wrong pair answers 401 even if a valid Authorization header is sent too.
E-mail login (JWT)
The web app signs in with a code sent by e-mail. Each step is a POST /login: first with the address, then with the address and the code.
# 1. Ask for a code (a 400 answer here means the e-mail was sent)
curl -X POST https://api.ontrama.com/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
# 2. Exchange the 6-digit code for a JWT (201: {"user": {…}, "token": "…"})
curl -X POST https://api.ontrama.com/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "otp": "123456"}'
# 3. Use it
curl https://api.ontrama.com/me -H "Authorization: Bearer YOUR_JWT"- Codes have 6 digits, last 10 minutes and work once. Only the latest code counts; five wrong tries lock it.
- Login is rate limited to 30 requests per IP and 10 per address every 15 minutes, answered with
429andRetry-After. POST /logoutends every JWT issued to the account until then, on all devices. Personal tokens keep working.
OAuth 2.1
Trama is an OAuth 2.1 authorization server, built for MCP clients: dynamic client registration (RFC 7591), server metadata (RFC 8414), protected-resource metadata (RFC 9728) and the authorization code flow with PKCE (S256). Clients are public (token_endpoint_auth_method: none). Access tokens (lxt_at_…) last 7 days; refresh tokens (lxt_rt_…) rotate on every use. An MCP client does all of this on its own; the MCP guide walks through the flow.
| Endpoint | URL |
|---|---|
| Server metadata | GEThttps://api.ontrama.com/.well-known/oauth-authorization-server |
| Register a client | POSThttps://api.ontrama.com/oauth2/register |
| Consent (in the browser) | https://ontrama.com/oauth2/authorize |
| Token | POSThttps://api.ontrama.com/oauth2/token |
| Revoke | POSThttps://api.ontrama.com/oauth2/revoke |
Three scopes. A client that asks for none gets all three, and you see them on the consent page.
| Scope | Allows |
|---|---|
boards.read | View boards, columns and tasks you can already access, and search them. |
boards.write | Create tasks, move cards and change details, assignees, labels and relations on boards you can edit. |
comments.write | Comment on tasks you can edit. |
| Operation | Scope |
|---|---|
| GET/boards | boards.read |
| GET/boards/{id} | boards.read |
| GET/boards/{id}/search | boards.read |
| GET/search | boards.read |
| GET/boards/{board_id}/tasks/{id} | boards.read |
| POST/boards/{board_id}/tasks | boards.write |
| PUT/boards/{board_id}/tasks/{id} | boards.write |
| PUT…/tasks/{id}/move, /task_users, /taggings | boards.write |
| POST · DELETE…/tasks/{task_id}/relations | boards.write |
| POST…/tasks/{task_id}/comments | comments.write |
You can see and revoke the apps you connected in SettingsConnected apps.
Conventions
- Base URL
https://api.ontrama.com. Requests and responses are JSON; paths work with or without a.jsonsuffix. - Tasks have a numeric
idand an issue key likeACME-42. Routes that take{id}accept either; nested routes ({task_id}) take the numeric id. - Errors:
401no or bad credentials,403your role doesn't allow it,404not found or not visible to you,422validation ({"field": ["message"]}or{"error": "…"}),429rate limited, withRetry-After. - Rate limits: login (above), OAuth client registration 20 per IP per hour, upload presigns 60 per user per hour, invitations 30 per user per hour.
- The spec at /openapi.yaml is the contract. The API reference renders it with a console to try requests.
Endpoints
The main resources at a glance. Board-level routes start at /boards/{board_id}.
| Area | Routes |
|---|---|
| Account | GET · PUT/meGET/me/tasksGET/me/api_credentialsGET/me/oauth_clients /me/tasks lists the open issues assigned to you, across boards. |
| Search | GET/search Pass q, and board_id to search one board. Covers boards, tasks and comments you can see; 50 results at most. |
| Notifications | GET/notificationsGET/notifications/unread_countPUT/notifications/mark_all_readGET · PUT/notification_preferences |
| Workspaces | GET · POST/workspacesGET · PUT/workspaces/{id}…/workspace_users…/workspace_documents…/workspace_activities…/integrations |
| Boards | GET · POST/boardsGET · PUT · DELETE/boards/{id}PUT/boards/{id}/archivePOST/boards/{id}/share GET /boards/{id} is the whole kanban: columns, cards and templates. |
| Board parts | …/board_columns…/tags…/milestones…/board_users |
| Tasks | POST/boards/{board_id}/tasksGET · PUT · DELETE…/tasks/{id}PUT…/movePUT…/task_usersPUT…/taggingsPUT…/stopwatch |
| Inside a task | …/comments…/task_checklists…/task_check_items…/relations…/sub_issues…/pull_requests…/attachments |
# Create a task at the end of a column
curl https://api.ontrama.com/boards/41/tasks \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET" \
-H "Content-Type: application/json" \
-d '{"task": {"board_column_id": 310, "name": "Write the release notes",
"priority": "priority_high"}}'
# Open a task by its issue key
curl https://api.ontrama.com/boards/41/tasks/ACME-42 \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET"
# Move it to another column, right after card 7090
curl -X PUT https://api.ontrama.com/boards/41/tasks/ACME-42/move \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET" \
-H "Content-Type: application/json" \
-d '{"to_board_id": 41, "to_column_id": 312, "insert_after_task_id": 7090}'
# Comment (bodies are HTML)
curl https://api.ontrama.com/boards/41/tasks/7104/comments \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET" \
-H "Content-Type: application/json" \
-d '{"body": "<p>Shipped in 2.4</p>"}'Concepts
Issue keys
Every task gets a key from its workspace's prefix and a running number, like ACME-42. Keys stay put when cards move between columns and boards of the workspace; moving a task to another workspace gives it a new one. Mentioning a key in a description or a comment links the two tasks as related, when you can edit the other board.
- Ordering. Cards in a column are ordered by a fractional
sort_key. New and moved cards go to the end unless you passinsert_after_task_id. - Priority.
priority_none,priority_low,priority_medium,priority_highorpriority_urgent. Anything else is a422. - Dates and milestones.
start_atanddue_at(start on or before due), and an optionalmilestone_idfrom/boards/{board_id}/milestoneson the same board. They drive the board's planning view.
Sub-issues & relations
A task can have a parent (parent_id): same workspace, up to 8 levels deep, no cycles, no templates. Create children with POST …/tasks/{task_id}/sub_issues, sending name, a list of names, or existing_task_id to adopt an issue that already exists. Reorder them with PUT …/tasks/{id}/reorder_sub_issues.
Relations are related, blocks, blocked_by and duplicate, created with POST …/tasks/{task_id}/relations. When a blocking task is done, its blocks links turn into related. The other task has to be on a board you can edit.
# Break a task into sub-issues
curl https://api.ontrama.com/boards/41/tasks/7104/sub_issues \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET" \
-H "Content-Type: application/json" \
-d '{"names": ["Draft the copy", "Review with legal"]}'
# Mark it as blocked by another issue (id or key)
curl https://api.ontrama.com/boards/41/tasks/7104/relations \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET" \
-H "Content-Type: application/json" \
-d '{"related_task_id": "ACME-7", "relation_type": "blocked_by"}'Templates
A task with is_template: true stays out of the columns and is listed under templates in the board payload; it has no issue key. Create a task from it with template_id on POST /boards/{board_id}/tasks: the name, description, priority, labels and checklists are copied (items unchecked). Assignees, dates, comments and attachments are not.
Who can see what
- You can see a board if you are one of its members, an admin of its workspace, a workspace member and the board is visible to the workspace, or signed in at all when the board is public (by link). Everything inside a board you can see is readable.
- Editing needs a member, admin or owner role on the board, or workspace admin. Managing it (members, sharing, visibility, archive, delete) needs admin or owner.
- A board you can't see answers
404, not403, so its existence doesn't leak.
Realtime
Boards and notifications update live over ActionCable at wss://api.ontrama.com/cable. The socket authenticates with a JWT only, as ?token= or a Bearer header; personal tokens and OAuth tokens are not accepted there.
BoardChannelwith aboard_idsends the whole board after a change, or a smalltask_reorderedmessage when only a card's position changed. Subscribing to a board you can't see is rejected.NotificationChannelsendsnew_notificationmessages for the signed-in user.
import { createConsumer } from "@rails/actioncable";
const cable = createConsumer(`wss://api.ontrama.com/cable?token=${jwt}`);
cable.subscriptions.create(
{ channel: "BoardChannel", board_id: 41 },
{
received(data) {
const message = JSON.parse(data);
// Either the whole board (same shape as GET /boards/41), or
// { type: "task_reordered", task_id, board_column_id, sort_key }
},
},
);GitHub & GitLab
Link pull requests and merge requests to tasks, and let them move cards as they progress. A task shows each linked PR with its status, review state and CI result.
Paste a link (works anywhere)
Paste a github.com pull request or gitlab.com merge request URL on any task. Nothing to set up; the badge updates once the workspace is connected. Through the API it isPOST …/tasks/{task_id}/pull_requests.Connect the workspace
A workspace admin opens Workspace settingsIntegrations and connects GitHub (installs the Trama GitHub App on the repositories you choose) or GitLab (authorize, then turn on the projects to watch; turning one on adds its merge-request webhook). GitHub repositories start on, GitLab projects start off.Mention issue keys
When a pull request in a connected repository mentions an issue key such asACME-42in its title, description or branch name, Trama links it to that task. One PR can link several tasks.Pick the review and done columns
On the board, open a column's menu and choose Use this column as the board’s…In review, and the same for Done.
| When the pull request… | The card… |
|---|---|
| Is opened or reopened (drafts too) | Moves to the In review column |
| Is merged | Moves to the Done column, which also unblocks the tasks it was blocking |
| Is closed without merging | Stays where it is |
| Gets a review or a CI result | Keeps its column; the badge shows approved / changes requested and the latest check |
Cards move only when the status changes, not on every push, and only when the board has that column set. Moves are made as the admin who connected the integration, who needs edit rights on the board.
curl https://api.ontrama.com/boards/41/tasks/7104/pull_requests \
-H "X-Api-Key: $TRAMA_KEY" -H "X-Api-Secret: $TRAMA_SECRET" \
-H "Content-Type: application/json" \
-d '{"url": "https://github.com/acme/website/pull/99"}'Keep going
Using an AI agent? The MCP server gives Claude, Cursor and other clients the same API as tools, with OAuth sign-in. Missing an endpoint in this guide? The spec has all of them.