# Integration Patterns for the Plane API: A Complete Guide to Third-Party Connections

> Explore integration patterns for the Plane API. Learn how to connect third-party services like Slack, GitHub, and GitLab for bidirectional sync using REST endpoints, OAuth, and webhooks.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: how-to-guide
- Published: 2026-06-18

---

**Plane’s backend implements a three-layer Django/DRF architecture that exposes REST endpoints for provider registries, workspace-specific OAuth connections, and webhook handling to enable bidirectional sync with external services like Slack, GitHub, and GitLab.**

The **Integration Patterns for the Plane API** define how external services connect to workspace data in the `makeplane/plane` repository. Built on Django REST Framework, the system separates provider metadata, per-workspace authentication tokens, and provider-specific extensions into distinct models that support scalable third-party integrations.

## Core Integration Architecture

Plane’s integration layer organizes data into three distinct tiers: a global provider registry, workspace-specific connection records, and specialized models for individual services.

### The Integration Registry

The `Integration` model in [`apps/api/plane/db/models/integration/base.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/integration/base.py) serves as the central registry for all supported third-party providers. This table stores static metadata about each service, including webhook URLs, redirect URLs, and verification status.

```python
class Integration(AuditModel):
    id = models.UUIDField(...)
    title = models.CharField(max_length=400)
    provider = models.CharField(max_length=400, unique=True)   # e.g. "slack", "github"

    network = models.PositiveIntegerField(choices=((1, "Private"), (2, "Public")))
    description = models.JSONField(default=dict)
    webhook_url = models.TextField(blank=True)
    webhook_secret = models.TextField(blank=True)
    redirect_url = models.TextField(blank=True)
    metadata = models.JSONField(default=dict)
    verified = models.BooleanField(default=False)
    avatar_url = models.TextField(blank=True, null=True)

```

Adding a new provider requires inserting a row into this table with a unique `provider` string and relevant static URLs. The `verified` flag indicates whether the integration has completed platform-specific validation.

### Per-Workspace Connections

The `WorkspaceIntegration` model establishes the link between a specific workspace, a user actor, and the third-party provider. Located in the same [`base.py`](https://github.com/makeplane/plane/blob/main/base.py) file, this model enforces a one-connection-per-provider constraint through `unique_together = ["workspace", "integration"]`.

```python
class WorkspaceIntegration(BaseModel):
    workspace = models.ForeignKey("db.Workspace", related_name="workspace_integrations", on_delete=models.CASCADE)
    actor = models.ForeignKey("db.User", related_name="integrations", on_delete=models.CASCADE)
    integration = models.ForeignKey("db.Integration", related_name="integrated_workspaces", on_delete=models.CASCADE)
    api_token = models.ForeignKey("db.APIToken", related_name="integrations", on_delete=models.CASCADE)
    metadata = models.JSONField(default=dict)
    config = models.JSONField(default=dict)

```

The `api_token` field stores the OAuth token returned during the authorization flow, while the `config` JSON field holds provider-specific settings such as Slack channel IDs or repository mappings.

## Provider-Specific Extensions

Each supported service extends the base pattern through dedicated models in `apps/api/plane/db/models/integration/`. These files wrap the generic `WorkspaceIntegration` to expose service-specific fields required for API operations.

For example, the Slack integration model in [`slack.py`](https://github.com/makeplane/plane/blob/main/slack.py) adds fields for access tokens and bot identification:

```python
class ISlackIntegration(models.Model):
    id = models.CharField(max_length=255, primary_key=True)
    access_token = models.CharField(max_length=255)
    bot_user_id = models.CharField(max_length=255)
    webhook_url = models.URLField()
    data = models.JSONField()          # payload returned by Slack’s OAuth flow

    # ... plus foreign keys to Workspace and WorkspaceIntegration

```

Similar extension files exist for GitHub ([`github.py`](https://github.com/makeplane/plane/blob/main/github.py)) and GitLab ([`gitlab.py`](https://github.com/makeplane/plane/blob/main/gitlab.py)), following the same structural pattern of augmenting the base `WorkspaceIntegration` with provider-specific data.

## REST API Surface and Webhook Handling

The API exposes standard CRUD endpoints for managing integrations and processing incoming webhooks. These routes are implemented in the DRF viewsets under `apps/api/plane/api/views/`.

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/v1/integrations/` | `GET` | List available providers (`Integration` objects) |
| `/api/v1/workspace-integrations/` | `GET` | List active workspace connections |
| `/api/v1/workspace-integrations/` | `POST` | Create new connection with `integration` ID and `api_token` |
| `/api/v1/workspace-integrations/{id}/` | `PATCH/DELETE` | Update configuration or remove connection |
| `/api/v1/webhooks/` | `POST` | Receive and validate webhook events |

The webhook endpoint validates incoming requests against the `webhook_secret` stored in the `Integration` model. The OpenAPI schema documentation, generated by `drf-spectacular`, pulls parameter definitions from [`apps/api/plane/utils/openapi/parameters.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/openapi/parameters.py):

```python
integration_source = openapi.Parameter(
    name="integration_source",
    location=openapi.IN_QUERY,
    description="GitHub integration source",
    type=openapi.TYPE_STRING,
)

```

## External-ID Tracking for Bidirectional Sync

To maintain sync between Plane and external systems, core models like issues and assets include `external_id` and `external_source` fields. These fields enable webhook handlers to locate the corresponding Plane work items when processing updates from third-party services.

The issue serializer in [`apps/api/plane/api/serializers/issue.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/api/serializers/issue.py) exposes these fields:

```python
external_id = serializers.CharField(required=False, help_text="External identifier for the issue (for integration tracking)")
external_source = serializers.CharField(required=False, help_text="External source system (for integration tracking)")

```

When importing data from GitHub or Slack, populate these fields with the provider’s native identifiers. This creates a reliable mapping that webhook handlers use to apply updates to the correct Plane entities.

## Typical Integration Flow

Implementing a complete integration follows these sequential steps:

1. **Discover providers** – Query `GET /api/v1/integrations/` to retrieve available services.
2. **OAuth handshake** – Redirect the user to the provider’s authorization URL using the `redirect_url` from the `Integration` record.
3. **Store credentials** – Create an `APIToken` with the OAuth response, then POST to `/api/v1/workspace-integrations/` with the token ID and provider configuration.
4. **Import with external IDs** – When creating Plane work items from the provider, populate `external_id` and `external_source` to establish bidirectional links.
5. **Process webhooks** – Listen for incoming events at `/api/v1/webhooks/` and update work items by matching their external identifiers.

## Adding a New Integration Provider

To extend Plane with a new provider like Asana:

1. **Seed the registry** – Insert a row into the `Integration` table with a unique `provider` string and appropriate webhook URLs.
2. **Create extension model** – Add a new file in `apps/api/plane/db/models/integration/` defining provider-specific fields if the generic `config` JSON is insufficient.
3. **Expose API fields** – Update relevant serializers in `apps/api/plane/api/serializers/` to include the new integration’s configuration options.
4. **Implement webhook handling** – Add view logic to validate secrets and process payload formats specific to the new service.
5. **Document parameters** – Add parameter definitions to [`apps/api/plane/utils/openapi/parameters.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/utils/openapi/parameters.py) for OpenAPI schema generation.

## Summary

- **Integration registry** – The `Integration` model in [`apps/api/plane/db/models/integration/base.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/integration/base.py) stores metadata for all supported providers.
- **Workspace connections** – `WorkspaceIntegration` links workspaces to providers via OAuth tokens stored in `api_token` fields.
- **Provider extensions** – Service-specific models like `ISlackIntegration` in [`slack.py`](https://github.com/makeplane/plane/blob/main/slack.py) extend the base pattern for unique API requirements.
- **External ID tracking** – Fields `external_id` and `external_source` on work items enable bidirectional synchronization with third-party systems.
- **Webhook validation** – Incoming events are validated against `webhook_secret` stored in the provider registry.

## Frequently Asked Questions

### How does Plane handle authentication for third-party integrations?

Plane stores OAuth tokens in the `APIToken` model and references them through the `WorkspaceIntegration.api_token` foreign key. When a user connects a service, the system creates a new `WorkspaceIntegration` record linking the workspace, user, provider, and token, ensuring scoped access control.

### What is the purpose of the external_id field on Plane issues?

The `external_id` field stores the native identifier from a third-party system (such as a GitHub issue number). This allows webhook handlers to locate and update the correct Plane work item when receiving change notifications from external services, maintaining synchronization between systems.

### Can a workspace connect to multiple instances of the same provider?

No. The `unique_together = ["workspace", "integration"]` constraint on the `WorkspaceIntegration` model enforces one active connection per provider per workspace. To support multiple instances (e.g., two GitHub organizations), the `config` JSON field can store multiple authentication contexts, though this requires custom implementation.

### Where are the TypeScript types for integrations defined?

Frontend type definitions reside in [`packages/types/src/integration.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/integration.ts), which exports interfaces like `IAppIntegration`, `IWorkspaceIntegration`, and `ISlackIntegration`. These types mirror the Django model structure and ensure type safety across the Plane client application.