Bonaventure OgetoBy Bonaventure Ogeto|

Verify Paystack Payments in Vue

Vue cannot verify Paystack payments directly because verification requires your secret key. Build a backend endpoint that calls Paystack's Verify Transaction API, then call that endpoint from Vue after the popup callback or redirect. Check that data.data.status equals "success" and that the amount matches what you expected.

Why Verification Happens on the Backend

When a customer finishes paying through the Paystack popup, your Vue component receives a callback with the transaction reference. That callback runs in the browser. It tells you the customer completed the payment form, but it does not prove the money actually landed. A user could fake the callback, or the network could drop before Paystack finished processing.

The Paystack Verify Transaction API (GET /transaction/verify/:reference) is the only reliable way to confirm a payment. It requires your secret key in the Authorization header. That key must never appear in client-side code. So your Vue app calls your own backend, and your backend calls Paystack.

This guide uses Express for the backend examples, but the pattern is the same for Django, Laravel, FastAPI, or any server framework. The Vue side stays identical regardless of backend choice.

Build the Backend Verification Endpoint

Create a simple endpoint that takes a reference, calls Paystack, and returns the result:

// server.js (Express)
const express = require('express');
const app = express();

const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;

app.get('/api/verify', async (req, res) => {
  var reference = req.query.reference;

  if (!reference) {
    return res.status(400).json({ error: 'Reference is required' });
  }

  var response = await fetch(
    'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
    {
      headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET },
    }
  );

  var data = await response.json();

  if (!data.status) {
    return res.status(400).json({ verified: false, message: data.message });
  }

  var txn = data.data;

  // Look up the expected amount from your database
  // var order = await db.orders.findOne({ reference: reference });
  // if (txn.amount !== order.expectedAmountInKobo) { return res.status(400).json({ verified: false }); }

  if (txn.status === 'success') {
    return res.json({
      verified: true,
      amount: txn.amount / 100,
      currency: txn.currency,
      reference: txn.reference,
      channel: txn.channel,
      paidAt: txn.paid_at,
    });
  }

  res.status(400).json({ verified: false, status: txn.status });
});

app.listen(4000);

The endpoint does three things: calls Paystack, checks the status, and validates the amount. The amount check (commented out above) is critical in production. Without it, a customer could initialize a transaction for 100 NGN and you would verify a 100 NGN payment when they should have paid 10,000 NGN.

Verify After the Inline Popup

In your Vue component, call your backend verification endpoint inside the popup's onSuccess callback:

<script setup>
import { ref } from 'vue';

var API_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';
var verifyState = ref('idle'); // idle | verifying | success | failed
var verifyResult = ref(null);

async function onPaystackSuccess(transaction) {
  verifyState.value = 'verifying';

  try {
    var res = await fetch(
      API_URL + '/api/verify?reference=' + transaction.reference
    );
    var data = await res.json();

    if (data.verified) {
      verifyState.value = 'success';
      verifyResult.value = data;
    } else {
      verifyState.value = 'failed';
    }
  } catch (err) {
    verifyState.value = 'failed';
  }
}
</script>

<template>
  <div v-if="verifyState === 'verifying'">
    <p>Verifying your payment...</p>
  </div>
  <div v-else-if="verifyState === 'success'">
    <h2>Payment Confirmed</h2>
    <p>{{ verifyResult.currency }} {{ verifyResult.amount }} received.</p>
    <p>Reference: {{ verifyResult.reference }}</p>
  </div>
  <div v-else-if="verifyState === 'failed'">
    <p>Verification failed. Contact support with your reference number.</p>
  </div>
</template>

The verifyState ref drives the UI. The user sees a loading indicator during verification, a confirmation on success, and a clear error message on failure. Keep the reference visible so the customer can share it with support if needed.

Verify After Redirect Checkout

When you use redirect checkout instead of the inline popup, Paystack sends the customer back to your callback URL with ?trxref=xxx&reference=xxx in the query string. Create a callback page in Vue Router that verifies on mount:

<!-- src/views/PaymentCallback.vue -->
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';

var route = useRoute();
var router = useRouter();
var API_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';

var status = ref('verifying');
var result = ref(null);

onMounted(async function() {
  var reference = route.query.reference || route.query.trxref;

  if (!reference) {
    status.value = 'error';
    return;
  }

  try {
    var res = await fetch(
      API_URL + '/api/verify?reference=' + reference
    );
    var data = await res.json();

    if (data.verified) {
      status.value = 'success';
      result.value = data;
    } else {
      status.value = 'failed';
    }
  } catch (err) {
    status.value = 'error';
  }
});
</script>

