A robust backend wallet service built with Django and Django Ninja that enables users to deposit money via Paystack, manage wallet balances, view transaction history, and transfer funds between users. The service supports both JWT authentication (Google OAuth) and API key-based authentication for service-to-service access.
- Google OAuth Integration - Sign in with Google to generate JWT tokens
- JWT Authentication - Secure user authentication with access and refresh tokens
- API Key System - Service-to-service authentication with granular permissions
- Permission-based Access - API keys support
deposit,transfer, andreadpermissions
- Paystack Deposits - Initialize deposits and receive payment links
- Webhook Integration - Automatic wallet crediting via Paystack webhooks with signature validation
- Wallet Transfers - Send money to other users using their wallet numbers
- Balance Management - View current wallet balance
- Transaction History - Track all deposits and transfers
- Webhook Signature Validation - Verifies Paystack webhook authenticity
- Idempotent Webhooks - Prevents double-crediting from duplicate events
- Atomic Transfers - Ensures no partial deductions (all or nothing)
- API Key Limits - Maximum 5 active keys per user
- API Key Expiration - Keys expire based on configured duration (1H, 1D, 1M, 1Y)
- Hashed API Keys - Keys stored as SHA256 hashes for security
- Python 3.11+
- PostgreSQL (or SQLite for development)
- Paystack Account (test/live)
- Google OAuth Credentials
git clone <repository-url>
cd wallet_service# Install UV if you haven't
curl -LsSf https://astral.sh/uv/install.sh | sh
# Install project dependencies
uv syncCreate a .env file in the project root:
# Django Settings
SECRET_KEY=your-secret-key-here
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1
# Database (PostgreSQL)
DB_NAME=wallet_db
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=localhost
DB_PORT=5432
# Google OAuth
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:8000/api/auth/google/callback
# JWT Settings
JWT_SECRET_KEY=your-jwt-secret-key
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
JWT_REFRESH_TOKEN_EXPIRE_DAYS=7
# Paystack
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxxxxxxxxxxx
# API Settings
API_KEY_PREFIX=sk_live_
API_KEY_LENGTH=32
# Application URL
BASE_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
# CORS
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000- Go to Google Cloud Console
- Create a new project or select existing
- Enable Google+ API
- Create OAuth 2.0 credentials
- Add authorized redirect URI:
http://localhost:8000/api/auth/google/callback - Copy Client ID and Client Secret to
.env
- Sign up at Paystack
- Get your test API keys from the dashboard
- Add keys to
.env
# Run migrations
uv run python manage.py makemigrations
uv run python manage.py migrate
# Create superuser (optional)
uv run python manage.py createsuperuseruv run python manage.py runserverThe API will be available at http://localhost:8000
Visit http://localhost:8000/api/docs for the auto-generated OpenAPI documentation.
http://localhost:8000/api
GET /auth/googleResponse:
{
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?..."
}GET /auth/google/callback?code={code}Response:
{
"access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"user": {
"id": 1,
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe",
"wallet_number": "1234567890123",
"profile_picture": "https://..."
}
}POST /keys/create
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"name": "wallet-service",
"permissions": ["deposit", "transfer", "read"],
"expiry": "1M"
}Expiry Options: 1H (hour), 1D (day), 1M (month), 1Y (year)
Response:
{
"api_key": "sk_live_abc123xyz...",
"expires_at": "2025-01-10T12:00:00Z",
"name": "wallet-service",
"permissions": ["deposit", "transfer", "read"]
}GET /keys/list
Authorization: Bearer {jwt_token}Response:
[
{
"id": 1,
"name": "wallet-service",
"prefix": "sk_live_abc1",
"permissions": ["deposit", "transfer", "read"],
"expires_at": "2025-01-10T12:00:00Z",
"is_revoked": false,
"is_expired": false,
"is_active": true,
"created_at": "2024-12-10T12:00:00Z",
"last_used_at": "2024-12-10T14:30:00Z"
}
]POST /keys/revoke
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"key_id": 1
}Response:
{
"message": "API key revoked successfully"
}POST /keys/rollover
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"expired_key_id": "123",
"expiry": "1M"
}Response:
{
"api_key": "sk_live_new123xyz...",
"expires_at": "2025-02-10T12:00:00Z",
"name": "wallet-service",
"permissions": ["deposit", "transfer", "read"]
}All wallet endpoints support two authentication methods:
- JWT:
Authorization: Bearer {token} - API Key:
x-api-key: {api_key}
POST /wallet/deposit
Authorization: Bearer {jwt_token}
# OR
x-api-key: {api_key_with_deposit_permission}
Content-Type: application/json
{
"amount": 500000
}π‘ Note: Amount is in kobo (1 NGN = 100 kobo). So 500000 kobo = 5000 NGN.
Response:
{
"reference": "TXN-ABC123XYZ456",
"authorization_url": "https://checkout.paystack.com/...",
"amount": 5000.00
}Flow:
- Call this endpoint to get payment link
- Redirect user to
authorization_url - User completes payment on Paystack
- Paystack sends webhook to your server
- Wallet is automatically credited
GET /wallet/deposit/{reference}/status
Authorization: Bearer {jwt_token}Response:
{
"reference": "TXN-ABC123XYZ456",
"status": "success",
"amount": 5000.00
}Status Values: pending, success, failed
GET /wallet/balance
Authorization: Bearer {jwt_token}
# OR
x-api-key: {api_key_with_read_permission}Response:
{
"balance": 15000.00
}POST /wallet/transfer
Authorization: Bearer {jwt_token}
# OR
x-api-key: {api_key_with_transfer_permission}
Content-Type: application/json
{
"wallet_number": "1234567890123",
"amount": 300000
}π‘ Note: Amount is in kobo (300000 kobo = 3000 NGN).
Response:
{
"status": "success",
"message": "Transfer completed",
"reference": "TXN-XYZ789ABC123",
"amount": 3000.00
}GET /wallet/transactions
Authorization: Bearer {jwt_token}
# OR
x-api-key: {api_key_with_read_permission}Response:
{
"transactions": [
{
"id": 1,
"type": "deposit",
"amount": 5000.00,
"status": "success",
"reference": "TXN-ABC123",
"recipient_wallet_number": null,
"created_at": "2025-12-10T10:00:00Z"
},
{
"id": 2,
"type": "transfer",
"amount": 3000.00,
"status": "success",
"reference": "TXN-XYZ789",
"recipient_wallet_number": "1234567890123",
"created_at": "2025-12-10T11:00:00Z"
}
],
"count": 2
}- Go to Paystack Dashboard β Settings β Webhooks
- Add your webhook URL:
https://yourdomain.com/api/wallet/paystack/webhook - Paystack will send events to this URL
# Install ngrok
brew install ngrok # macOS
# or download from https://ngrok.com
# Start your Django server
python manage.py runserver
# In another terminal, start ngrok
ngrok http 8000
# Copy the HTTPS URL (e.g., https://abc123.ngrok-free.app)
# Add to Paystack: https://abc123.ngrok-free.app/api/wallet/paystack/webhookPOST /wallet/paystack/webhook
x-paystack-signature: {signature}This endpoint:
- Validates Paystack signature using your secret key
- Credits wallet on successful payment
- Is idempotent (won't credit twice for same transaction)
For testing without ngrok, you can manually process a pending deposit:
Create apps/wallet/management/commands/test_webhook.py:
from django.core.management.base import BaseCommand
from apps.wallet.services import WalletService
class Command(BaseCommand):
help = 'Manually process a pending deposit'
def add_arguments(self, parser):
parser.add_argument('reference', type=str)
def handle(self, *args, **options):
try:
txn = WalletService.process_successful_deposit(options['reference'])
self.stdout.write(self.style.SUCCESS(f'β
Credited: {txn.amount} NGN'))
except Exception as e:
self.stdout.write(self.style.ERROR(f'β Error: {str(e)}'))Usage:
python manage.py test_webhook TXN-ABC123XYZ456# 1. Sign in with Google
curl http://localhost:8000/api/auth/google
# 2. Create API Key
curl -X POST http://localhost:8000/api/keys/create \
-H "Authorization: Bearer {jwt_token}" \
-H "Content-Type: application/json" \
-d '{
"name": "test-key",
"permissions": ["deposit", "transfer", "read"],
"expiry": "1M"
}'
# 3. Initiate Deposit
curl -X POST http://localhost:8000/api/wallet/deposit \
-H "x-api-key: {api_key}" \
-H "Content-Type: application/json" \
-d '{"amount": 500000}'
# 4. Complete payment at the authorization_url
# 5. Check Balance
curl http://localhost:8000/api/wallet/balance \
-H "x-api-key: {api_key}"
# 6. Transfer Funds
curl -X POST http://localhost:8000/api/wallet/transfer \
-H "x-api-key: {api_key}" \
-H "Content-Type: application/json" \
-d '{
"wallet_number": "1234567890123",
"amount": 300000
}'
# 7. View Transactions
curl http://localhost:8000/api/wallet/transactions \
-H "x-api-key: {api_key}"wallet_service/
βββ manage.py
βββ pyproject.toml
βββ .env
βββ README.md
β
βββ config/ # Project settings
β βββ settings.py
β βββ urls.py
β βββ wsgi.py
β
βββ apps/
β βββ authentication/ # Google OAuth + JWT
β β βββ models.py # UserProfile with wallet_number
β β βββ schemas.py
β β βββ services.py # Google OAuth logic
β β βββ jwt_utils.py # JWT token generation
β β βββ api.py # Auth endpoints
β β
β βββ api_keys/ # API Key management
β β βββ models.py # APIKey model
β β βββ schemas.py
β β βββ services.py # Key generation/validation
β β βββ utils.py # Hashing utilities
β β βββ permissions.py # Permission validators
β β βββ authentication.py # Auth backends
β β βββ api.py # API key endpoints
β β
β βββ wallet/ # Wallet operations
β β βββ models.py # Wallet, Transaction models
β β βββ schemas.py
β β βββ services.py # Business logic
β β βββ paystack.py # Paystack integration
β β βββ webhook.py # Webhook validation
β β βββ exceptions.py # Custom exceptions
β β βββ api.py # Wallet endpoints
β β
β βββ core/ # Shared utilities
β βββ exceptions.py # APIException
β βββ ...
- Environment Variables: Never commit
.envto version control - API Keys: Only shown once during creation - store securely
- Webhook Signature: Always validated using Paystack secret key
- Atomic Transactions: Database transactions ensure consistency
- Idempotent Webhooks: Duplicate events won't credit wallet twice
- Permission Checks: API keys restricted by assigned permissions
- Balance Validation: Transfers blocked if insufficient balance
- Key Expiration: Expired keys automatically rejected
Solution: Refresh your JWT token using the refresh endpoint or sign in again.
Solution: Ensure you're using PAYSTACK_SECRET_KEY (not a separate webhook secret).
Solution: Revoke unused API keys before creating new ones.
Solution: Deposit more funds or reduce transfer amount.
Solution: Wallet number is auto-generated during user creation. Check user profile or /wallet/balance response.
Update .env for production:
DEBUG=False
ALLOWED_HOSTS=yourdomain.com
SECRET_KEY=strong-random-secret-key
PAYSTACK_SECRET_KEY=sk_live_xxxxx # Use live keysUse PostgreSQL in production:
DB_NAME=wallet_production
DB_USER=wallet_user
DB_PASSWORD=strong-password
DB_HOST=db.example.comUpdate Paystack webhook URL to production domain:
https://yourdomain.com/api/wallet/paystack/webhook
MIT License
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions, please open an issue on GitHub or contact the development team.
Built with β€οΈ using Django, Django Ninja, and Paystack