Applications

Build your own integrations with the Trellis API and webhooks.

An Application is an integration your team builds and runs: a script that syncs tasks from another tool, a service that posts updates to your chat, or a dashboard that tracks delivery. You register it in Trellis and install it in your workspace. It then calls the Trellis API with its own identity and can receive webhooks when things change.

Each installed Application gets its own app user in your workspace. Everything it does, such as creating a task or posting a comment, shows up as that app, not as the person who installed it. Its access is exactly what an admin grants: a set of scopes, and optionally a limited set of projects.

Register an application

1
Open Settings → Applications and select New application.
2
Give it a name (up to 255 characters) and, optionally, a description (up to 2,000 characters). Admins see these when installing or reviewing it.
3
Optionally, select Upload logo and choose a PNG or JPEG of up to 1 MB and 1024×1024 pixels. It shows in your Applications list and on the consent screen.
4
Add at least one redirect URI. It must use https; http is only accepted for localhost, 127.0.0.1 and ::1. You only use redirect URIs with the connect flow, but every application needs one.
5
Choose the API scopes it asks for by default. Pick No access, Read or Read/Write for each resource.
6
Select Create application. Copy the client ID and client secret.

You can change the logo, name, description, redirect URIs and default scopes later from the application's page.

Registering a new application with a logo, a redirect URI and API scopes.

Install it

An application can't do anything until it's installed. You can only install an application in the workspace that registered it.

1
In Settings → Applications, select Install on the application's row, or on its page.
2
Trellis opens the consent screen, showing the workspace it's Installing into. Review the permissions, which come from the application's API scopes.
3
Under Projects, choose All projects to allow every project, including ones created later, or Only selected projects and pick each one from Select project.
4
Select Install. Trellis creates the application's app user and it's ready to get tokens.

To change what an installed application can do, update its API scopes and save, then select Configure (on its row or its page) and confirm on the consent screen. If you remove a scope or a project, every token it holds stops working straight away, and it has to request new ones. Adding access doesn't affect existing tokens.

Uninstall revokes its tokens, removes its app user from the workspace and cancels webhook deliveries that haven't been sent. You can install it again later. Delete does the same and also removes its client credentials, webhooks and delivery history for good.

Get an access token

Client credentials

Once an application is installed, it gets access tokens with its client ID and secret. There's no browser step.

curl -s https://trellisapp.co.uk/oauth/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials
{
  "access_token": "tr_oat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "issues:write workspace:read"
}

Access tokens last one hour. No refresh token is issued: when a token expires, request a new one the same way. To get a token with less access than the installation has, pass scope, for example -d scope=issues:read. You can't ask for more than the installation was granted.

You can send the credentials with HTTP Basic auth, as above, or as client_id and client_secret form fields.

Connect flow

If your application has its own sign-in, you can give admins a Connect to Trellis button instead of asking them to install it from Settings. This is the standard OAuth 2.1 authorization code flow with PKCE.

1
Generate a random code_verifier (43 to 128 characters) and its S256 code_challenge, plus a random state.
2
Send the admin to https://trellisapp.co.uk/oauth/authorize with response_type=code, client_id, redirect_uri (one you registered), scope (space-separated, optional), state, code_challenge and code_challenge_method=S256.
3
The admin signs in, reviews the scopes, optionally limits the projects, and approves. Approving installs the application, or updates its access if it's already installed.
4
Trellis redirects back to your redirect_uri with code and state. Check that state matches.
5
Exchange the code within 10 minutes by posting grant_type=authorization_code, code, redirect_uri and code_verifier to /oauth/token, authenticated with your client ID and secret.

This returns an access token and a refresh token. Refresh tokens last 90 days and rotate: each refresh (grant_type=refresh_token with refresh_token) returns a new pair and revokes the old refresh token and its access token. Reusing an old refresh token revokes the whole chain.

If the admin denies access, the redirect carries error=access_denied instead of a code.

Revoking tokens

Post token to https://trellisapp.co.uk/oauth/revoke, authenticated with your client ID and secret. Revoking a refresh token also revokes the access tokens issued from it.

Call the API

Send the access token as a Bearer token. API paths start with https://trellisapp.co.uk/api/v1/workspace/{handle}, where {handle} is your workspace's handle.

curl -s https://trellisapp.co.uk/api/v1/workspace/acme/app \
  -H "Authorization: Bearer $ACCESS_TOKEN"

GET /workspace/{handle}/app describes the token: the application and installation IDs, the app user's ID, the workspace, the scopes the token can use, and the projects it's limited to. An empty project_ids means all projects. It works with any scope, so it's a good first call to check a token.

The app user has the workspace Member role, and it's a member of every project it's allowed to access. Admin-only endpoints aren't available to applications, and project endpoints return 403 for projects outside the installation.

App users don't take a seat, can't sign in, and don't receive notifications. They're left out of member lists and people pickers, so they don't show up on the Members page or in resourcing.

Scopes