<template>
  <div v-if="status === 'verifying'">Verifying payment...</div>
  <div v-else-if="status === 'success'">
    <h1>Payment Successful</h1>
    <p>{{ result.currency }} {{ result.amount }}</p>
  </div>
  <div v-else>
    <p>Something went wrong. Please contact support.</p>
  </div>
</template>

Register this route in your Vue Router config:

// router/index.js
{ path: '/payment/callback', component: () => import('../views/PaymentCallback.vue') }

Set this URL as the callback_url when initializing the transaction on your backend.

Reusable Verification Composable

If you verify payments in multiple places, extract the logic:

// src/composables/usePaystackVerify.js
import { ref } from 'vue';

var API_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';

export function usePaystackVerify() {
  var loading = ref(false);
  var verified = ref(false);
  var error = ref(null);
  var transaction = ref(null);

  async function verify(reference) {
    loading.value = true;
    error.value = null;

    try {
      var res = await fetch(
        API_URL + '/api/verify?reference=' + reference
      );
      var data = await res.json();

      if (data.verified) {
        verified.value = true;
        transaction.value = data;
      } else {
        error.value = 'Payment not confirmed';
      }
    } catch (err) {
      error.value = 'Network error during verification';
    } finally {
      loading.value = false;
    }
  }

  return { verify, loading, verified, error, transaction };
}

Use it in any component with a single line: var { verify, loading, verified, transaction } = usePaystackVerify();

Amount Validation Is Not Optional

Verifying that status === "success" is necessary but not sufficient. You must also check that the amount matches what you charged. Here is why:

  1. Your Vue app sends amount: 10000 (100 NGN in kobo) to your backend.
  2. An attacker intercepts the request and changes it to amount: 100 (1 NGN).
  3. Paystack initializes a 1 NGN transaction. The attacker pays 1 NGN.
  4. Your backend verifies the reference. Paystack says "success." You deliver the product.

The fix: your backend should look up the correct price from your database, not from the frontend request. When verifying, compare data.data.amount against the expected amount in your database.

// In your verify endpoint
var txn = data.data;
var order = await db.orders.findOne({ reference: reference });

if (txn.status === 'success' && txn.amount === order.amountInKobo) {
  order.status = 'paid';
  await order.save();
  return res.json({ verified: true, amount: txn.amount / 100 });
}

return res.status(400).json({ verified: false, reason: 'Amount mismatch or payment not successful' });

Webhooks as Your Safety Net

Frontend verification can fail. The customer might close their browser after paying, or your backend could be temporarily down when the popup callback fires. Webhooks solve this.

Paystack sends a charge.success webhook to your backend independently of the frontend flow. Your backend should be able to fulfill orders from either the verification call or the webhook, whichever arrives first. Use a database flag to prevent double fulfillment.

Vue cannot receive webhooks because it runs in the browser. See Handle Paystack Webhooks in Vue for the architectural explanation and links to backend webhook guides.

Ship Payments Faster

Key Takeaways

  • Vue runs in the browser. Verification must happen on your backend because it requires the Paystack secret key.
  • After the Paystack popup onSuccess callback fires, call your backend to verify. Do not trust the popup callback alone.
  • Your backend calls GET https://api.paystack.co/transaction/verify/:reference and checks that data.data.status is "success".
  • Always compare the verified amount and currency against your database records. A customer could tamper with frontend values.
  • For redirect checkout, read the reference from the URL query string on your callback page and verify immediately on mount.
  • Use reactive refs for verification state so your UI shows loading, success, or error states clearly.
  • Webhooks are the ultimate safety net. Even if verification from the frontend fails, the webhook will confirm the payment.

Frequently Asked Questions

Can I verify a Paystack payment directly from Vue without a backend?
No. The Verify Transaction API requires your Paystack secret key in the Authorization header. If you call it from Vue, you expose the key to anyone who opens browser DevTools. You must call it from your backend.
What does the Paystack Verify API return?
It returns a JSON object with data.status, data.amount (in kobo/pesewas/cents), data.currency, data.reference, data.channel, data.paid_at, and more. Check data.data.status for "success" to confirm the payment went through.
Should I verify on the frontend or rely on webhooks only?
Both. Verify from the frontend for immediate user feedback. Use webhooks as the reliable backend confirmation. Either path should be able to fulfill the order independently, with a database flag to prevent double fulfillment.
What if the verification call times out?
Show the customer a message like "We are confirming your payment. You will receive a confirmation email shortly." Your webhook handler will pick up the payment and fulfill the order. Do not charge the customer again.
How do I handle the case where the customer paid but the amount does not match?
This usually means someone tampered with the amount on the frontend. Log the mismatch, do not fulfill the order, and flag it for manual review. The customer can contact support with their reference number.

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