# How to Integrate Kaneo with External APIs: A Complete Guide to Webhooks and Custom Plugins

> Integrate Kaneo with external APIs using ready-made webhook plugins or build a custom plugin. Implement event handlers and validation logic in TypeScript for seamless integration.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: how-to-guide
- Published: 2026-08-05

---

**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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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:

```typescript
// 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`](https://github.com/usekaneo/kaneo/blob/main/slack/config.ts), etc.) and implements handlers like `handleTaskCreated` in [`apps/api/src/plugins/slack/events.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/slack/events.ts):

```typescript
// 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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts):

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/myservice/index.ts):

```typescript
// 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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/myservice/config.ts):

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/registry.ts):

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts) to extend Kaneo
- **Validate configs with valibot** and register plugins in [`apps/api/src/plugins/registry.ts`](https://github.com/usekaneo/kaneo/blob/main/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`](https://github.com/usekaneo/kaneo/blob/main/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.).