ScopeGrants
workspace:readWorkspace details and members
projects:readProjects, their members, sections and bookmarks, the roadmap, and read access to tasks, comments, sprints, workflows, labels, attachments and the timeline
issues:readTasks, comments, sprints, workflow statuses, labels, attachments, members and resource allocations
issues:writeEverything in issues:read, plus creating, updating, archiving and deleting tasks and dependencies; comments, reactions and inline comments; sprints and moving tasks between them; attachments; and resource allocations
timeline:readMilestones and timeline snapshots
timeline:writeEverything in timeline:read, plus creating, updating and deleting milestones, and creating, restoring and deleting snapshots

Time logs and timesheet reporting, quotes, rate cards, budgets and profitability, packages, leave and workspace settings aren't available to applications. Neither is managing applications themselves. The timelogs scopes are only for MCP connections.

Webhooks

Webhooks tell your application when something happens, so it doesn't have to poll. Add them from the Webhooks section on the application's page.

1
Select Add webhook and enter the payload URL. It must be publicly reachable over https.
2
Choose the events to send.
3
Copy the signing secret. It's only shown once; to get a new one, select Rotate secret from the webhook's three-dot menu.

A webhook only sends events the installation can see: events in projects it has access to, when it holds the scope the event needs.

EventSent whenNeeds
issue.createdA task is createdissues:read
issue.updatedA task changesissues:read
issue.status_changedA task moves to a different status (sent alongside issue.updated)issues:read
issue.assignedSomeone is assigned to a taskissues:read
comment.createdA comment is posted on a taskissues:read

Payload

Each delivery is a POST with a JSON body:

{
  "id": "0b6f3c1e-…",
  "type": "issue.updated",
  "created_at": "2026-09-28T10:15:00Z",
  "workspace_id": "…",
  "project_id": "…",
  "actor": { "type": "user", "id": "…", "name": "Sam Taylor" },
  "data": {
    "issue": { "id": "…", "code": "WR-142", "title": "…" },
    "changed": ["status", "assignee"]
  }
}
  • id identifies the event. It stays the same if the delivery is retried or redelivered, so use it to ignore duplicates.
  • actor.type is app when an application made the change.
  • data.issue is the full task, as the API returns it.
  • issue.updated lists the changed fields. issue.status_changed includes previous_status_id, and issue.assigned includes added_assignee_ids.
  • comment.created includes data.issue_id, data.issue_code and data.comment.

Headers

HeaderValue
X-Trellis-EventThe event type, such as issue.created
X-Trellis-DeliveryA unique ID for this delivery. A redelivery gets a new one
X-Trellis-TimestampWhen this attempt was sent, in Unix seconds
X-Trellis-Signaturesha256= followed by the hex HMAC of the request

Verify the signature

The signature is an HMAC-SHA256 of {timestamp}.{raw body}, using the whole signing secret (including its whsec_ prefix) as the key. Compute it over the raw request body, before parsing the JSON.

import crypto from "node:crypto";
 
function isValidTrellisWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-trellis-timestamp"];
  const signature = headers["x-trellis-signature"] ?? "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  return (
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  );
}

We recommend rejecting timestamps more than five minutes old, as above, to stop old requests being replayed.

Delivery and retries

Respond with any 2xx status within 15 seconds to confirm a delivery. Anything else, including a redirect or a timeout, counts as a failure. Failed deliveries are retried after 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 8 hours and 8 hours: ten attempts over about a day. After that the delivery is marked Failed.

Deliveries can arrive out of order, especially when some are being retried. Use each event's created_at, or fetch the latest state from the API, rather than relying on arrival order.

Select Deliveries from a webhook's three-dot menu to see recent attempts, their status, the response code and the payload. Redeliver sends the same event again as a new delivery. The same menu has Edit, Rotate secret and Delete.

To pause a webhook, turn off the switch on its row and confirm. Pausing a webhook stops new events and cancels deliveries waiting to retry. When you resume it, cancelled deliveries aren't resent automatically; you can redeliver them from the log. Deliveries are also cancelled if the application is uninstalled, or loses the scope or project access an event needs.

Pausing a webhook and opening its deliveries from the three-dot menu.

Rate limits

Each installation can make 300 API requests per minute. Requests over the limit return 429 with a Retry-After header. The /oauth/token, /oauth/revoke and /oauth/introspect endpoints are limited to 60 requests per minute from each IP address, so cache access tokens until they expire rather than requesting one per API call.

Errors

API errors return JSON with error and message fields. Token and scope problems also set a WWW-Authenticate header.

  • 401 invalid_token: the token is missing, expired or revoked, or the application was uninstalled or deleted. Get a new token.
  • 403 insufficient_scope: the installation hasn't been granted access to that endpoint, or the endpoint isn't available to applications.
  • 403 on a project: the installation isn't allowed to access that project.
  • 429: over the rate limit. Wait for Retry-After seconds.

The token endpoint returns standard OAuth errors: invalid_client for a wrong client ID or secret, unauthorized_client if the application isn't installed, invalid_scope if you ask for a scope it wasn't granted, and invalid_grant for an expired or already-used code or refresh token.

Activity

Changes an application makes appear in Activities under the application's name. Installing an application, or changing its access, is also recorded.

Who can do what

ActionOwnerAdminEditorMemberContributorViewer
Register, edit and delete applications
Install, edit access and uninstall applications
Manage webhooks and redeliver events