Verify Paystack Payments in FastAPI
To verify a Paystack payment in FastAPI, make an async GET request to the Paystack verify endpoint using httpx. Check that data.status is "success", the amount matches your database, and the currency is correct. Use an async database update with a condition to prevent double-granting.
Verification Service
# services.py
import httpx
from decouple import config
PAYSTACK_SECRET = config('PAYSTACK_SECRET_KEY')
async def verify_paystack_transaction(reference: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(
'https://api.paystack.co/transaction/verify/' + reference,
headers={'Authorization': 'Bearer ' + PAYSTACK_SECRET},
)
return response.json()
async def verify_and_fulfill(reference: str, db) -> dict:
order = await db.get_order(reference)
if not order:
return {'success': False, 'error': 'Order not found'}
if order.paid:
return {'success': True, 'already_fulfilled': True}
result = await verify_paystack_transaction(reference)
if not result.get('status') or result['data']['status'] != 'success':
return {'success': False, 'error': 'Payment not successful'}
if result['data']['amount'] != order.amount_in_kobo:
return {'success': False, 'error': 'Amount mismatch'}
if result['data']['currency'] != order.currency:
return {'success': False, 'error': 'Currency mismatch'}
updated = await db.fulfill_order(reference)
if not updated:
return {'success': True, 'already_fulfilled': True}
return {
'success': True,
'already_fulfilled': False,
'amount': order.amount_in_kobo / 100,
'currency': order.currency,
}
Verification Endpoint
# main.py
from fastapi import FastAPI, HTTPException, Depends
@app.get('/api/verify')
async def verify_payment(reference: str):
result = await verify_and_fulfill(reference, db)
if not result['success']:
raise HTTPException(status_code=400, detail=result['error'])
return result
Verification as a Dependency
# dependencies.py
from fastapi import Query, HTTPException
async def verified_payment(reference: str = Query(...)):
result = await verify_paystack_transaction(reference)
if not result.get('status') or result['data']['status'] != 'success':
raise HTTPException(status_code=400, detail='Payment not verified')
return result['data']
# Usage in an endpoint:
@app.get('/api/payment/confirmed')
async def payment_confirmed(payment_data: dict = Depends(verified_payment)):
return {
'amount': payment_data['amount'] / 100,
'currency': payment_data['currency'],
'email': payment_data['customer']['email'],
}
Retry Logic
import asyncio
async def verify_with_retry(reference: str, max_retries: int = 3) -> dict:
for attempt in range(max_retries):
try:
result = await verify_paystack_transaction(reference)
if result.get('status') is not None:
return result
except httpx.RequestError:
pass
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
return None
FastAPI async functions work naturally with asyncio.sleep. The event loop is not blocked during the wait.
Common Mistakes
- Using requests instead of httpx. The requests library blocks the event loop in async endpoints. Use httpx.AsyncClient.
- Not validating the amount. Always compare the Paystack amount against your database, not the client.
- Creating a new AsyncClient per request. For production, use a shared client with connection pooling.
- Mixing test and live keys. Both initialization and verification must use the same mode.
Key Takeaways
- ✓Use httpx.AsyncClient for non-blocking verification calls in FastAPI async endpoints.
- ✓Create a reusable verification dependency or service function for consistent logic across endpoints.
- ✓Always validate amount and currency against your database, not client-submitted values.
- ✓Use conditional database updates for idempotent fulfillment that prevents double-granting.
- ✓Share the same verification function between your callback endpoint and webhook handler.
- ✓FastAPI Pydantic models ensure consistent response shapes for success and error cases.
Frequently Asked Questions
- Can I reuse the httpx client across requests?
- Yes. Create the client in a lifespan event and store it on the app state. This enables connection pooling and is more efficient than creating a new client per request.
- How do I handle verification timeouts?
- Set a timeout on your httpx client: httpx.AsyncClient(timeout=10.0). If the request times out, retry. The Paystack transaction state does not change.
- Should I verify in the webhook handler too?
- Yes. Call the same verify_and_fulfill function from both your callback endpoint and webhook handler. The idempotent fulfillment logic ensures only one of them actually processes the order.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon