McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Handle Paystack Webhooks in Svelte and SvelteKit
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Handle Paystack Webhooks in Svelte and SvelteKit

Create a webhook endpoint at src/routes/api/webhooks/paystack/+server.ts. Use request.text() to read the raw body before parsing. Compute HMAC SHA512 of the raw body using your PAYSTACK_SECRET_KEY from $env/static/private. Compare the hex digest to the x-paystack-signature header. Return a 200 response immediately, then process the event. SvelteKit server endpoints have no CSRF protection by default, so no exemption is needed.

In this article

  1. 1. SvelteKit Webhook Endpoint Setup
  2. 2. The Webhook Handler
  3. 3. Idempotency: Handle Duplicate Events
  4. 4. Register the Webhook URL
  5. 5. Production Checklist
  6. 6. Ship Payments Faster

SvelteKit Webhook Endpoint Setup

Create a POST handler in a +server.ts file. SvelteKit routes this to the URL based on its file path:

src/routes/api/webhooks/paystack/+server.ts
# Handles POST requests to /api/webhooks/paystack

Store your Paystack secret key as a private environment variable:

# .env
PAYSTACK_SECRET_KEY=sk_live_xxxxxxxxxxxx

The Webhook Handler

// src/routes/api/webhooks/paystack/+server.ts
import { PAYSTACK_SECRET_KEY } from '$env/static/private';
import { json, error } from '@sveltejs/kit';
import { createHmac } from 'crypto';

export async function POST({ request }) {
  // Read raw body BEFORE any JSON parsing
  var rawBody = await request.text();

  // Verify signature
  var signature = request.headers.get('x-paystack-signature');

  if (!signature) {
    throw error(400, 'Missing signature');
  }

  var hash = createHmac('sha512', PAYSTACK_SECRET_KEY)
    .update(rawBody)
    .digest('hex');

  if (hash !== signature) {
    throw error(400, 'Invalid signature');
  }

  // Parse event after verification
  var event = JSON.parse(rawBody);

  // Return 200 immediately — process in background or synchronously
  // For production, push to a queue here instead
  handleEvent(event).catch(console.error);

  return json({ received: true });
}

async function handleEvent(event: { event: string; data: Record }) {
  switch (event.event) {
    case 'charge.success':
      await handleChargeSuccess(event.data);
      break;
    case 'transfer.success':
      await handleTransferSuccess(event.data);
      break;
    case 'transfer.failed':
      await handleTransferFailed(event.data);
      break;
    case 'subscription.create':
      await handleSubscriptionCreate(event.data);
      break;
    case 'invoice.payment_failed':
      await handleInvoicePaymentFailed(event.data);
      break;
    default:
      console.log('Unhandled event:', event.event);
  }
}

async function handleChargeSuccess(data: Record) {
  var reference = data.reference as string;
  var amount = data.amount as number;
  var currency = data.currency as string;
  var customerEmail = (data.customer as { email: string }).email;

  console.log('Payment confirmed:', reference, amount / 100, currency, customerEmail);
  // Update your database here
}

async function handleTransferSuccess(data: Record) {
  console.log('Transfer successful:', data.reference);
}

async function handleTransferFailed(data: Record) {
  console.log('Transfer failed:', data.reference);
}

async function handleSubscriptionCreate(data: Record) {
  console.log('Subscription created:', data.subscription_code);
}

