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

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 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.

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 file, this model enforces a one-connection-per-provider constraint through unique_together = ["workspace", "integration"].

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 adds fields for access tokens and bot identification:

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) and GitLab (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:

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 exposes these fields:

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 for OpenAPI schema generation.

Summary

  • Integration registry – The Integration model in 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 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, which exports interfaces like IAppIntegration, IWorkspaceIntegration, and ISlackIntegration. These types mirror the Django model structure and ensure type safety across the Plane client application.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →