Bonaventure OgetoBy Bonaventure Ogeto|

Verify Paystack Payments in Flask

To verify a Paystack payment in Flask, make a GET request to https://api.paystack.co/transaction/verify/:reference using the requests library. Check that the status is "success", the amount matches your stored order, and the currency is correct. Use a database update with a condition to prevent double-granting.

Verification Helper

# helpers.py
import os
import requests

PAYSTACK_SECRET = os.environ.get('PAYSTACK_SECRET_KEY')

def verify_transaction(reference):
    response = requests.get(
        'https://api.paystack.co/transaction/verify/' + reference,
        headers={'Authorization': 'Bearer ' + PAYSTACK_SECRET},
    )
    return response.json()

def verify_and_fulfill(reference, db):
    order = db.get_order(reference)
    if not order:
        return {'error': 'Order not found'}

    if order['paid']:
        return {'success': True, 'already_fulfilled': True}

    result = verify_transaction(reference)

    if not result.get('status') or result['data']['status'] != 'success':
        return {'error': 'Payment not successful'}

    if result['data']['amount'] != order['amount_kobo']:
        return {'error': 'Amount mismatch'}

    if result['data']['currency'] != order['currency']:
        return {'error': 'Currency mismatch'}

    updated = db.mark_paid(reference)
    if not updated:
        return {'success': True, 'already_fulfilled': True}

    return {'success': True, 'amount': order['amount_kobo'] / 100}

Verification Route

# app.py
from flask import request, jsonify
from helpers import verify_and_fulfill

@app.route('/api/verify')
def verify_payment():
    reference = request.args.get('reference')
    if not reference:
        return jsonify({'error': 'Missing reference'}), 400

    result = verify_and_fulfill(reference, db)

    if result.get('error'):
        return jsonify(result), 400

    return jsonify(result)

Callback Route with Verification

@app.route('/payment/callback')
def payment_callback():
    reference = request.args.get('reference') or request.args.get('trxref')

    if not reference:
        return 'Missing reference', 400

    result = verify_and_fulfill(reference, db)

    if result.get('success'):
        return render_template('success.html', reference=reference)

    return render_template('failed.html', error=result.get('error', 'Unknown'))

Retry Logic

import time

def verify_with_retry(reference, max_retries=3):
    for attempt in range(max_retries):
        try:
            result = verify_transaction(reference)
            if result.get('status') is not None:
                return result
        except requests.exceptions.RequestException:
            pass

        if attempt < max_retries - 1:
            time.sleep(2 ** attempt)

    return None

Common Mistakes

  • Trusting form data for amounts. Always compare against your database.
  • Mixing test and live keys. Both initialization and verification must use the same mode.
  • Not checking currency. An attacker could pay in a different currency.
  • Granting value before database check. Always check if the order exists and is not already fulfilled.

Ship Payments Faster

Key Takeaways

  • Verification is a single GET request to the Paystack API using the requests library.
  • Create a reusable verify_and_fulfill function for consistent logic across callbacks and webhooks.
  • Always check three things: status equals "success", amount matches your database, and currency is correct.
  • Use conditional database updates to prevent double-granting when webhook and callback race.
  • If the verify request fails, retry. The Paystack transaction state does not change.
  • Never trust redirect URL parameters as proof of payment.

Frequently Asked Questions

Can I verify a transaction multiple times?
Yes. The Paystack verify endpoint is idempotent. It returns the same result every time.
What if my verification call fails?
Retry with exponential backoff. The transaction state on Paystack does not change. The webhook will also deliver the event independently.
Should I verify in the webhook handler too?
Yes. Call the same verify_and_fulfill function from both the callback route and webhook handler. The idempotent logic ensures only one fulfills 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