> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corsair.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Ko-fi plugin

> Payment notifications from Ko-fi - receive the webhook Ko-fi sends when a donation, membership payment, shop order or commission is paid, with every delivery verified against the account's token. Ko-fi publishes no REST API.

Use **Ko-fi** through Corsair: one client, typed API calls, and incoming webhooks.

Paste your webhook URL and copy the verification token from your Ko-fi page under Settings, Webhooks. That token is the only credential this plugin needs. Store it as the account's API key (`pnpm corsair setup --plugin=kofi api_key=<token>`, or the API key field on Hub's connect page), as the account's `verification_token`, or pass it as the `key` option.

How a delivery arrives:

* **Ko-fi POSTs form-encoded, not JSON.** The body is `application/x-www-form-urlencoded` with a single `data` field holding the payment as a JSON string. `processWebhook` runs `JSON.parse` on a string body, so neither the raw text nor `request.json()` works: the route reads the `data` field and passes the payment object, as below. Use this route for Ko-fi instead of the generic one on the Webhooks page.
* **Every delivery carries a `verification_token`**, and the plugin's handler compares it in constant time before it accepts the payment. With no token configured the delivery is refused rather than accepted: an unverified payment notification would let whoever posted to the URL trigger a sale. The one exception is a delivery Hub forwards with `hubVerified`: Hub checked the token before forwarding, so the handler skips its own comparison and needs no local token.
* **Act on a payment in an `after` hook, not a `before` hook.** Core runs `webhookHooks.before` ahead of the handler, so a `before` hook also sees deliveries whose token is wrong. `after` runs only when the handler accepted the delivery.
* **A refused delivery answers 401** (missing or wrong token) **or 400** (not a Ko-fi payment). Ko-fi retries anything other than a 200, with the same `message_id`.
* **With `multiTenancy`, give each tenant its own URL**, such as `https://<your-app>/api/webhook?tenantId=acme`, and register it on that tenant's Ko-fi page. A delivery names no Ko-fi account, so the `tenantId` in the URL is what picks the tenant whose token, hooks and data handle it; without it the delivery runs as `default`.

```ts app/api/webhook/route.ts theme={null}
import { processWebhook } from "corsair";
import { corsair } from "@/server/corsair";

export async function POST(request: Request) {
    const headers = Object.fromEntries(request.headers);
    const query = Object.fromEntries(new URL(request.url).searchParams);
    // Ko-fi's body is a form with one `data` field holding the payment JSON.
    const data = new URLSearchParams(await request.text()).get("data");
    // unknown: the payment is untrusted JSON from the request; the plugin
    // checks its token and shape before anything uses it.
    let payment: Record<string, unknown>;
    try {
        payment = JSON.parse(data ?? "");
    } catch {
        return Response.json({ success: false }, { status: 400 });
    }
    const result = await processWebhook(corsair, headers, payment, query);
    if (!result.plugin) {
        return Response.json({ success: false }, { status: 404 });
    }
    const status =
        result.response?.success === false ? (result.response.statusCode ?? 400) : 200;
    return Response.json(result.response ?? { success: true }, { status });
}
```

The four payment types:

* **`Donation`** is a one-off tip. `amount` is a decimal string such as `5.00`, not a number, and `message` holds the note left with it.
* **`Subscription`** is a membership payment. It fires on every payment, not only the first, so use `is_first_subscription_payment` to tell them apart, and `tier_name` for the tier.
* **`Shop Order`** carries `shop_items`, and `shipping` when the goods are physical.
* **`Commission`** is a paid commission. Ko-fi documents the type but publishes no example payload, so its fields follow the same base shape and may vary.

Things to know before you call:

* **There is no API to call.** `api.ko-fi.com` does not resolve, and Ko-fi's help page says the API is "currently limited to when a payment is made". `webhooks.listEvents` therefore returns the documented payment types from Ko-fi's reference and makes no request.
* **No payments are stored locally.** The plugin declares no entities, and Ko-fi has no API to read payments back from, so keep what you need from each delivery in a `webhookHooks` `after` hook. When Corsair has a database, each delivery's event log entry keeps the message ID, amount, currency and whether it was a membership payment.
* **Ko-fi only tells you about payments.** It cannot notify you when a membership ends, so a lapse has to be inferred from the absence of a payment or read from Ko-fi itself.
* **A retry keeps its `message_id`.** Ko-fi retries when the endpoint does not answer 200, with the same `message_id` each time, so that field is what makes a delivery safe to de-duplicate.
* **Supporter details are personal data.** The plugin logs the amount, currency and message ID, and never the supporter's name, email, address or message.

