g14o
Packages@g14o/paystack

Webhooks

Verify, parse, and process Paystack webhook deliveries with typed events and optional deduplication.

Paystack sends event notifications to your webhook URL as signed JSON POST requests. Always verify the x-paystack-signature header against the raw request body before parsing JSON or delivering value.

Processing flow

Incoming POST Request processWebhookDelivery verify signature + raw body parse webhook payload WebhookDeliveryStore.claim optional handler event markProcessed / markFailed optional

Re-serializing JSON after reading the body will break signature verification — use the exact bytes Paystack sent.

Client webhook APIs

Namespace: paystack.webhook.* on a Paystack client instance (uses the configured secretKey for HMAC-SHA512).

MethodUse case
processWebhookDelivery(request, options)Primary — verify + parse + run handler with optional deduplication via WebhookDeliveryStore.
processWebhookRequest(request)Advanced — verify + parse only, no handler.
verifyWebhookRequest(request)Advanced — returns raw body after signature verification.
parseWebhookPayload(rawBody)Advanced — parse JSON and Zod-validate. Call only after verification.
verifyPaystackWebhookSignature(rawBody, signature)Advanced — manual verification when you already have the raw body and header.

Production handler (primary)

import { Paystack, isPaystackEvent } from "@g14o/paystack";

const paystack = new Paystack({ secretKey: process.env.PAYSTACK_SECRET_KEY! });

export async function POST(request: Request) {
  const result = await paystack.webhook.processWebhookDelivery(request, {
    handler: async (event) => {
      if (isPaystackEvent(event, "charge.success")) {
        // Fulfill order using event.data.reference
      }
    },
    store: {
      claim: async ({ eventId }) => {
        // Return "duplicate" if already processed
        return "claimed";
      },
      markProcessed: async (eventId) => {},
      markFailed: async (eventId, errorMessage) => {},
    },
  });

  if (result.duplicate) {
    return Response.json({ received: true, duplicate: true });
  }

  return Response.json({ received: true });
}

Options: ProcessWebhookDeliveryRequestOptions, WebhookDeliveryStore.

Omit store for at-least-once delivery when deduplication is not required.

Advanced two-step flow

Use when you need to persist the raw body before handling:

const rawBody = await paystack.webhook.verifyWebhookRequest(request);
await saveRawPayload(rawBody);
const event = paystack.webhook.parseWebhookPayload(rawBody);

Verify signature manually

paystack.webhook.verifyPaystackWebhookSignature(
  rawBody,
  request.headers.get("x-paystack-signature")
);
// throws WebhookVerificationError on missing/invalid signature

Standalone exports

Import from @g14o/paystack (not on the client instance):

ExportUse case
processWebhookDelivery(request, options, { secretKey })Request-first delivery without a client instance.
processVerifiedWebhookDelivery(options)Advanced — delivery when verification/parsing happens elsewhere (e.g. queue workers). Requires pre-verified event + rawBody.
createWebhookEventId(event)Advanced — stable dedupe key from Webhook Delivery identity rules.
parsePaystackWebhookEvent(payload)Parse/validate without the client. Throws ZodError on failure.
safeParsePaystackWebhookEvent(payload)Non-throwing parse for logging or soft validation.
isPaystackEvent(event, name)Type-narrowing guard in handler blocks.
paystackWebhookEventSchemaZod schema for custom validation pipelines.
SUPPORTED_PAYSTACK_EVENTSRuntime list of all validated event names.
import {
  createWebhookEventId,
  isPaystackEvent,
  SUPPORTED_PAYSTACK_EVENTS,
} from "@g14o/paystack";

Supported event types

The package validates 27 Paystack webhook events via Zod. Each event is discriminated by the event field with a typed data payload.

CategoryEvents
Bank transfersbank.transfer.rejected
Chargescharge.success
Disputescharge.dispute.create, charge.dispute.remind, charge.dispute.resolve
Customer identificationcustomeridentification.failed, customeridentification.success
Dedicated accountsdedicatedaccount.assign.failed, dedicatedaccount.assign.success
Invoicesinvoice.create, invoice.payment_failed, invoice.update
Payment requestspaymentrequest.pending, paymentrequest.success
Refundsrefund.failed, refund.pending, refund.processed, refund.processing
Subscriptionssubscription.create, subscription.disable, subscription.not_renew, subscription.expiring_cards
Transferstransfer.failed, transfer.success, transfer.reversed

Exported data types include ChargeSuccessData, TransferData, SubscriptionCreateData, and PaystackEventDataMap for typed handler code.

Errors

ErrorCodeWhen
WebhookVerificationErrorWEBHOOK_MISSING_SIGNATUREx-paystack-signature header absent
WebhookVerificationErrorWEBHOOK_INVALID_SIGNATUREHMAC mismatch
WebhookVerificationErrorWEBHOOK_INVALID_PAYLOADMissing or empty request body during verify
WebhookDeliveryErrorWEBHOOK_INVALID_PAYLOADInvalid JSON or event shape after verify (400)
WebhookDeliveryErrorWEBHOOK_PROCESSING_ERRORHandler threw during processWebhookDelivery (400)

Important constraints

  • Use the same secretKey as your REST API client for HMAC verification.
  • Read x-paystack-signature from headers; never parse JSON before verification.
  • In Next.js, avoid middleware that consumes the request body before the route handler.
  • Respond with 200 quickly; defer heavy work to a queue after verification.
  • For idempotency, implement WebhookDeliveryStore or use @g14o/paystack-better-auth built-in persistence.

Better Auth webhooks

When using @g14o/paystack-better-auth, configure your Paystack dashboard webhook URL to:

https://your-domain.com/api/auth/paystack/webhook

The plugin uses processWebhookDelivery with database-backed deduplication via paystackWebhookEvent.

On this page