karni-payments
Payments module for Karni: hosted checkout, refunds and webhook-driven payment state for Stripe (Checkout Sessions) and Razorpay (Payment Links), plus an in-memory test provider.
karni add payments # wires dependency + INSTALLED_APPS + agent guidance
pip install "karni-payments[stripe]" # or [razorpay]
One line of project wiring (the viewsets ride Karni's auto-router):
# config/urls.py
(path("payments/", include("karni_payments.urls")),) # the webhook endpoint
then make migrate. Register https://<host>/payments/webhook/stripe/ (or
.../razorpay/) at the provider.
What you get
- Models (all on
karni.models.BaseModel):Payment,Refund,WebhookEvent(the(provider, event_id)dedupe record). Derived admin viaAdminMeta— noadmin.py. - API (auto-routed, authenticated only):
GET /api/payments/— owner-scoped; thepayments-adminrole sees all.POST /api/payments/checkout/— create a pending payment + provider session; redirect the customer to the returnedurl.POST /api/payments/{id}/refund/— requires thepayments-adminrole; omitamountto refund the full unrefunded remainder.GET /api/refunds/—payments-adminonly.- Writes to the resources themselves answer 405: state belongs to webhooks.
- Webhooks:
POST /payments/webhook/<provider>/verifies the signature (400 + warning log on failure, nothing persisted), dedupes on(provider, event_id)and hands processing to a Celery task (process_webhook_event: row lock + monotonic state machine +processed_atstamp in one transaction; 3 retries with backoff). Nothing state-changing runs in the request cycle. - Services (
karni_payments.services) — the only module project code should call:create_checkout,create_refund(lock-validated against the unrefunded remainder — over-refunds are impossible),apply_event.
Settings (via the karni.settings fragment — all overridable)
| Setting (env var of the same name) | Default | Meaning |
|---|---|---|
KARNI_PAYMENTS_PROVIDER |
"stripe" |
"stripe", "razorpay", "test" or a dotted path to your own PaymentProvider subclass |
KARNI_PAYMENTS_CURRENCY |
"usd" |
default checkout currency |
KARNI_PAYMENTS_CHECKOUT_VALIDATOR |
pass-through | dotted path called with (user=, amount=, currency=, metadata=) before a Payment exists — set it; never trust client amounts |
KARNI_PAYMENTS_EVENT_HANDLER |
karni_payments.services.apply_event |
dotted path the webhook task calls — the state-machine escape hatch |
KARNI_PAYMENTS_ALLOWED_REDIRECTS |
[] |
extra origins allowed for checkout redirect URLs (on top of KARNI_FRONTEND_URL) |
STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET |
"" |
validated lazily at provider use |
RAZORPAY_KEY_ID / RAZORPAY_KEY_SECRET / RAZORPAY_WEBHOOK_SECRET |
"" |
validated lazily at provider use |
Semantics worth knowing
- Monotonic state:
pending → processing → succeeded → partially_refunded → refunded;failed/canceledare terminal. Events that would regress a status are recorded as ano-op:note on theWebhookEventand count as processed. Expired checkout sessions map tocanceled. - Zero-decimal currencies (JPY, KRW, …) are handled by
providers.base.to_minor_units/from_minor_units; 3-decimal currencies (BHD, KWD, …) are unsupported in v0.1 and raise. - A provider failure during checkout marks the Payment
failedbefore re-raising. Abandoned pending payments are not reaped in v0.1 — expire them yourself if you need to (sessions that expire at the provider arrive as webhook events and cancel the payment). - Razorpay webhooks take their event id from the
X-Razorpay-Event-Idheader; Payment Links have a singlecallback_url, socancel_urlis unused there.
Testing your project
Set KARNI_PAYMENTS_PROVIDER = "test" in test settings:
TestProvider is in-memory and deterministic (test_cs_1, test_re_1, …),
and TestProvider.build_webhook({...}) returns a correctly signed
(body, headers) pair for driving the full webhook pipeline. Call
TestProvider.reset() between tests.
Development
pip install -e . --group dev
python -m pytest # standalone suite (sqlite + eager Celery + TestProvider)
ruff check . && ruff format --check .
This module is also the reference implementation of the Karni module
contract — see docs/modules.md in the Karni repo.
Metadata
Release files for karni-payments 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| karni_payments-0.3.1.tar.gz | 30.0 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| karni_payments-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 70.5 kB
Release files / karni_payments-0.3.1.tar.gz
| Download URL | karni_payments-0.3.1.tar.gz |
|---|---|
| Size | 30.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
449705576f95898b7b27b8900bb95025fdc9dc43b25c10390d81bcf4cce408bf
|
|
BLAKE2b-256 checksum How to use checksums |
1ffedd9348ed8ed063672facd2ed443665c52c7137ea5e0ca3fc6a2a80d8aab8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / karni_payments-0.3.1-py3-none-any.whl
| Download URL | karni_payments-0.3.1-py3-none-any.whl |
|---|---|
| Size | 40.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e6d0fd9bd5967f927997e70e0938370be4198f3ba334d0c459d911fb7f216b2b
|
|
BLAKE2b-256 checksum How to use checksums |
cea8ebcd7a95f645960d30c5af6cfb52909aa1769dd3024ddeda5c0b1316a379
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for karni-payments 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.