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 notifications | NotifiableIntegration |
| Rewrites content via AI for the platform | AdaptableIntegration |
| Posts content to the platform’s API | PublishableIntegration |
| Uses OAuth 2.0 for authentication | OAuthIntegration |
| Is configured by pasting a URL | WebhookIntegration |
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
FetchLikevia 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
-
FetchLikedependency 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)