Skip to content
397 changes: 397 additions & 0 deletions guides/build-plane-app.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,397 @@
---
title: Build a Plane App (Beta)
sidebarTitle: Build a Plane App (Beta)
description: Step-by-step development guide to build and integrate an app with Plane using OAuth-based authentication and authorization workflow.
---

<Note>Plane apps are currently in **Beta**. Please send any feedback to support@plane.so.</Note>

## Introduction
Plane apps seamlessly integrate tools and services with Plane so you can
use them without ever leaving your Workspace. Apps are conveniently available
from our [marketplace](https://plane.so/marketplace/integrations), helping you
stay focused and productive.

## Why Build a Plane App?

**Stop doing manual work.**
Plane integrations eliminate repetitive tasks like copying updates between
tools, creating work items from support tickets, and generating status reports.
Instead of spending hours on administrative work, let your app handle it
automatically.

**Connect everything you already use.**
Your team probably uses dozens of different tools. Plane apps create a unified
workflow by connecting your favorite CRM, time tracking app, CI/CD pipelines,
communication tools, and more, together into Plane. One change in Plane can
trigger updates across your entire tech stack.

**Build exactly what you need.**
Unlike rigid SaaS platforms, Plane's open core nature means you can create
integrations that fit your specific workflow.

## Prerequisites

- A [Plane](https://app.plane.so) workspace
- Admin access to your workspace settings
- Familiarity with OAuth 2.0 concepts (authorization code flow)
- A backend server to handle OAuth token exchange

## High-Level Workflow

1. [Register your app on Plane developer portal](/guides/build-plane-app/#registering-your-app)
2. [Implement OAuth authorization code flow](/guides/build-plane-app#authentication-setup)
3. Obtain and store access tokens securely
4. Make authenticated API requests to Plane
5. Handle token refresh
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated

## Registering Your App

To build an OAuth application with Plane:

1. Navigate to `https://app.plane.so/<workspace_slug>/settings/applications/`.
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated
2. Click on the **Build your own** button.
3. Fill out the form with the required details:

- **Redirect URIs**: Provide the URIs where Plane will send the authorization code.
Copy link
Copy Markdown
Contributor

@aheckmann aheckmann Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **Redirect URIs**: Provide the URIs where Plane will send the authorization code.
- **Redirect URIs**: An allow-list of your app URIs to which Plane is permitted to redirect with your authorization codes during User Authorized Actions and Content URL Generation flows. Plane will not send your authorization codes to any other URIs.

We also need to demonstrate in this guide an example of an app redirect endpoint, what data it should expect to receive and how, and any expectations we have on them (should they respond with HTTP status 200, 204, etc)

- **Contact Details**: Add your email or other contact information.
- **Webhook URL Endpoint**: Update this with your service's webhook endpoint. Plane will send webhooks for all changes that happen in the installed workspace.
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated
- **Organization Details**: Include contact email, privacy policy URL, terms of service URL, and any other relevant information. This helps Plane validate and approve your application for listing in the marketplace.
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated

4. If you're building an agent (with or without using Plane's ADK) capable of performing operations when assigned or mentioned, enable the **Is Mentionable** checkbox during app creation.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We haven't talked about agents before this section. Do we need to use this term here?
  • Are agents the only type of app which is mentionable? My instinct tells me agents are just apps so all should be mentionable.
  • What does mentionable mean?
  • Are apps not otherwise mentionable if this box is checked? Why do developers need to check this box? Mentioning apps in Slack / Github is pretty common, before AI came around. Seems like this should always be enabled and we would remove this option from the UI.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We enabled Is Mentionable by default. This will result in all apps coming in assignees and other user fields. It'll be noise if there are too many apps in the assignees/members list.
We took inspiration from other products in similar to space to control this using a flag. Which will be moved to a scope in future once they're available.

5. Once the app is created, securely store the **Client ID** and **Client Secret**. You will need these credentials to interact with Plane's API during the OAuth flow and for making authenticated API requests.
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated

Comment thread
Prashant-Surya marked this conversation as resolved.
## Authentication Setup

### Generating Consent URL

Before handling authentication, if your app manages installation, you must generate the consent (authorization) URL to initiate the OAuth flow. Below are sample implementations:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if your app manages installation

We need to define what managing installation means before we reference it. When I read this, I wonder if I missed something and I scroll back up to scan for anything that might be related to managing installation.

  • What does managing installation mean?
  • When would I need or not need to manage installation?

If generating a consent URL is optional, I recommend moving this section later in the guide, to a location next to where the consent URL is required.


<Tabs>
<Tab title="Python">

```python
import os
from urllib.parse import urlencode

params = {
"client_id": os.getenv("PLANE_CLIENT_ID"),
"response_type": "code",
"redirect_uri": os.getenv("PLANE_REDIRECT_URI"),
# Optional: include state if needed
}

consent_url = f"https://api.plane.so/auth/o/authorize-app/?{urlencode(params)}"
```
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated

</Tab>
<Tab title="TypeScript">

```typescript
import { URLSearchParams } from 'url';

const params = new URLSearchParams({
client_id: process.env.PLANE_CLIENT_ID!,
response_type: "code",
redirect_uri: process.env.PLANE_REDIRECT_URI!,
// Optional: include state if needed
});

const consentUrl = `https://api.plane.so/auth/o/authorize-app/?${params.toString()}`;
```

</Tab>
</Tabs>

There are two types of authenticated actions your application can perform:

1. **User-authorized actions**: Actions performed on behalf of a user after they grant permission to your app via OAuth.
2. **App-authorized actions**: Actions that the app can perform independently within the workspace where it is installed (such as responding to webhooks or automation triggers).

We will describe how to configure and use each type in the following sections.

### App-Authorized Actions (Client Credentials Flow)

When the app is installed, Plane will send an `app_installation_id` as part of the callback to the `redirect_uri` provided during consent URL generation. You can use this `app_installation_id` to request a bot token for your app.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the other flow, we tell the developer to securely store the returned token. Let's do that here too.


#### Examples

<Tabs>
<Tab title="Python">

Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated
```python
import base64
import requests

# Prepare basic auth header using client_id and client_secret
client_id = "your_client_id"
client_secret = "your_client_secret"
basic_auth = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()

# Prepare request data
payload = {
"grant_type": "client_credentials",
"app_installation_id": app_installation_id
}

# Make a POST request to fetch bot token
response = requests.post(
url="https://api.plane.so/auth/o/token/",
headers={"Authorization": f"Basic {basic_auth}",
"Content-Type": "application/x-www-form-urlencoded"},
data=payload
)

# Parse the response
response_data = response.json()
bot_token = response_data['access_token']
expires_in = response_data["expires_in"]
```

</Tab>
<Tab title="TypeScript">

```typescript
import axios from 'axios';

// Prepare basic auth header using client_id and client_secret
const clientId = "your_client_id";
const clientSecret = "your_client_secret";
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');

// Prepare request data
const payload = {
grant_type: "client_credentials",
app_installation_id: appInstallationId
};

// Make a POST request to fetch bot token
const response = await axios.post(
"https://api.plane.so/auth/o/token/",
payload,
{
headers: {
Authorization: `Basic ${basicAuth}`,
"Content-Type": "application/x-www-form-urlencoded"
}
}
);

// Parse the response
const responseData = response.data;
const botToken = responseData.access_token;
const expiresIn = responseData.expires_in;
```

</Tab>
</Tabs>

### User-Authorized Actions (Authorization Code Flow)

In this flow, your app exchanges the `code` received as a query parameter on the callback (to your `redirect_uri`) for an access token and refresh token. The access token is short-lived and must be refreshed using the refresh token when it expires. Both tokens should be securely stored.

#### Examples

<Tabs>
<Tab title="Python">

```python
Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated
import requests

# Exchange authorization code for access and refresh tokens
code = "authorization_code_from_callback"
client_id = "your_client_id"
client_secret = "your_client_secret"
redirect_uri = "your_redirect_uri"

payload = {
"grant_type": "authorization_code",
"code": code,
"client_id": client_id,
"client_secret": client_secret,
"redirect_uri": redirect_uri
}

response = requests.post(
url="https://api.plane.so/auth/o/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=payload
)

# Parse the response
response_data = response.json()
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
expires_in = response_data["expires_in"]

# When access token expires, use refresh token to get a new access token
refresh_payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret
}

refresh_response = requests.post(
url="https://api.plane.so/auth/o/token/",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=refresh_payload
)

# Parse the refresh response
refresh_response_data = refresh_response.json()
access_token = refresh_response_data["access_token"]
```

</Tab>
<Tab title="TypeScript">

```typescript
import axios from 'axios';

// Exchange authorization code for access and refresh tokens
const code = "authorization_code_from_callback";
const clientId = "your_client_id";
const clientSecret = "your_client_secret";
const redirectUri = "your_redirect_uri";

const payload = {
grant_type: "authorization_code",
code: code,
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri
};

const response = await axios.post(
"https://api.plane.so/auth/o/token/",
payload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);

// Parse the response
const responseData = response.data;
const accessToken = responseData.access_token;
const refreshToken = responseData.refresh_token;
const expiresIn = responseData.expires_in;

// When access token expires, use refresh token to get a new access token
const refreshPayload = {
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret
};

const refreshResponse = await axios.post(
"https://api.plane.so/auth/o/token/",
refreshPayload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
}
);

// Parse the refresh response
const refreshResponseData = refreshResponse.data;
const accessToken = refreshResponseData.access_token;
```

</Tab>
</Tabs>

### Fetching App Installation Details

In both user-authorized and app-authorized flows, the `app_installation_id` identifies the app's installation within a specific workspace. It is recommended that developers fetch workspace details after OAuth is successfully completed. Plane provides an `app-installation` endpoint that works with both types of tokens.
Copy link
Copy Markdown
Contributor

@aheckmann aheckmann Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recommended that developers fetch workspace details after OAuth is successfully completed.

Is it possible to use the API to fetch workspace details if Oauth hasn't been completed? Should we remove this line?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both types of tokens

Are you referring to the refresh_token and access_token or the bot token and access_token?


#### Examples

<Tabs>
<Tab title="Python">

Comment thread
Prashant-Surya marked this conversation as resolved.
Outdated
```python
import requests

# Set authorization header with either access token or bot token
headers = {
"Authorization": f"Bearer {token}",
}

# Make GET request to fetch installation/workspace details
response = requests.get(
url=f"https://api.plane.so/auth/o/app-installation/?id={app_installation_id}",
headers=headers
)

workspace_details = response.data[0]
```

</Tab>
<Tab title="TypeScript">

```typescript
import axios from 'axios';

// Set authorization header with either access token or bot token
const headers = {
Authorization: `Bearer ${token}`,
};

// Make GET request to fetch installation/workspace details
const response = await axios.get(
`https://api.plane.so/auth/o/app-installation/?id=${app_installation_id}`,
{ headers }
);

const workspaceDetails = response.data[0];
```

</Tab>
</Tabs>

#### Sample Response

```
[
{
"id": "34b97361-8636-43dc-953e-90deedc8498f",
"workspace_detail": {
"name": "sandbox",
"slug": "sandbox",
"id": "7a2e5944-c117-4a7d-b5f4-058fe705d7d1",
"logo_url": null
},
"created_at": "2025-05-16T13:50:27.865821Z",
"updated_at": "2025-06-23T08:57:26.976742Z",
"deleted_at": null,
"status": "installed",
"workspace": "7a2e5944-c117-4a7d-b5f4-058fe705d7d1",
"application": "ab235529-388a-4f51-a55a-78272251f5f1",
"installed_by": "63333ab1-c605-42fc-82f7-5cd86799eca1",
"app_bot": "7286aaa7-9250-4851-a520-29c904fd7654", // app's bot user id in the workspace
"webhook": "b1f4b7f1-51e8-4919-a84c-0b1143b51d2c"
}
]
```

## Using Plane SDKs

To simplify the OAuth flow and make it easier to build Plane apps, official SDKs are available:

| Language | Package Link | Source Code |
|----------|---------|-------------|
| Node.js | [npm i @makeplane/plane-node-sdk](https://www.npmjs.com/package/@makeplane/plane-node-sdk) | [plane-node-sdk](https://github.com/makeplane/plane-node-sdk) |
| Python | [pip install plane-sdk](https://pypi.org/project/plane-sdk/) | [plane-python-sdk](https://github.com/makeplane/plane-python-sdk) |

These SDKs provide helpers for OAuth and other common tasks, allowing developers to implement the above flows efficiently.

## Listing Your App on Plane Marketplace

Apps built using the OAuth flow can be listed on the Plane Marketplace: [https://plane.so/marketplace/integrations](https://plane.so/marketplace/integrations)

To list your app, please contact the Plane team at [**support@plane.so**](mailto:support@plane.so).
Loading