Bonaventure OgetoBy Bonaventure Ogeto|

Verify Paystack Payments in React Native

After checkout, react-native-paystack-webview calls your onSuccess callback with the transaction reference. Send this reference to your backend. Your backend calls GET https://api.paystack.co/transaction/verify/:reference with the secret key. Return the result to React Native and navigate to a success or failure screen.

React Native Payment Service

// services/paymentService.ts
const BASE_URL = 'https://your-backend.com';

export interface VerifyResult {
  verified: boolean;
  amount?: number;
  currency?: string;
  reference?: string;
  error?: string;
}

export async function verifyPayment(reference: string): Promise<VerifyResult> {
  try {
    var response = await fetch(
      BASE_URL + '/api/pay/verify?reference=' + encodeURIComponent(reference),
      {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
      }
    );

    var data = await response.json();

    if (response.ok && data.verified) {
      return {
        verified: true,
        amount: data.amount,
        currency: data.currency,
        reference: data.reference,
      };
    }

    return { verified: false, error: data.error || 'Payment not verified' };
  } catch (error) {
    return { verified: false, error: 'Network error. Please check your connection.' };
  }
}

Verification Screen

// screens/VerifyPaymentScreen.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, ActivityIndicator, TouchableOpacity, StyleSheet, Alert } from 'react-native';
import { verifyPayment, VerifyResult } from '../services/paymentService';

interface Props {
  route: { params: { reference: string } };
  navigation: any;
}

export function VerifyPaymentScreen({ route, navigation }: Props) {
  var { reference } = route.params;
  var [loading, setLoading] = useState(true);
  var [result, setResult] = useState<VerifyResult | null>(null);

  useEffect(() => {
    runVerification();
  }, []);

  async function runVerification() {
    setLoading(true);
    var res = await verifyPayment(reference);
    setResult(res);
    setLoading(false);

    if (res.verified) {
      // Navigate to success screen after a brief delay
      setTimeout(() => {
        navigation.replace('OrderSuccess', { reference: res.reference });
      }, 1500);
    }
  }

  if (loading) {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#4CAF50" />
        <Text style={styles.loadingText}>Verifying your payment...</Text>
      </View>
    );
  }

  if (!result?.verified) {
    return (
      <View style={styles.center}>
        <Text style={styles.errorTitle}>Verification Failed</Text>
        <Text style={styles.errorText}>{result?.error}</Text>
        <TouchableOpacity style={styles.retryButton} onPress={runVerification}>
          <Text style={styles.retryText}>Retry</Text>
        </TouchableOpacity>
        <TouchableOpacity onPress={() => navigation.navigate('Support', { reference })}>
          <Text style={styles.supportText}>Contact Support</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.center}>
      <Text style={styles.successTitle}>Payment Confirmed!</Text>
      <Text>{result.currency} {result.amount?.toFixed(2)}</Text>
      <Text style={styles.ref}>Ref: {result.reference}</Text>
    </View>
  );
}

var styles = StyleSheet.create({
  center: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24 },
  loadingText: { marginTop: 12, color: '#666' },
  errorTitle: { fontSize: 20, fontWeight: 'bold', color: '#e53935', marginBottom: 8 },
  errorText: { color: '#666', marginBottom: 24, textAlign: 'center' },
  retryButton: { backgroundColor: '#4CAF50', padding: 12, borderRadius: 8, marginBottom: 12 },
  retryText: { color: '#fff', fontWeight: 'bold' },
  supportText: { color: '#666', textDecorationLine: 'underline' },
  successTitle: { fontSize: 24, fontWeight: 'bold', color: '#4CAF50', marginBottom: 8 },
  ref: { color: '#999', marginTop: 8, fontSize: 12 },
});

Learn More

Key Takeaways

  • The transaction reference comes from the onSuccess callback of react-native-paystack-webview.
  • Verification must happen on your backend — never call the Paystack API with a secret key from React Native.
  • Call your backend verify endpoint, check the verified flag, and navigate accordingly.
  • Handle network failures gracefully — mobile networks are unreliable. Always show a retry option.
  • Your backend should also receive a webhook from Paystack. The webhook is more reliable than the mobile verification call.
  • Store the reference locally (AsyncStorage) before checkout in case the app crashes during verification.

Frequently Asked Questions

What reference format does react-native-paystack-webview return?
The onSuccess callback receives an object. The reference is typically in response.transactionRef.reference or response.transactionRef depending on the library version. Log the full response object in development to see the exact shape.
What if verification fails but the money was deducted?
Your backend webhook handler receives the charge.success event from Paystack regardless of what happens in the mobile app. If the webhook fires, the payment was successful. Check your webhook logs. The mobile verification failure was likely a network issue on the device.
Should I store the reference in AsyncStorage before checkout?
Yes. Store the reference before opening the WebView. If the app crashes or the user force-closes it during or after checkout, you can detect on the next launch that there is an unverified payment and run verification again.
How do I verify payments when the user is offline?
Queue the verification for when connectivity returns. Use NetInfo from @react-native-community/netinfo to detect when the network is available, then run the queued verification. In the meantime, your backend webhook should have already confirmed the payment.

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