Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Sep 14, 2026

Using OAuth 2.0 with a Confidential Client

OAuth 2.0 clients with their client security type set to confidential are suitable for Power-Ups with a backend component or non-Power-Up Trello apps. They are more secure, but you won't be able to take advantage of the Power-Up Client Library's OAuth 2.0 helper functions.

This means the under-the-hood details like PKCE and the authorization code exchange will be things you'll have to handle yourself inside your application. See our OAuth 2.0 client configuration page for more information about public vs confidential OAuth 2.0 clients.

Additionally, if you've used Atlassian's OAuth 2.0 (3LO) before, Trello's OAuth 2.0 is similar, save for a few key differences:

  1. Trello has adopted PKCE for its OAuth 2.0 flow, which adds an extra layer of security.
  2. Trello is not site-based, unlike other Atlassian products like Jira and Confluence, so any fields related to sites can be ignored.

Example Confidential Client App

We've created an example confidential client app for you to clone and learn about how to build with Trello OAuth 2.0. Note that this example app is not a Power-Up, but its concepts also apply to Power-Ups using a confidential OAuth 2.0 client. Follow the instructions in the project's README to get started!

The example project follows these general steps to authorize users with OAuth 2.0:

Step 1: Create your authorization URL

You'll need to first construct the authorization URL to direct your users to the OAuth 2.0 consent screen, in order to generate the authorization code. Combine the authorization base url, https://auth.atlassian.com/authorize with the relevant query parameters:

FieldDescription
client_idThe ID of your OAuth 2.0 client.
scopeYour desired scopes, space separated. Besides the special scope offline_access, the scopes you pass in must match the ones set in your app's OAuth 2.0 client configuration. You must pass in offline_access to receive a refresh token in this flow. For example: "read:member:trello read:board:trello offline_access".
redirect_uriThe URL of the page that will be redirected to after the user presses "Allow". This should match one of the callback URLs set in your OAuth 2.0 client configuration. See this page for more information.
response_typeThis must be set to "code".
promptThis must be set to "consent".
code_challengeThe PKCE code challenge.
code_challenge_methodThis should be set to "S256".

Here is how the example project sets that up, in authorization.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// in the server's /auth route handler (handleAuthorRoute.ts)
export const handleAuthRoute = (req: Request, res: Response) => {
  const params = {
    client_id: CLIENT_ID,
    scope: req.query.scopes.toString(), // e.g. "read:member:trello read:board:trello offline_access"
    redirect_uri: REDIRECT_URI, // e.g. "https://foo-bar.ngrok.dev/authorize-redirect.html"
    response_type: "code",
    prompt: "consent",
    code_challenge: req.query.codeChallenge.toString(),
    code_challenge_method: "S256",
  };
  const queryString = new URLSearchParams(params).toString();
  const url = `https://auth.atlassian.com/authorize?${queryString}`;

  return res.redirect(url);
};

Step 2: Exchange authorization code for tokens

When the user presses "Allow" in the consent screen after clicking on your authorization URL, they will be redirected to the redirect_uri that you passed in. This page will receive the authorization code via a URL parameter called code, for example: https://foo-bar.ngrok.dev/authorize-redirect.html?code=...

You must take that code and exchange it for your access and refresh tokens. The exchange happens by hitting https://auth.atlassian.com/authorize/oauth/token with the correct payload:

FieldDescription
client_idThe ID of your OAuth 2.0 client.
client_secretThe secret of your OAuth 2.0 client. Your client must be set to confidential to have a client secret. Keep this value safe and do not expose it to your users.
grant_typeThis must be set to "authorization_code".
redirect_uriThe URL of the page that will be redirected to after the user presses "Allow". This should match one of the callback URLs set in your OAuth 2.0 client configuration.
codeThe authorization code obtained from the initial authorize call.
code_verifierThe PKCE code verifier. This must be generated along with the initial code challenge.

If the exchange is successful, you will be returned the following in the response:

FieldDescription
access_tokenAn access token which can be used to hit the Trello API. By default, it expires after 1 hour.
refresh_tokenA refresh token which can be exchanged for a new access token and refresh token. By default, it expires after 90 days.
expires_inThe amount of seconds the access token will expire after it's been issued. Default is 3600 (1 hour).
scopeThe scopes that were requested during the initial authorize call.

If there's an error, you will be returned this error object instead:

FieldDescription
errorThe name of the error.
error_descriptionThe description of the error.

