Accept Payments with Paystack in Vue
To accept Paystack payments in Vue, create a backend API that initializes transactions with your Paystack secret key. Your Vue frontend calls that API, gets an access code, and opens the Paystack inline popup using PaystackPop. After payment, call your backend to verify the transaction before granting value.
Architecture Overview
Vue is a frontend framework. Like React and Angular, it runs in the browser. Paystack's API requires a secret key for initialization and verification, so you need a backend. The flow:
- Vue component collects the customer email and knows the amount.
- Vue calls your backend API with the email and amount.
- Your backend calls Paystack's Initialize Transaction endpoint and returns the access code or authorization URL.
- Vue opens the Paystack inline popup (or redirects to the authorization URL).
- After payment, Vue calls your backend to verify the transaction.
This guide uses Vue 3 with the Composition API. The backend examples use Express.js, but any server-side language works. If you use Nuxt, see Accept Payments with Paystack in Nuxt instead, which handles the backend with Nuxt server routes.
Backend Setup
Create a minimal Express server with initialize and verify endpoints:
// server.js
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({ origin: 'http://localhost:5173' })); // Vite default port
app.use(express.json());
const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;
app.post('/api/pay', async (req, res) => {
const { email, amount } = req.body;
const reference = 'vue_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
amount: Math.round(amount * 100),
reference,
}),
});
const data = await response.json();
if (!data.status) return res.status(400).json({ error: data.message });
res.json({
access_code: data.data.access_code,
authorization_url: data.data.authorization_url,
reference: data.data.reference,
});
});
app.get('/api/verify', async (req, res) => {
const { reference } = req.query;
const response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{ headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET } }
);
const data = await response.json();
if (data.status && data.data.status === 'success') {
return res.json({ success: true, amount: data.data.amount / 100, currency: data.data.currency });
}
res.status(400).json({ success: false });
});
app.listen(4000, () => console.log('Backend running on port 4000'));
Start the server: PAYSTACK_SECRET_KEY=sk_test_xxx node server.js
Inline Popup Checkout in Vue
Create a checkout component that loads Paystack Inline.js and opens the popup:
<!-- src/components/PaystackCheckout.vue -->
<script setup>
import { ref, onMounted } from 'vue';
const email = ref('');
const loading = ref(false);
const amount = 5000; // 5,000 NGN
const API_URL = 'http://localhost:4000';
onMounted(() => {
const script = document.createElement('script');
script.src = 'https://js.paystack.co/v2/inline.js';
script.async = true;
document.head.appendChild(script);
});
async function handlePay() {
if (!email.value) return;
loading.value = true;
try {
const res = await fetch(API_URL + '/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, amount }),
});
const data = await res.json();
if (!data.access_code) {
alert(data.error || 'Initialization failed');
loading.value = false;
return;
}
const popup = new window.PaystackPop();
popup.checkout({
accessCode: data.access_code,
onSuccess: async (transaction) => {
const verifyRes = await fetch(
API_URL + '/api/verify?reference=' + transaction.reference
);
const result = await verifyRes.json();
if (result.success) {
alert('Payment confirmed: ' + result.currency + ' ' + result.amount);
} else {
alert('Verification failed');
}
loading.value = false;
},
onCancel: () => {
loading.value = false;
},
});
} catch (err) {
alert('Network error');
loading.value = false;
}
}
</script>
<template>
<div class="checkout">
<h1>Checkout</h1>
<input v-model="email" type="email" placeholder="Your email" />
<p>Amount: NGN {{ amount.toLocaleString() }}</p>
<button @click="handlePay" :disabled="loading || !email">
{{ loading ? 'Processing...' : 'Pay Now' }}
</button>
</div>
</template>
The component loads the Paystack script in onMounted, initializes the transaction through your backend, and opens the popup with the access code. After success, it verifies the payment on your backend before showing a confirmation.
Reusable usePaystack Composable
If you accept payments in multiple places, extract the logic into a composable:
// src/composables/usePaystack.js
import { ref, onMounted } from 'vue';
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:4000';
export function usePaystack() {
const loading = ref(false);
const scriptLoaded = ref(false);
onMounted(() => {
if (document.querySelector('script[src*="paystack"]')) {
scriptLoaded.value = true;
return;
}
const script = document.createElement('script');
script.src = 'https://js.paystack.co/v2/inline.js';
script.onload = () => { scriptLoaded.value = true; };
document.head.appendChild(script);
});
async function pay({ email, amount, onSuccess, onCancel }) {
loading.value = true;
const res = await fetch(API_URL + '/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, amount }),
});
const data = await res.json();
if (!data.access_code) {
loading.value = false;
throw new Error(data.error || 'Failed to initialize');
}
const popup = new window.PaystackPop();
popup.checkout({
accessCode: data.access_code,
onSuccess: async (transaction) => {
const verifyRes = await fetch(
API_URL + '/api/verify?reference=' + transaction.reference
);
const result = await verifyRes.json();
loading.value = false;
if (result.success && onSuccess) {
onSuccess(result);
}
},
onCancel: () => {
loading.value = false;
if (onCancel) onCancel();
},
});
}
return { pay, loading, scriptLoaded };
}
Usage in any component:
<script setup>
import { usePaystack } from '../composables/usePaystack';
const { pay, loading } = usePaystack();
function buyProduct() {
pay({
email: 'customer@example.com',
amount: 2500,
onSuccess: (result) => {
console.log('Paid:', result.currency, result.amount);
},
});
}
</script>
<template>
<button @click="buyProduct" :disabled="loading">Buy for NGN 2,500</button>
</template>
Redirect Checkout
For a simpler approach where the customer leaves your Vue app to pay on Paystack's hosted page:
async function handleRedirectPay() {
const res = await fetch(API_URL + '/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.value, amount: 5000 }),
});
const data = await res.json();
if (data.authorization_url) {
window.location.href = data.authorization_url;
}
}
Set the callback URL in your backend initialization to point to a route in your Vue app (for example, /payment/callback). Use Vue Router to handle that route and verify the payment on mount using the reference from the query string.
Production Checklist
- CORS. Restrict your backend CORS to your Vue app's production domain.
- HTTPS. Both your Vue app and backend must use HTTPS in production.
- Environment variables. Use
VITE_API_URLto point to your production backend. Switch to live Paystack keys. - Amount validation. Validate the amount on your backend. Look up the correct price from your database rather than trusting the frontend.
- Webhooks. Set up a webhook endpoint on your backend for reliable payment confirmation.
- Idempotent fulfillment. Both webhook and callback can fire for the same transaction. Use a database flag to prevent double-granting.
Key Takeaways
- ✓Vue runs in the browser. Your Paystack secret key must live on a separate backend server. Never import it in a .vue file.
- ✓The Composition API composable pattern works well for encapsulating Paystack payment logic. Create a usePaystack composable for reuse across components.
- ✓Load Paystack Inline.js dynamically in onMounted. Do not install it as an npm package; load the official CDN script.
- ✓After the popup onSuccess callback fires, verify the transaction on your backend. The popup callback alone is not proof of payment.
- ✓Amounts must be in the smallest currency unit (kobo for NGN, pesewas for GHS, cents for ZAR/KES/USD).
- ✓Use reactive refs for loading states to disable the pay button during processing and prevent double submissions.
Frequently Asked Questions
- Do I need the vue-paystack npm package?
- No. Community packages exist but are not maintained by Paystack and may lag behind API changes. Loading the official Paystack Inline.js from the CDN gives you the same functionality with fewer dependencies.
- Can I use Paystack with Vue 2?
- Yes. The Paystack Inline.js script is framework-agnostic. In Vue 2, you would use the Options API instead of the Composition API, but the Paystack integration logic is the same. Load the script, call your backend, and open the popup.
- How do I handle the popup on mobile devices?
- The Paystack inline popup works on mobile browsers. However, some mobile browsers block popups. If you experience issues, use the redirect checkout approach instead, which works reliably on all devices and browsers.
- What if the customer pays but my verification call fails?
- This is why webhooks are essential. Even if the verification call from your Vue app fails (network error, server down), the webhook will deliver the payment confirmation to your backend independently. Set up a webhook handler that can fulfill orders on its own.
- Can I use Pinia to manage payment state?
- You can, but it is usually not necessary. Payment state is short-lived: the customer pays, you verify, and the transaction is done. A composable with reactive refs handles this well. Use Pinia if you need to share payment history or subscription status across multiple components.
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