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.

On this page

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.

  1. Create a personal token

    In SettingsAPI & MCP, choose Create token and copy it. It is shown once.
  2. 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 as X-Api-Key and the rest as X-Api-Secret.
  3. Call the API

    The base URL is https://api.ontrama.com.
Terminal
# 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.

CredentialSend it asLastsCan do
Personal tokenX-Api-Key + X-Api-SecretUntil you replace itEverything you can do in the app
OAuth access tokenAuthorization: Bearer lxt_at_…7 days, then refreshedOnly the operations its scopes allow
JWT from e-mail loginAuthorization: Bearer <jwt>24 hoursEverything 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.

Shown once, valid until replaced

Trama keeps only a fingerprint of the secret, so a lost token can't be shown again: replace it instead. Replacing it stops the old one right away, for every script and MCP client that uses it. OAuth connections are not affected.

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.

Terminal
# 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 429 and Retry-After.
  • POST /logout ends 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.

EndpointURL
Server metadataGEThttps://api.ontrama.com/.well-known/oauth-authorization-server
Register a clientPOSThttps://api.ontrama.com/oauth2/register
Consent (in the browser)https://ontrama.com/oauth2/authorize
TokenPOSThttps://api.ontrama.com/oauth2/token
RevokePOSThttps://api.ontrama.com/oauth2/revoke

Three scopes. A client that asks for none gets all three, and you see them on the consent page.

ScopeAllows
boards.readView boards, columns and tasks you can already access, and search them.
boards.writeCreate tasks, move cards and change details, assignees, labels and relations on boards you can edit.
comments.writeComment on tasks you can edit.

OAuth tokens only reach what MCP uses

On the REST API an OAuth token works only on the operations below, and only with the matching scope. Anything else answers 403 with {"error": "missing_scope"}. Scopes never widen your role: a token can only edit boards you can edit.

OperationScope
GET/boardsboards.read
GET/boards/{id}boards.read
GET/boards/{id}/searchboards.read
GET/searchboards.read
GET/boards/{board_id}/tasks/{id}boards.read
POST/boards/{board_id}/tasksboards.write
PUT/boards/{board_id}/tasks/{id}boards.write
PUT…/tasks/{id}/move, /task_users, /taggingsboards.write
POST · DELETE…/tasks/{task_id}/relationsboards.write
POST…/tasks/{task_id}/commentscomments.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 .json suffix.
  • Tasks have a numeric id and an issue key like ACME-42. Routes that take {id} accept either; nested routes ({task_id}) take the numeric id.
  • Errors: 401 no or bad credentials, 403 your role doesn't allow it, 404 not found or not visible to you, 422 validation ({"field": ["message"]} or {"error": "…"}), 429 rate limited, with Retry-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}.

AreaRoutes
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
Terminal
# 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 pass insert_after_task_id.
  • Priority. priority_none, priority_low, priority_medium, priority_high or priority_urgent. Anything else is a 422.
  • Dates and milestones. start_at and due_at (start on or before due), and an optional milestone_id from /boards/{board_id}/milestones on 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.

Terminal
# 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, not 403, 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.

  • BoardChannel with a board_id sends the whole board after a change, or a small task_reordered message when only a card's position changed. Subscribing to a board you can't see is rejected.
  • NotificationChannel sends new_notification messages for the signed-in user.
board.js
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.

  1. 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 is POST …/tasks/{task_id}/pull_requests.
  2. 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.
  3. Mention issue keys

    When a pull request in a connected repository mentions an issue key such as ACME-42 in its title, description or branch name, Trama links it to that task. One PR can link several tasks.
  4. 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 mergedMoves to the Done column, which also unblocks the tasks it was blocking
Is closed without mergingStays where it is
Gets a review or a CI resultKeeps 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.

Terminal
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"}'

Chat, too

Connect a GRUPIM server to post task updates to a channel and create tasks from slash commands. See the GRUPIM guide.

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.