Skip to main content

Overview

The Webhook class provides a secure, type-safe way to handle webhook events from Paystack. It automatically verifies webhook signatures, parses event payloads, and routes events to registered handlers.

Key Features

  • Automatic signature verification using HMAC SHA-512
  • Type-safe event handlers with full TypeScript support
  • Event-driven architecture with chainable .on() method
  • Platform-agnostic works with Express, Next.js, Hono, and more
  • Zod validation for runtime type safety

Methods

on

Registers a handler function for a specific webhook event. This method is chainable, allowing you to register multiple handlers at once.
paystack.webhook
  .on("charge.success", (data) => {
    console.log(`Payment received: ${data.reference}`);
    console.log(`Amount: ${data.amount / 100}`);
    console.log(`Customer: ${data.customer.email}`);
  })
  .on("transfer.success", (data) => {
    console.log(`Transfer completed: ${data.reference}`);
    console.log(`Recipient: ${data.recipient.name}`);
  });

Parameters

event
string
required
The event type to listen for. See Supported Events below
handler
function
required
The function to execute when the event is received. The function receives the event data as its parameter and can be async

Returns

Returns the Webhook instance for chaining.

process

Processes an incoming webhook request by verifying the signature, parsing the payload, and dispatching it to the appropriate handler.
import express from 'express';
import { Paystack } from '@efobi/paystack';

const app = express();
const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);

// Register event handlers
paystack.webhook.on("charge.success", (data) => {
  console.log("Payment successful:", data.reference);
});

// Webhook endpoint
app.post(
  '/webhooks/paystack',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    try {
      const signature = req.headers['x-paystack-signature'] as string;
      const rawBody = req.body.toString('utf-8');
      
      await paystack.webhook.process(rawBody, signature);
      
      res.status(200).json({ message: 'Webhook processed' });
    } catch (error) {
      console.error(error.message);
      res.status(400).json({ message: 'Error processing webhook' });
    }
  }
);

Parameters

rawBody
string
required
The raw, unparsed request body as a string. Important: Do not parse the JSON before passing it to this method, as the signature verification requires the raw body
signature
string
required
The value of the x-paystack-signature header from the webhook request

Returns

Returns a Promise that resolves to the parsed webhook payload if successful.

Throws

  • Error("Missing 'x-paystack-signature' header") - If the signature header is missing
  • Error("Invalid webhook signature") - If the signature verification fails
  • Error("Failed to parse webhook payload") - If the payload doesn’t match the expected schema

Supported Events

The SDK supports all Paystack webhook events with full TypeScript types:

Charge Events

Triggered when a charge is successful.Handler data includes:
  • reference - Transaction reference
  • amount - Amount in kobo
  • customer - Customer details (email, name, etc.)
  • authorization - Card/payment authorization details
  • metadata - Custom metadata
Triggered when a dispute is created on a charge.Handler data includes:
  • id - Dispute ID
  • transaction - Full transaction details
  • customer - Customer details
  • status - Dispute status
Triggered to remind you of a pending dispute.
Triggered when a dispute is resolved.

Transfer Events

Triggered when a transfer is successful.Handler data includes:
  • reference - Transfer reference
  • amount - Transfer amount
  • recipient - Recipient details
  • status - Transfer status
Triggered when a transfer fails.
Triggered when a transfer is reversed.

Subscription Events

Triggered when a subscription is created.Handler data includes:
  • subscription_code - Unique subscription code
  • plan - Plan details
  • customer - Customer details
  • authorization - Payment authorization
Triggered when a subscription is disabled.
Triggered when a subscription will not renew.
Triggered to alert about expiring cards on subscriptions. Returns an array of subscriptions with expiring cards.

Invoice Events

Triggered when an invoice is created.
Triggered when an invoice is updated.
Triggered when an invoice payment fails.

Refund Events

Triggered when a refund is pending.
Triggered when a refund is being processed.
Triggered when a refund has been processed.
Triggered when a refund fails.

Other Events

Triggered when a dedicated virtual account is successfully assigned to a customer.
Triggered when dedicated virtual account assignment fails.
Triggered when customer identification is successful.
Triggered when customer identification fails.
Triggered when a payment request is pending.
Triggered when a payment request is successful.

Security

Signature Verification

The SDK automatically verifies webhook signatures using HMAC SHA-512. This ensures that webhooks are genuinely from Paystack and haven’t been tampered with.
// The process method handles verification automatically
await paystack.webhook.process(rawBody, signature);
// If this doesn't throw, the signature is valid

Best Practices

The signature is computed against the raw request body. Parse the body to JSON only after verification fails.
// ✅ Correct
const rawBody = await req.text();
await paystack.webhook.process(rawBody, signature);

// ❌ Wrong
const body = await req.json();
await paystack.webhook.process(JSON.stringify(body), signature);
Never hardcode your Paystack secret key.
const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
Paystack may send the same webhook multiple times. Use the event’s reference/ID to prevent duplicate processing.
paystack.webhook.on("charge.success", async (data) => {
  const { reference } = data;
  
  // Check if already processed
  const exists = await db.transaction.findUnique({ where: { reference } });
  if (exists) return;
  
  // Process the payment
  await db.transaction.create({ data: { reference, /* ... */ } });
});
Process webhooks quickly or queue them for background processing. Paystack expects a 200 response within a reasonable time.
app.post('/webhook', async (req, res) => {
  try {
    await paystack.webhook.process(rawBody, signature);
    res.status(200).json({ received: true });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Error Handling

Handle webhook processing errors gracefully:
try {
  await paystack.webhook.process(rawBody, signature);
  console.log('Webhook processed successfully');
} catch (error) {
  if (error.message.includes('signature')) {
    console.error('Invalid webhook signature - possible security issue');
  } else if (error.message.includes('parse')) {
    console.error('Webhook payload validation failed');
  } else {
    console.error('Unexpected error:', error);
  }
  // Still return 200 to Paystack to avoid retries for unrecoverable errors
}

Testing Webhooks

Using Paystack Test Mode

  1. Use your test secret key to initialize the SDK
  2. Make a test transaction on your staging/test environment
  3. Paystack will send webhooks to your configured endpoint

Local Testing with Ngrok

# Install ngrok
npm install -g ngrok

# Start your local server
npm run dev

# Expose it to the internet
ngrok http 3000

# Use the ngrok URL in Paystack dashboard
# Example: https://abc123.ngrok.io/webhooks/paystack

Manual Testing

You can manually trigger webhooks using the Paystack API or dashboard for testing.