To accept Paystack payments in Nuxt, create a server route at server/api/pay.post.ts that initializes transactions using your secret key stored in runtimeConfig. Your Nuxt page loads Paystack Inline.js, calls the server route, and opens the popup with the access code. After payment, another server route verifies the transaction.
Nuxt is the full-stack framework for Vue. Unlike a plain Vue SPA, Nuxt gives you server routes where you can safely store your Paystack secret key and make API calls to Paystack. You do not need a separate Express, Django, or Laravel backend. Everything lives in one project.
Nuxt server routes run on the server during both SSR and as API endpoints. When you create a file at server/api/pay.post.ts, Nuxt exposes it at /api/pay as a POST endpoint. Your Vue pages call these endpoints with $fetch or useFetch.
Add your Paystack keys to the environment and Nuxt config:
# .env
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxx
PAYSTACK_PUBLIC_KEY=pk_test_xxxxxxxxxxxx
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
paystackSecretKey: process.env.PAYSTACK_SECRET_KEY,
public: {
paystackPublicKey: process.env.PAYSTACK_PUBLIC_KEY,
},
},
});
The paystackSecretKey is only available in server routes via useRuntimeConfig(). It never reaches the browser. The public key is safe for the frontend.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// server/api/pay.post.ts
export default defineEventHandler(async (event) => {
var config = useRuntimeConfig();
var body = await readBody(event);
var email = body.email;
var amount = body.amount;
var reference = 'nuxt_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + config.paystackSecretKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: Math.round(amount * 100),
reference: reference,
}),
});
var data = await response.json();
if (!data.status) {
throw createError({ statusCode: 400, message: data.message });
}
return {
access_code: data.data.access_code,
authorization_url: data.data.authorization_url,
reference: data.data.reference,
};
});
This runs on the server. The secret key never leaves the server environment.
Create a checkout page that loads Paystack Inline.js and opens the popup:
<!-- pages/checkout.vue -->
<script setup>
useHead({
script: [{ src: 'https://js.paystack.co/v2/inline.js', defer: true }],
});
var email = ref('');
var loading = ref(false);
var amount = 5000; // 5,000 NGN
async function handlePay() {
if (!email.value) return;
loading.value = true;
try {
var data = await $fetch('/api/pay', {
method: 'POST',
body: { email: email.value, amount: amount },
});
var popup = new window.PaystackPop();
popup.checkout({
accessCode: data.access_code,
onSuccess: async function(transaction) {
var result = await $fetch('/api/verify', {
params: { reference: transaction.reference },
});
if (result.verified) {
navigateTo('/payment/success?ref=' + transaction.reference);
}
loading.value = false;
},
onCancel: function() {
loading.value = false;
},
});
} catch (err) {
alert('Payment initialization failed');
loading.value = false;
}
}
</script>
<template>
<div>
<h1>Checkout</h1>
<input v-model="email" type="email" placeholder="Email address" />
<p>Amount: NGN {{ amount.toLocaleString() }}</p>
<button @click="handlePay" :disabled="loading || !email">
{{ loading ? 'Processing...' : 'Pay Now' }}
</button>
</div>
</template>
Nuxt's $fetch calls your server routes directly. No need to configure CORS or specify a separate API URL. The useHead composable loads the Paystack script.
// server/api/verify.get.ts
export default defineEventHandler(async (event) => {
var config = useRuntimeConfig();
var query = getQuery(event);
var reference = query.reference;
if (!reference) {
throw createError({ statusCode: 400, message: 'Reference required' });
}
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(String(reference)),
{
headers: { Authorization: 'Bearer ' + config.paystackSecretKey },
}
);
var data = await response.json();
if (data.status && data.data.status === 'success') {
// Update order in database
return {
verified: true,
amount: data.data.amount / 100,
currency: data.data.currency,
reference: data.data.reference,
};
}
throw createError({ statusCode: 400, message: 'Payment not verified' });
});
For redirect checkout, send the customer to the authorization URL and handle the callback:
async function handleRedirectPay() {
var data = await $fetch('/api/pay', {
method: 'POST',
body: { email: email.value, amount: amount },
});
if (data.authorization_url) {
window.location.href = data.authorization_url;
}
}
Set the callback URL in your initialization to point to a Nuxt page:
<!-- pages/payment/callback.vue -->
<script setup>
var route = useRoute();
var { data, error } = await useFetch('/api/verify', {
params: { reference: route.query.reference || route.query.trxref },
});
</script>
<template>
<div v-if="data">
<h1>Payment Successful</h1>
<p>{{ data.currency }} {{ data.amount }}</p>
</div>
<div v-else-if="error">
<p>Verification failed. Please contact support.</p>
</div>
<div v-else>
<p>Verifying payment...</p>
</div>
</template>
With Nuxt SSR, useFetch runs the verification on the server during page load. The customer sees the result immediately without a loading spinner on most connections.
sk_test_ and pk_test_ with live keys in your production .env file.KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course