Django Paystack Wallet
A pluggable, production-grade wallet for Django, backed by Paystack. Drop it into an e-commerce site, marketplace, fintech or any app that needs users to hold, send and receive money — and switch off anything you don't need.
- Fund wallets with card, bank, USSD, QR, mobile money, bank transfer or a dedicated virtual account (a personal NUBAN per user)
- Withdraw to bank accounts (Paystack Transfers) with OTP support
- Send money between wallets by wallet ID, tag, phone number or email
- Pay with wallet at checkout, with optional escrow for marketplaces
- Fees that are fully configurable: who pays (customer / merchant / platform / split), Paystack-accurate defaults, database-driven pricing per wallet, or your own calculator
- Refunds back to the payer's card, staff reversals, settlements (scheduled payouts)
- Transaction PIN, daily limits, minimum balances, webhook signature + IP checks
- Every Paystack API wrapped in one client (
wallet.paystack.PaystackClient) - Signals for every money movement, pluggable notifications, optional Celery
- REST API (Django REST Framework), Django admin, management commands,
.envconfiguration
Why it's safe with money
| Rule | What it prevents |
|---|---|
Every balance change locks the wallet row (SELECT … FOR UPDATE) and is recorded as a ledger entry with balance_after |
Double spending under concurrent requests; unexplained balances |
| Withdrawals debit before calling Paystack and reverse automatically on failure | Spending the same money twice while a transfer is in flight |
| Only a Paystack rejection reverses a withdrawal; timeouts stay pending and are reconciled | Paying out twice when Paystack succeeded but the response was lost |
| Webhooks are signature-checked, stored, de-duplicated and processed idempotently | Double credits from Paystack retries or a webhook racing a manual verify |
| Wallets are credited from what Paystack actually collected, never from the client | Under-payment and tampered amounts |
| Signals fire only after the database commits | Sending "you've been paid" for a transaction that rolled back |
Idempotency-Key support on every money-moving endpoint |
Double charges when a mobile app retries after a timeout |
| Per-user rate limits on lookups, PINs, OTPs and payments | Phone-number enumeration, PIN/OTP guessing, abuse |
| Audit log of every refund, reversal, escrow decision and lock | "Who did this?" |
These are covered by 520+ tests, including race-condition tests against PostgreSQL, and a ledger benchmark that proves the books balance under load.
Installation
pip install django-paystack-wallet # core
pip install "django-paystack-wallet[celery]" # + background tasks
pip install "django-paystack-wallet[export]" # + Excel/PDF exports
# settings.py
from pathlib import Path
from wallet.conf import load_env_file
BASE_DIR = Path(__file__).resolve().parent.parent
load_env_file(BASE_DIR / '.env') # optional: or use python-dotenv / django-environ
INSTALLED_APPS = [
# ...
'rest_framework',
'djmoney',
'wallet',
]
# urls.py
urlpatterns = [
# ...
path('wallet/', include('wallet.urls')),
]
cp .env.example .env # then set PAYSTACK_SECRET_KEY / PAYSTACK_PUBLIC_KEY
python manage.py migrate
python manage.py sync_banks
python manage.py check # the package validates its own configuration
In the Paystack dashboard set the
webhook URL to https://your-domain.com/wallet/webhook/.
That's it: every user gets a wallet automatically, and the API is live under /wallet/api/.
Try it in 5 minutes
The demo/ folder is a small Django site built on the package: fund a
wallet through real Paystack test checkout, send money by phone number, withdraw to a
bank, save cards, buy from a mini marketplace with escrow, and watch webhooks arrive.
pip install -e ".[all]"
cd demo && cp .env.example .env # add your Paystack TEST keys
python manage.py migrate && python manage.py seed_demo && python manage.py runserver
A quick tour
from wallet.services import WalletService
service = WalletService()
wallet = service.get_wallet(request.user)
# Fund it (redirect the user to checkout['authorization_url'])
checkout = service.initialize_deposit(wallet, 5000, callback_url='https://shop.com/paid')
# Send money — by phone number, tag, email or wallet id
service.set_phone_number(wallet, '0803 123 4567')
service.transfer(wallet, '08099998888', 1500, description='Lunch')
service.transfer(wallet, '@ada', 2000)
# Pay a seller, held in escrow until delivery
order_payment = service.pay(buyer_wallet, 25000, merchant_wallet=seller_wallet, escrow=True)
service.release_payment(order_payment) # or service.cancel_payment(order_payment)
# Withdraw to a bank account
account = service.add_bank_account(wallet, bank_code='058', account_number='0123456789')
txn, transfer = service.withdraw_to_bank(wallet, 10000, account)
if txn.requires_otp:
service.finalize_withdrawal(txn, otp='123456')
# Anything else Paystack offers
from wallet.paystack import get_paystack_client
paystack = get_paystack_client()
paystack.subscriptions.create(customer='CUS_xxx', plan='PLN_xxx')
React to money movement with signals:
from django.dispatch import receiver
from wallet.signals import deposit_completed, transfer_completed
@receiver(deposit_completed)
def fulfil(sender, transaction, wallet, **kwargs):
...
Use only what you need
Everything is switchable with a setting or environment variable:
WALLET_ENABLE_WITHDRAWALS=false
WALLET_ENABLE_INTERNAL_TRANSFERS=true
WALLET_ENABLE_PAYMENTS=true
WALLET_ENABLE_DEDICATED_ACCOUNTS=false
WALLET_ENABLE_SETTLEMENTS=false
WALLET_ENABLE_FEES=true
WALLET_REQUIRE_TRANSACTION_PIN=true
What is core and what is pluggable:
| Core (always there) | Pluggable (opt in / replace) |
|---|---|
| Ledger, wallets, transactions, locking | Notifications (WALLET_NOTIFICATION_BACKENDS) |
| Paystack client & webhook handling | Fee pricing (WALLET_FEE_CALCULATOR, DB configs) |
| Fee mechanics (who pays, how much moves) | Phone normalisation (WALLET_PHONE_NUMBER_NORMALIZER) |
| Signals | Background processing (Celery) |
REST API permissions (WALLET_API_PERMISSION_CLASSES), URLs, admin |
|
| Webhook forwarding to other services, Excel/PDF export |
Documentation
- Installation
- Configuration & environment variables
- Usage guide: deposits, transfers by phone, payments & escrow, withdrawals, fees, refunds, settlements
- REST API reference
- Extending: signals, notifications, custom fees, your own endpoints, the Paystack client
Load testing
loadtest/benchmark_ledger.py measures ledger throughput on your database and checks
the books balance afterwards; loadtest/locustfile.py load-tests your full HTTP stack.
See loadtest/README.md.
Development
pip install -e ".[dev]"
pytest # SQLite
pytest --ds=your_postgres_settings # includes the race-condition tests
License
MIT — see LICENSE.
Metadata
Release files for django-paystack-wallet 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_paystack_wallet-1.0.0.tar.gz | 182.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_paystack_wallet-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 339.6 kB
Release files / django_paystack_wallet-1.0.0.tar.gz
| Download URL | django_paystack_wallet-1.0.0.tar.gz |
|---|---|
| Size | 182.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c4fd9f88f7c1546d176b9b391d0111345b988a92e87bdffddced45e2ce010f04
|
|
BLAKE2b-256 checksum How to use checksums |
5d1491f84d9099475fccc6324abf1526adb2eb03f870d32154262b493720fc57
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / django_paystack_wallet-1.0.0-py3-none-any.whl
| Download URL | django_paystack_wallet-1.0.0-py3-none-any.whl |
|---|---|
| Size | 156.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7afd5b754271821fc568ac62461cec1a3f525154037d2dd2b86a47ab90bc3aee
|
|
BLAKE2b-256 checksum How to use checksums |
66cb96560d1ad5a217f99567d4ceed0b1de17d1550589b3a0613906ad63f1c07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log