Here is how the example app performs the authorization code exchange via its own endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// in the /code-exchange handler (handleCodeExchangeRoute.ts)
export const handleCodeExchangeRoute = async (req: Request, res: Response) => {
  const requestBody = {
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    grant_type: "authorization_code",
    redirect_uri: REDIRECT_URI,
    code: req.body.code.toString(),
    code_verifier: req.body.code_verifier.toString(),
  };

  const response = await fetch(`https://auth.atlassian.com/oauth/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
    body: JSON.stringify(requestBody),
  });

  const responseJson = await response.json();
  res.json(responseJson);
};

Step 3: Make API requests using the access token

Now that you have your access token you can use it to make Trello API requests. Simply pass the token in the Authorization http header as a bearer token. Checkout how the example app achieves this in its client-side code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// in api.ts
const trelloGetRequest = async (path: string) => {
  const accessToken = await getAccessToken();

  const TRELLO_URL = `https://trello.com/${path}`;
  const headers = { Authorization: `Bearer ${accessToken}` };

  const response = await fetch(TRELLO_URL, { headers });

  const responseJson = await response.json();
  return responseJson;

};

export const getMember = async () => {
  const response = await trelloGetRequest("1/members/me?fields=id,username");
  return response;
};

Step 4: Refresh your access token using your refresh token

When your access token expires (the default expiry time is within 1 hour of being issued), you need to use your refresh token to get a new access token and refresh token. Refresh tokens may only be used once and expire after 90 days.

Similar to the code exchange, you must hit https://auth.atlassian.com/authorize/oauth/token with the correct payload:

FieldDescription
client_idThe ID of your OAuth 2.0 client.
client_secretThe secret of your OAuth 2.0 client. Your client must be set to confidential.
grant_typeThis must be set to "refresh_token".
refresh_tokenThe current refresh token. Refresh tokens expire and cannot be used after 90 days.

The response has the same shape as the authorization code exchange step (see above).

Again, here is how the endpoint the example app uses to refresh tokens:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// the /refresh-token handler (handleRefreshTokenRoute.ts)
export const handleRefreshTokenRoute = async (req: Request, res: Response) => {
  const requestBody = {
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    grant_type: "refresh_token",
    refresh_token: req.body.refresh_token,
  };

  const response = await fetch(`https://auth.atlassian.com/oauth/token`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
    body: JSON.stringify(requestBody),
  });

  const responseJson = await response.json();
  res.json(responseJson);

  return;
};

If your app already has users authorized via Trello Auth tokens, you can migrate them to OAuth 2.0 without making them re-authorize by calling the token exchange endpoint from your backend.

The token exchange endpoint accepts a valid Trello Auth token and returns a new set of OAuth 2.0 tokens for the same user. The original Trello Auth token is revoked as part of the exchange.

The original Trello Auth token is revoked as part of the exchange and can no longer be used to call the Trello API. After the exchange, all Trello API requests for that user must use the new OAuth 2.0 access token as a bearer token (Authorization: Bearer {access_token}).

Additionally, for Power-Up OAuth 2.0 clients, the new OAuth 2.0 token is workspace-restricted to the workspace the user is currently in. If your users use your Power-Up across multiple workspaces, only the first workspace can be migrated this way. For every other workspace, you must fall back to the standard authorization flow described above so the user can consent to that workspace.

Endpoint

1
2
POST https://api.trello.com/1/token/exchange

Authentication

The request must be authenticated with the user's existing Trello Auth token using one of the standard authorization methods (query parameters, Authorization header, or request body).

Request body

FieldTypeRequiredDescription
scopesstring[]YesThe OAuth 2.0 scopes to request for the new tokens. These must be valid scopes configured on your OAuth 2.0 client and must not exceed the permissions of the Trello Auth token being exchanged. The offline_access scope is added automatically.
idOrganizationstringRequired for Power-UpsThe ID of the Trello workspace. This is required because Power-Up OAuth 2.0 clients are workspace-restricted.

Response

On success, the endpoint returns a JSON object:

FieldDescription
access_tokenAn OAuth 2.0 access token which can be used to hit the Trello API. By default, it expires after 1 hour.
refresh_tokenA refresh token which can be exchanged for a new access token and refresh token. By default, it expires after 90 days.
id_tokenAn OpenID Connect ID token containing information about the authenticated user.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const response = await fetch("https://api.trello.com/1/token/exchange", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `OAuth oauth_consumer_key="${API_KEY}", oauth_token="${TRELLO_AUTH_TOKEN}"`,
  },
  body: JSON.stringify({
    scopes: ["read:board:trello", "write:board:trello"],
    idOrganization: WORKSPACE_ID,
  }),
});

const { access_token, refresh_token, id_token } = await response.json();

Errors

Status CodeCause
400Requested scopes exceed the permissions granted by the Trello Auth token.
401The Trello Auth token is invalid, or the user does not have a linked Atlassian account.
403The Trello Auth API key is not linked to the Power-Up, or the Power-Up does not have an OAuth 2.0 client configured.

After the exchange

Once you have your OAuth 2.0 tokens, use them the same way as tokens obtained through the standard OAuth 2.0 authorization flow (see Step 3: Make API requests using the access token and Step 4: Refresh your access token using your refresh token above).

Rate this page: