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 generation
  • events — 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

  1. Task event occurs — core task service emits a typed event
  2. Scheduler lookup — apps/api/src/scheduler/project-webhook-reminders.ts loads active integrations from integrationTable
  3. Handler invocation — for each matching integration, the scheduler calls the appropriate handler (e.g., handleTaskCreated in apps/api/src/plugins/generic-webhook/events.ts)
  4. Delivery execution — handler uses its client module (postToGenericWebhook, postToSlack, etc.) to POST the payload
  5. Health tracking — success/failure timestamps update the health object in the config (line 119 in generic-webhook/config.ts)

Payload Structure

Event payloads are strongly typed. A TaskCreatedEvent includes:

  • taskId, title, description, status
  • assigneeId, projectId, columnId
  • dueDate, priority, tags
  • createdAt, 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.ts line 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 IntegrationPlugin interface in apps/api/src/plugins/types.ts to 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:

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 →