To accept Paystack payments in SvelteKit, create a server endpoint at src/routes/api/pay/+server.ts that initializes transactions using your secret key from $env/static/private. Your Svelte page loads Paystack Inline.js, calls the endpoint, and opens the popup with the access code. After payment, another endpoint verifies the transaction.
SvelteKit is the full-stack framework for Svelte. Like Nuxt for Vue and Next.js for React, SvelteKit gives you server-side capabilities alongside your Svelte frontend. Server endpoints (+server.ts) and load functions (+page.server.ts) run on the server, so your Paystack secret key never reaches the browser.
If you use plain Svelte without SvelteKit, you need a separate backend (Express, Django, etc.) for Paystack integration. SvelteKit handles everything in one project.
Store your Paystack keys in a .env file:
# .env
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxx
PUBLIC_PAYSTACK_KEY=pk_test_xxxxxxxxxxxx
SvelteKit makes private env variables available through $env/static/private and public ones through $env/static/public. Private variables are only accessible in server-side code (+server.ts, +page.server.ts, +layout.server.ts).
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
// src/routes/api/pay/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json, error } from '@sveltejs/kit';
export async function POST({ request }) {
var body = await request.json();
var email = body.email;
var amount = body.amount;
var reference = 'svelte_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + PAYSTACK_SECRET_KEY,
'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 error(400, data.message);
}
return json({
access_code: data.data.access_code,
authorization_url: data.data.authorization_url,
reference: data.data.reference,
});
}
<!-- src/routes/checkout/+page.svelte -->
<svelte:head>
<script src="https://js.paystack.co/v2/inline.js"></script>
</svelte:head>
<script>
var email = '';
var amount = 5000;
var loading = false;
async function handlePay() {
if (!email) return;
loading = true;
try {
var res = await fetch('/api/pay', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email, amount: amount }),
});
var data = await res.json();
if (!data.access_code) {
alert('Initialization failed');
loading = false;
return;
}
var popup = new window.PaystackPop();
popup.checkout({
accessCode: data.access_code,
onSuccess: async function(transaction) {
var verifyRes = await fetch('/api/verify?reference=' + transaction.reference);
var result = await verifyRes.json();
if (result.verified) {
window.location.href = '/payment/success?ref=' + transaction.reference;
}
loading = false;
},
onCancel: function() {
loading = false;
},
});
} catch (err) {
alert('Network error');
loading = false;
}
}
</script>
<div>
<h1>Checkout</h1>
<input bind:value={email} type="email" placeholder="Email address" />
<p>Amount: NGN {amount.toLocaleString()}</p>
<button on:click={handlePay} disabled={loading || !email}>
{loading ? 'Processing...' : 'Pay Now'}
</button>
</div>
Svelte's reactivity means you do not need useState or signals. Just assign to the variable and the UI updates automatically.
// src/routes/api/verify/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json, error } from '@sveltejs/kit';
export async function GET({ url }) {
var reference = url.searchParams.get('reference');
if (!reference) {
throw error(400, 'Reference required');
}
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{
headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET_KEY },
}
);
var data = await response.json();
if (data.status && data.data.status === 'success') {
return json({
verified: true,
amount: data.data.amount / 100,
currency: data.data.currency,
reference: data.data.reference,
});
}
throw error(400, 'Payment not verified');
}
For redirect checkout, the customer leaves your app. On return, use a SvelteKit load function to verify:
// src/routes/payment/callback/+page.server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
export async function load({ url }) {
var reference = url.searchParams.get('reference') || url.searchParams.get('trxref');
if (!reference) {
return { verified: false, error: 'No reference' };
}
var response = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{
headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET_KEY },
}
);
var data = await response.json();
if (data.status && data.data.status === 'success') {
return {
verified: true,
amount: data.data.amount / 100,
currency: data.data.currency,
reference: data.data.reference,
};
}
return { verified: false, error: 'Payment not verified' };
}
<!-- src/routes/payment/callback/+page.svelte -->
<script>
export var data;
</script>
{#if data.verified}
<h1>Payment Confirmed</h1>
<p>{data.currency} {data.amount}</p>
<p>Reference: {data.reference}</p>
{:else}
<h1>Verification Failed</h1>
<p>{data.error}</p>
{/if}
The load function runs on the server, so the verification happens before the page renders. The customer sees the result instantly.
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