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.
Workspace owners and admins
Tip
Register an application
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.Warning
You can change the logo, name, description, redirect URIs and default scopes later from the application's page.
Install it
An application can't do anything until it's installed. You can only install an application in the workspace that registered it.
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.
code_verifier (43 to 128 characters) and its S256 code_challenge, plus a random state.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.redirect_uri with code and state. Check that state matches.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
| Scope | Grants |
|---|---|
workspace:read | Workspace details and members |
projects:read | Projects, their members, sections and bookmarks, the roadmap, and read access to tasks, comments, sprints, workflows, labels, attachments and the timeline |
issues:read | Tasks, comments, sprints, workflow statuses, labels, attachments, members and resource allocations |
issues:write | Everything 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:read | Milestones and timeline snapshots |
timeline:write | Everything 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.
https.A webhook only sends events the installation can see: events in projects it has access to, when it holds the scope the event needs.
| Event | Sent when | Needs |
|---|---|---|
issue.created | A task is created | issues:read |
issue.updated | A task changes | issues:read |
issue.status_changed | A task moves to a different status (sent alongside issue.updated) | issues:read |
issue.assigned | Someone is assigned to a task | issues:read |
comment.created | A comment is posted on a task | issues: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"]
}
}ididentifies the event. It stays the same if the delivery is retried or redelivered, so use it to ignore duplicates.actor.typeisappwhen an application made the change.data.issueis the full task, as the API returns it.issue.updatedlists thechangedfields.issue.status_changedincludesprevious_status_id, andissue.assignedincludesadded_assignee_ids.comment.createdincludesdata.issue_id,data.issue_codeanddata.comment.
Headers
| Header | Value |
|---|---|
X-Trellis-Event | The event type, such as issue.created |
X-Trellis-Delivery | A unique ID for this delivery. A redelivery gets a new one |
X-Trellis-Timestamp | When this attempt was sent, in Unix seconds |
X-Trellis-Signature | sha256= 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.
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.403on a project: the installation isn't allowed to access that project.429: over the rate limit. Wait forRetry-Afterseconds.
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
| Action | Owner | Admin | Editor | Member | Contributor | Viewer |
|---|---|---|---|---|---|---|
| Register, edit and delete applications | ||||||
| Install, edit access and uninstall applications | ||||||
| Manage webhooks and redeliver events |