async function handleInvoicePaymentFailed(data: Record) {
  console.log('Invoice payment failed:', data.subscription);
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Idempotency: Handle Duplicate Events

Paystack retries failed webhook deliveries. Your handler may receive the same event more than once. Use a database table to track processed events:

// With a Supabase client, for example
async function isAlreadyProcessed(eventId: string): Promise {
  var { data } = await supabase
    .from('processed_webhook_events')
    .select('id')
    .eq('event_id', eventId)
    .single();

  return !!data;
}

async function markProcessed(eventId: string): Promise {
  await supabase
    .from('processed_webhook_events')
    .insert({ event_id: eventId, processed_at: new Date().toISOString() });
}

// In your handler:
async function handleEvent(event: { id?: string; event: string; data: Record }) {
  var eventId = event.id || (event.data.reference as string);

  if (await isAlreadyProcessed(eventId)) {
    console.log('Duplicate event, skipping:', eventId);
    return;
  }

  await markProcessed(eventId);

  // Process event
}

The event object from Paystack may not have a unique id field. Use the transaction reference or a combination of event type and reference as the idempotency key.

Register the Webhook URL

Add your webhook URL in the Paystack dashboard:

  1. Log in to your Paystack dashboard
  2. Go to Settings → API Keys & Webhooks
  3. Add your webhook URL: https://yourdomain.com/api/webhooks/paystack

For local development, use a tunnel to expose your local server:

# With ngrok
npx ngrok http 5173
# Then set webhook URL to: https://abc123.ngrok.io/api/webhooks/paystack

# With Cloudflare Tunnel
npx cloudflared tunnel --url http://localhost:5173

Production Checklist

  1. Use live keys. Switch PAYSTACK_SECRET_KEY to your live key in production.
  2. Queue heavy work. For database writes that might be slow, push events to a queue (BullMQ, Inngest, etc.) rather than processing synchronously.
  3. Log all events. Store every incoming webhook payload for debugging. You will need it.
  4. Add idempotency. Track processed event references in your database.
  5. Test locally. Use ngrok or Cloudflare Tunnel to test before deploying.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series.

Sign up for the McTaba newsletter for more Paystack guides.

Key Takeaways

  • ✓SvelteKit server endpoints (+server.ts) handle Paystack webhooks cleanly. No separate backend required.
  • ✓You must read the raw body with request.text() before JSON parsing. Parsing first destroys the signature.
  • ✓The x-paystack-signature header contains an HMAC SHA512 hex digest of the raw body signed with your secret key.
  • ✓Return a 200 response before doing any database work. Paystack retries if your endpoint takes too long.
  • ✓Store a processed flag in your database for each webhook event to avoid double-processing on retries.
  • ✓SvelteKit server endpoints do not have CSRF protection, so no exemption configuration is needed.

Frequently Asked Questions

Why does signature verification fail in SvelteKit?
The most common cause is parsing the request body with request.json() before verification. JSON parsing reconstructs the string, which changes whitespace and key ordering, making the hash not match. Always use request.text() to read the raw body first, then JSON.parse() after signature verification.
Do I need to configure CSRF exemption for the webhook endpoint?
No. SvelteKit server endpoints (+server.ts) do not have CSRF protection built in. No exemption is needed. This differs from form actions (+page.server.ts), which have CSRF protection.
How do I test Paystack webhooks locally in SvelteKit?
Use ngrok (npx ngrok http 5173) or Cloudflare Tunnel (npx cloudflared tunnel --url http://localhost:5173) to create a public URL that tunnels to your local dev server. Register that URL as your Paystack webhook URL in the dashboard.
What happens if my webhook handler throws an error?
If your endpoint returns a non-200 status, Paystack will retry the webhook later. This is why you should return 200 immediately and handle errors in your processing code. Log errors so you can investigate, but do not let processing failures prevent the 200 response.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Accept Payments with Paystack in Svelte and SvelteKit

Accept Paystack payments in SvelteKit using server endpoints for backend logic and Svelte components for the frontend. This guide covers initializing transactions, inline popup checkout, redirect checkout, and verification in a single full-stack SvelteKit project.

Read more

Verify Paystack Payments in Svelte and SvelteKit

Verify Paystack transactions in SvelteKit using server endpoints for popup verification and load functions for redirect callback verification. Covers amount validation, idempotent fulfillment, and SSR verification for instant results.

Read more

Idempotent Paystack Webhook Handlers: The Complete Pattern

A complete guide to building idempotent Paystack webhook handlers. Covers why Paystack sends duplicate events, using transaction references as idempotency keys, database-level deduplication with a processed_events table, and the full production-ready pattern in Node.js.

Read more

Why You Must Return 200 Immediately from a Paystack Webhook

The number one Paystack webhook reliability rule: return 200 before doing anything else. This guide explains why Paystack has a timeout, what happens when you break the rule, and how to implement the respond-first-then-queue pattern in Node.js.

Read more

Paystack Webhook Not Firing: Diagnostic Checklist

When Paystack webhooks stop arriving, the problem is almost never Paystack itself. It is your URL configuration, your server, your firewall, or your response code. This checklist walks through every step from the dashboard setting to the server log, with verification commands for each.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.