How to Integrate Kaneo with External APIs: A Complete Guide to Webhooks and Custom Plugins
Integrate Kaneo with external APIs by configuring ready-made webhook plugins (generic webhook, Slack, Discord, GitHub, Gitea, Telegram) or building a custom IntegrationPlugin that implements event handlers and validation logic in TypeScript.
Kaneo's integration framework lets you push task-related events to any external service through a flexible plugin architecture. This guide walks you through using built-in integrations, the core mechanisms that power them, and how to extend the platform with your own custom connectors.
Built-In Integration Options
Kaneo ships with six ready-to-use integrations that cover common notification and sync scenarios. Each is implemented as a plugin in apps/api/src/plugins/.
Generic Webhook: The Universal Connector
The generic webhook plugin (apps/api/src/plugins/generic-webhook/index.ts) is the most versatile option. It delivers JSON payloads to any HTTPS endpoint you specify, with optional HMAC-SHA256 signature verification.
Configuration happens through apps/api/src/components/project/generic-webhook-integration-settings.tsx in the web UI. The API endpoint is:
POST /generic-webhook-integration/project/:projectId
The config schema in apps/api/src/plugins/generic-webhook/config.ts validates:
webhookUrl— must be a routable public address (SSRF protection at line 55)secret— optional key for HMAC signature generationevents— per-event opt-ins (taskCreated,taskStatusChanged,taskAssigned,taskCommented,dueDateReminder)reminderLeadTime— minutes before due date to trigger reminders
Here's how to create one programmatically:
// apps/web/src/fetchers/generic-webhook-integration/create-generic-webhook-integration.ts
import { postApiUrl } from "@/fetchers/post-api-url";
export async function createGenericWebhookIntegration(
projectId: string,
config: {
webhookUrl: string;
secret?: string;
events?: Partial<Record<GenericWebhookEventKey, boolean>>;
},
) {
const resp = await fetch(
postApiUrl(`/generic-webhook-integration/project/${projectId}`),
{
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(config),
},
);
if (!resp.ok) {
const err = await resp.text();
throw new Error(err);
}
return (await resp.json()) as GenericWebhookIntegration;
}
Slack, Discord, and Telegram: Chat Notifications
These three integrations follow an identical pattern with service-specific formatting:
- Slack:
apps/api/src/plugins/slack/*— sends formatted blocks to incoming webhook URLs - Discord:
apps/api/src/plugins/discord/*— mirrors Slack implementation for Discord webhooks - Telegram:
apps/api/src/plugins/telegram/*— uses bot token and chat ID via Telegram Bot API
Each validates its config with valibot schemas (slack/config.ts, etc.) and implements handlers like handleTaskCreated in apps/api/src/plugins/slack/events.ts:
// apps/api/src/plugins/slack/events.ts (excerpt)
import { postToSlack } from "./client";
import { SlackConfig } from "./config";
export async function handleTaskCreated(
event: TaskCreatedEvent,
ctx: PluginContext,
) {
const cfg = ctx.config as SlackConfig;
if (!cfg.events?.taskCreated) return; // event disabled by user
const message = {
text: `🆕 New task: ${event.title}`,
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: `*${event.title}*` },
},
],
};
await postToSlack(cfg.webhookUrl, message);
}
GitHub and Gitea: Bi-Directional Issue Sync
The GitHub and Gitea integrations (apps/api/src/plugins/github/*, apps/api/src/plugins/gitea/*) use the app authentication model rather than simple webhooks. They:
- Authenticate as GitHub/Gitea Apps using private keys
- Create and update issues when tasks change
- Sync status changes back to Kaneo when issues are modified
This enables full two-way sync between Kaneo tasks and external issue trackers.
How the Integration Architecture Works
Understanding the event flow helps debug failures and build custom plugins.
The IntegrationPlugin Interface
All integrations implement IntegrationPlugin defined in apps/api/src/plugins/types.ts:
interface IntegrationPlugin {
type: string;
name: string;
validateConfig: (config: unknown) => Promise<ValidationResult>;
onTaskCreated?: (event: TaskCreatedEvent, ctx: PluginContext) => Promise<void>;
onTaskStatusChanged?: (event: TaskStatusChangedEvent, ctx: PluginContext) => Promise<void>;
onTaskAssigned?: (event: TaskAssignedEvent, ctx: PluginContext) => Promise<void>;
onTaskCommented?: (event: TaskCommentedEvent, ctx: PluginContext) => Promise<void>;
onDueDateReminder?: (event: DueDateReminderEvent, ctx: PluginContext) => Promise<void>;
}
Event Dispatch Flow
- Task event occurs — core task service emits a typed event
- Scheduler lookup —
apps/api/src/scheduler/project-webhook-reminders.tsloads active integrations fromintegrationTable - Handler invocation — for each matching integration, the scheduler calls the appropriate handler (e.g.,
handleTaskCreatedinapps/api/src/plugins/generic-webhook/events.ts) - Delivery execution — handler uses its client module (
postToGenericWebhook,postToSlack, etc.) to POST the payload - Health tracking — success/failure timestamps update the
healthobject in the config (line 119 ingeneric-webhook/config.ts)
Payload Structure
Event payloads are strongly typed. A TaskCreatedEvent includes:
taskId,title,description,statusassigneeId,projectId,columnIddueDate,priority,tagscreatedAt,createdBy
The generic webhook sends this as JSON with an optional X-Kaneo-Signature header when a secret is configured.
Creating a Custom Kaneo Integration
When built-in options don't suffice, implement the IntegrationPlugin interface yourself.
Step 1: Define Your Plugin Structure
Create apps/api/src/plugins/myservice/index.ts:
// apps/api/src/plugins/myservice/index.ts
import { IntegrationPlugin, TaskCreatedEvent, PluginContext } from "../types";
import { validateMyServiceConfig, MyServiceConfig } from "./config";
export const myServicePlugin: IntegrationPlugin = {
type: "myservice",
name: "My External Service",
validateConfig: validateMyServiceConfig,
onTaskCreated: async (event: TaskCreatedEvent, ctx: PluginContext) => {
const config = ctx.config as MyServiceConfig;
await fetch(config.endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
event: "task.created",
taskId: event.taskId,
title: event.title,
url: `${config.kaneoBaseUrl}/projects/${event.projectId}/tasks/${event.taskId}`,
}),
});
},
onTaskStatusChanged: async (event, ctx) => {
// Implement if needed
},
};
Step 2: Write Config Validation
Use valibot for runtime validation in apps/api/src/plugins/myservice/config.ts:
import * as v from "valibot";
const myServiceConfigSchema = v.object({
endpoint: v.pipe(v.string(), v.url()),
apiKey: v.string(),
kaneoBaseUrl: v.pipe(v.string(), v.url()),
events: v.optional(v.object({
taskCreated: v.optional(v.boolean()),
taskStatusChanged: v.optional(v.boolean()),
})),
});
export type MyServiceConfig = v.InferInput<typeof myServiceConfigSchema>;
export async function validateMyServiceConfig(cfg: unknown) {
const result = v.safeParse(myServiceConfigSchema, cfg);
if (!result.success) {
return { valid: false, errors: result.issues.map(i => i.message) };
}
return { valid: true, data: result.output };
}
Step 3: Register Your Plugin
Add to apps/api/src/plugins/registry.ts:
import { myServicePlugin } from "./myservice";
export const integrationPlugins: IntegrationPlugin[] = [
// ... existing plugins
myServicePlugin,
];
The scheduler automatically discovers and invokes your plugin for configured projects.
Step 4: Add API Routes and UI (Optional)
Follow the pattern in apps/api/src/generic-webhook-integration/index.ts to expose CRUD endpoints, then create fetchers in apps/web/src/fetchers/myservice/ and a settings component for the UI.
Security and Reliability Features
Kaneo includes several safeguards for production API integrations:
- SSRF prevention: The generic webhook rejects non-public addresses unless explicitly allowed (
apps/api/src/plugins/generic-webhook/config.tsline 55) - Timeout handling: All client modules implement request timeouts (see
apps/api/src/plugins/slack/client.ts) - Health tracking: Each delivery updates success/failure timestamps, visible in the UI
- Event filtering: Users opt into specific events per integration, reducing unnecessary traffic
Summary
- Use built-in integrations for Slack, Discord, Telegram, GitHub, Gitea, or generic HTTPS webhooks
- Understand the
IntegrationPlugininterface inapps/api/src/plugins/types.tsto extend Kaneo - Validate configs with valibot and register plugins in
apps/api/src/plugins/registry.ts - Leverage existing infrastructure: health tracking, scheduler, and UI patterns work automatically for custom plugins
- Follow security defaults: public-address guards, HMAC signatures, and timeout handling are built in
Frequently Asked Questions
What events can trigger a Kaneo webhook integration?
Kaneo supports five event types: taskCreated, taskStatusChanged, taskAssigned, taskCommented, and dueDateReminder. Each integration plugin can implement handlers for any subset. Users enable specific events per integration in the configuration UI.
How does Kaneo prevent SSRF attacks on generic webhooks?
The generic webhook plugin validates that the provided URL resolves to a publicly routable IP address before allowing configuration. This check lives in apps/api/src/plugins/generic-webhook/config.ts at line 55 and can only be bypassed through explicit environment configuration.
Can I build a bi-directional sync like GitHub's integration?
Yes. The GitHub and Gitea plugins demonstrate this pattern: they authenticate as Apps, receive incoming webhooks from the external service, and update Kaneo tasks accordingly. Your custom plugin can implement similar incoming webhook handlers alongside outbound event delivery.
Where can I monitor if my webhook deliveries are succeeding?
Each integration stores a health object with lastSuccessAt and lastFailureAt timestamps. The UI surfaces this status in the project integrations page. For debugging, check the server logs for errors from the client modules (postToGenericWebhook, postToSlack, etc.).
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →