Framework-native Ko-fi payment webhook handling for Python
Project description
Mofi
Mofi turns Ko-fi payment webhooks into typed Python events inside an existing FastAPI, Flask, or Django application. It validates Ko-fi's shared verification token, preserves the complete payload, and runs application handlers before acknowledging the delivery.
Mofi covers payment notifications only: donations, subscriptions, commissions, and shop orders. Ko-fi does not expose account management, posts, messages, membership cancellations, refunds, or fulfillment operations through this API.
Install
Install only the integration your application uses:
uv add "mofi[fastapi]"
uv add "mofi[flask]"
uv add "mofi[django]"
Use mofi[all] for all three integrations. The equivalent pip install
commands also work. A base mofi install includes only Pydantic and the shared
event models.
Mofi requires Python 3.10 or newer.
FastAPI
import os
from fastapi import FastAPI
from mofi import Donation, PaymentEvent
from mofi.integrations.fastapi import KoFiRouter
app = FastAPI()
webhook = KoFiRouter(
os.environ["KOFI_VERIFICATION_TOKEN"],
prefix="/webhooks/kofi",
)
@webhook.on(Donation)
async def donation(event: Donation) -> None:
await record_donation(event.message_id, event.amount)
@webhook.on_payment
def every_payment(event: PaymentEvent) -> None:
audit_payment(event.message_id, event.type)
app.include_router(webhook)
Run this application with your normal ASGI server. Mofi does not create or run one.
Flask
import os
from flask import Flask
from mofi import Donation, PaymentEvent
from mofi.integrations.flask import KoFiBlueprint
app = Flask(__name__)
webhook = KoFiBlueprint(
"kofi",
__name__,
verification_token=os.environ["KOFI_VERIFICATION_TOKEN"],
url_prefix="/webhooks/kofi",
)
@webhook.on(Donation)
def donation(event: Donation) -> None:
record_donation(event.message_id, event.amount)
@webhook.on_payment
def every_payment(event: PaymentEvent) -> None:
audit_payment(event.message_id, event.type)
app.register_blueprint(webhook)
If verification_token is omitted, the Blueprint reads
app.config["MOFI_VERIFICATION_TOKEN"]. Explicit configuration takes
precedence. Flask's async extra is installed by mofi[flask], so handlers may
also be async def and still finish during the request.
Django
import os
from django.urls import path
from mofi import Donation, PaymentEvent
from mofi.integrations.django import KoFiWebhook
webhook = KoFiWebhook(
verification_token=os.environ["KOFI_VERIFICATION_TOKEN"]
)
@webhook.on(Donation)
def donation(event: Donation) -> None:
record_donation(event.message_id, event.amount)
@webhook.on_payment
def every_payment(event: PaymentEvent) -> None:
audit_payment(event.message_id, event.type)
urlpatterns = [
path("webhooks/kofi/", webhook.view, name="kofi_webhook"),
]
If verification_token is omitted, the view reads the
MOFI_VERIFICATION_TOKEN Django setting. The generated external webhook view
alone is CSRF-exempt; Mofi adds no application, middleware, model, or migration.
Runnable versions live in examples/.
Events and handler behavior
Known payments use these event classes:
| Ko-fi type | Mofi class |
|---|---|
| Donation | Donation |
| Subscription | Subscription |
| Commission | Commission |
| Shop Order | ShopOrder |
Every event extends PaymentEvent. A future type becomes UnknownPayment
instead of being rejected. The complete decoded object is available as
event.raw, and newly added fields are also retained by the Pydantic model.
Optional supporter messages, tier data, shop fields, shipping, and telephone
details may be absent. The Commission model intentionally exposes confirmed
common payment fields plus raw; it does not guess undocumented
Commission-only fields.
For a known event, Mofi calls handlers registered with .on(...) in
registration order, then handlers registered with .on_payment. Unknown events
call only .on_payment handlers. Dispatch is sequential. A valid event with no
handler is acknowledged with an empty HTTP 200 response.
Mofi returns HTTP 200 only after every selected handler finishes. Parsing and validation failures return a native bad-request response, token mismatch returns a forbidden response, and handler exceptions remain visible to the framework. This lets Ko-fi retry unsuccessful deliveries.
Retries and privacy
Ko-fi can retry a non-200 delivery with the same message_id. Mofi deliberately
delivers every attempt; it does not keep state or suppress duplicates. Use
event.message_id with your application's durable data store when an operation
must be idempotent.
Respect event.is_public before displaying supporter activity. event.raw can
contain the verification token, email, supporter message, transaction details,
shipping address, and telephone number. Do not log it or return it in responses;
apply your application's normal retention and access policies.
Configure Ko-fi securely
- Create a high-entropy token in Ko-fi's Webhook settings.
- Store the same token in your application's secret configuration.
- Set the webhook URL to the HTTPS route mounted above.
- Configure request-body limits in your framework or reverse proxy; Mofi keeps that policy with the host application.
Ko-fi puts the token inside the request body. It is a shared-secret check, not a cryptographic request signature, and it cannot prevent replay if the token is compromised. Rotate it in both Ko-fi and your application when exposure is suspected.
Upgrade from 0.2
Version 0.3 removes Mofi's server-owning Mofi class and GlobalType. See the
0.3 migration guide for direct FastAPI replacements and
the new Flask and Django integrations.
Development
uv sync --all-extras
uv run pytest
uv run python -m compileall mofi examples
uv build
uv run python tests/verify_distribution.py
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mofi-0.3.0.tar.gz.
File metadata
- Download URL: mofi-0.3.0.tar.gz
- Upload date:
- Size: 7.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff87d85ea2eba3317cfa562fff15c4cab8e6094b30679e27f584213230c73f26
|
|
| MD5 |
19ee70133e6ed1f2d045c542665a41d7
|
|
| BLAKE2b-256 |
230f869975ac78f8d8b9ded2fe8327e41663bd52ee3f61cdda72b4faefbe75b1
|
File details
Details for the file mofi-0.3.0-py3-none-any.whl.
File metadata
- Download URL: mofi-0.3.0-py3-none-any.whl
- Upload date:
- Size: 11.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae1a5f14d5e903983ca0ee593988f9f60d70470b1360eff3edb1fe2e65ccc616
|
|
| MD5 |
91bc54bc44df9c48e6c4068c0f815ecc
|
|
| BLAKE2b-256 |
3cdd5eb1222247f97a1416c09d7ae40f24b38d1cd1d2d6a70e8b9ca345866485
|