Skip to Content
ReferenceAuthoring a channel integration

Authoring a channel integration

This is a developer reference for anyone extending Structura with a new channel. If you’re looking for how to use existing channels, see How channels work instead.

This guide walks through adding a new integration to the channels system. We’ll use a hypothetical “Mailchimp” integration as an example.

Step 1: Choose Capabilities

Decide which capability interfaces your integration needs:

If your integration…Implement
Sends fire-and-forget notificationsNotifiableIntegration
Rewrites content via AI for the platformAdaptableIntegration
Posts content to the platform’s APIPublishableIntegration
Uses OAuth 2.0 for authenticationOAuthIntegration
Is configured by pasting a URLWebhookIntegration

Capabilities are composable. LinkedIn implements OAuth + Adapt + Publish. Slack implements Webhook + Notify. IndexNow implements just Notify.

Step 2: Create the Integration Class

Create functions/src/channels/integrations/MailchimpIntegration.ts:

import type { FetchLike } from "../contracts/Integration.js"; import type { Integration, IntegrationMetadata, HealthStatus, ConnectionRecord, } from "../contracts/Integration.js"; import type { NotifiableIntegration, NotifyContext, NotifyResult } from "../contracts/Integration.js"; export const MAILCHIMP_INTEGRATION_ID = "mailchimp"; export class MailchimpIntegration implements Integration, NotifiableIntegration { constructor(private readonly fetch: FetchLike = globalThis.fetch) {} readonly metadata: IntegrationMetadata = { id: MAILCHIMP_INTEGRATION_ID, name: "Mailchimp", category: "email", sku: "channels", // or "free" for free integrations capabilities: ["notify"], authType: "apikey", // or "oauth2", "webhook", "none" iconUrl: "/icons/mailchimp.svg", }; async healthCheck(connection: ConnectionRecord): Promise<HealthStatus> { if (connection.status === "connected") return { status: "healthy" }; return { status: "unhealthy", reason: connection.lastError?.message }; } async notify(ctx: NotifyContext): Promise<NotifyResult> { // Implementation here } }

Key rules:

  • Accept FetchLike via constructor for testability.
  • Never hardcode HTTP calls — always use the injected fetch.
  • Classify HTTP errors consistently (see error classification table in Channel integration contracts).

Step 3: Register the Integration

Add your class to functions/src/channels/registry/IntegrationRegistry.ts:

import { MailchimpIntegration } from "../integrations/MailchimpIntegration.js"; const CATALOG: Integration[] = [ // ... existing integrations new MailchimpIntegration(), ];

Step 4: Add a Catalog Entry

Add an entry to functions/src/channels/registry/catalog.ts:

{ id: "mailchimp", name: "Mailchimp", description: "Send email campaigns when posts are published", category: "email", capabilities: ["notify"], authType: "apikey", gating: { requiredPlan: "free", // Minimum plan requiredAddon: "channels", // null if free, "channels" if paid }, iconUrl: "/icons/mailchimp.svg", }

Step 5: Write Tests

Create functions/src/channels/__tests__/MailchimpIntegration.test.ts:

import { describe, it, expect, vi } from "vitest"; import { MailchimpIntegration } from "../integrations/MailchimpIntegration.js"; describe("MailchimpIntegration", () => { it("has correct metadata", () => { const integration = new MailchimpIntegration(); expect(integration.metadata.id).toBe("mailchimp"); expect(integration.metadata.capabilities).toContain("notify"); }); it("sends notification on publish", async () => { const stubFetch = vi.fn().mockResolvedValue({ ok: true, status: 200, json: async () => ({}), }); const integration = new MailchimpIntegration(stubFetch); const result = await integration.notify(/* NotifyContext */); expect(result.status).toBe("ok"); expect(stubFetch).toHaveBeenCalledOnce(); }); // Test error classification for each HTTP status... });

Update IntegrationRegistry.test.ts to include the new integration in expected counts.

Step 6: Update the WordPress Plugin (if needed)

For credential-based or webhook-based integrations, no plugin changes are needed — the existing channelsSaveCredentialConnection and channelsSaveWebhookConnection endpoints handle all auth types generically.

For OAuth integrations, the existing channelsOAuthInit and channelsOAuthCallback endpoints handle the flow. You may need to add new defineSecret() entries for the provider’s client ID/secret.

Step 7: Add Documentation

Create docs/pages/channels/integrations/mailchimp.mdx with:

  • How the integration works
  • Metadata table
  • Decrypted secret shape
  • Connection setup instructions
  • Error handling table

Update docs/pages/channels/integrations/_meta.json to include the new page.

Checklist

  • Integration class implementing correct capability interfaces
  • FetchLike dependency injection
  • Error classification for all HTTP status codes
  • Locale-aware messages (if NotifiableIntegration)
  • Health check implementation
  • Registered in IntegrationRegistry
  • Catalog entry with correct gating
  • Unit tests (metadata, happy path, error cases, edge cases)
  • Registry test updated with new expected counts
  • Documentation page
  • Secrets configured (if OAuth or API key)
Last updated on