**What you get:**

* 1 typed API operations
* 4 incoming webhook event types

## Setup

**1. Install**

```bash npm theme={null}
npm install corsair @corsair-dev/kofi better-sqlite3
npm install --save-dev @types/better-sqlite3
```

```bash yarn theme={null}
yarn add corsair @corsair-dev/kofi better-sqlite3
yarn add --dev @types/better-sqlite3
```

```bash pnpm theme={null}
pnpm install corsair @corsair-dev/kofi better-sqlite3
pnpm install --save-dev @types/better-sqlite3
```

```bash bun theme={null}
bun add corsair @corsair-dev/kofi better-sqlite3
bun add --dev @types/better-sqlite3
```

**2. Add the plugin**

```ts corsair.ts theme={null}
import Database from 'better-sqlite3';
import { createCorsair } from 'corsair';
import { kofi } from '@corsair-dev/kofi';

export const corsair = createCorsair({
	plugins: [
		kofi(),
	],
	database: new Database('corsair.db'),
	kek: process.env.CORSAIR_KEK!,
	multiTenancy: true,
	hub: {
		projectApiKey: process.env.CORSAIR_API_KEY!,
		signingSecret: process.env.CORSAIR_SIGNING_SECRET!,
	},
});
```

With `multiTenancy: true`, scope calls with `corsair.withTenant(id)`. See [Quick start](https://docs.corsair.dev/quick-start.md) for KEK + Hub keys, and [Multi-tenancy](https://docs.corsair.dev/concepts/multi-tenancy.md) for account isolation.

**3. Choose authentication**

> [tab] API Key (Recommended)
>
> Store the Ko-fi API key before the first call: `pnpm corsair setup --plugin=kofi --tenant=acme api_key=<your-key>` (leave out `--tenant` without `multiTenancy`), or pass it as `key` in the plugin options. With Hub, each tenant enters their own key on the connect page. See [API key auth](https://docs.corsair.dev/concepts/api-key).
>
> ```ts theme={null}
> kofi()
> ```
>
> More: [API Key](https://docs.corsair.dev/concepts/api-key)

**4. Connect a tenant**

Mint a connect link and send the tenant to it. Hub hosts the page and delivers the result to your app — see [Connect / OAuth](https://docs.corsair.dev/management/connect).

```ts theme={null}
const { connectUrl } = await corsair.manage.connect.createLink({
	plugin: 'kofi',
	tenantId: 'acme',
});
// redirect the user's browser to connectUrl
```

## Example API calls

**List the payment types Ko-fi can send**

```ts theme={null}
const tenant = corsair.withTenant('acme');
await tenant.kofi.api.webhooks.listEvents({});
```

**Write**

*No write-style endpoint inferred; pick any operation from the [API](https://docs.corsair.dev/plugins/kofi/api.md) reference.*

See the full list on the [API](https://docs.corsair.dev/plugins/kofi/api.md) page.

## Webhooks

This plugin registers **4** webhook event types. Configure the provider to POST to your Corsair HTTP handler, then use `webhookHooks` on the plugin factory.

See [Webhooks](https://docs.corsair.dev/plugins/kofi/webhooks.md) for every event path and payload shape, and [Webhooks concept](https://docs.corsair.dev/concepts/webhooks.md) for routing.

## What's next

> [card] [API reference](https://docs.corsair.dev/plugins/kofi/api.md)
>
> Every `kofi.api.*` operation with input and output types.

> [card] [Webhooks](https://docs.corsair.dev/plugins/kofi/webhooks.md)
>
> Event paths, payloads, and `webhookHooks` examples.

> [card] [Connect / OAuth](https://docs.corsair.dev/management/connect)
>
> createLink, Hub delivery, and tenant connect flows.

> [card] [Use with agents](https://docs.corsair.dev/mcp-adapters/mcp-adapters)
>
> Expose this plugin's operations as MCP tools.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.