> ## Documentation Index
> Fetch the complete documentation index at: https://paystack-sdk.efobi.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook

> Handle and process Paystack webhook events securely

## 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.

<CodeGroup>
  ```typescript TypeScript theme={null}
  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}`);
    });
  ```

  ```javascript JavaScript theme={null}
  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}`);
    });
  ```
</CodeGroup>

#### Parameters

<ParamField path="event" type="string" required>
  The event type to listen for. See [Supported Events](#supported-events) below
</ParamField>

<ParamField path="handler" type="function" required>
  The function to execute when the event is received. The function receives the event data as its parameter and can be async
</ParamField>

#### 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.

<CodeGroup>
  ```typescript Express theme={null}
  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' });
      }
    }
  );
  ```

  ```typescript Next.js theme={null}
  // app/api/webhooks/paystack/route.ts
  import { NextResponse } from "next/server";
  import { Paystack } from "@efobi/paystack";

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

  // Register event handlers
  paystack.webhook
    .on("charge.success", (data) => {
      console.log(`Payment successful for ${data.customer.email}`);
      // Update your database, grant access, etc.
    })
    .on("transfer.success", (data) => {
      console.log(`Transfer of ${data.amount} completed`);
    });

  export async function POST(req: Request) {
    try {
      const rawBody = await req.text();
      const signature = req.headers.get("x-paystack-signature");
      
      await paystack.webhook.process(rawBody, signature);
      
      return NextResponse.json({ message: "Webhook received" }, { status: 200 });
    } catch (error: any) {
      console.error("Webhook error:", error.message);
      return NextResponse.json({ message: "Error processing webhook" }, { status: 400 });
    }
  }
  ```

  ```typescript Hono theme={null}
  import { Hono } from 'hono';
  import { Paystack } from '@efobi/paystack';

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

  paystack.webhook.on("charge.success", (data) => {
    console.log("Payment received:", data.reference);
  });

  app.post('/webhooks/paystack', async (c) => {
    try {
      const signature = c.req.header('x-paystack-signature');
      const rawBody = await c.req.text();
      
      await paystack.webhook.process(rawBody, signature);
      
      return c.json({ message: 'Webhook received' }, 200);
    } catch (error: any) {
      console.error(error.message);
      return c.json({ message: 'Error processing webhook' }, 400);
    }
  });

  export default app;
  ```
</CodeGroup>

#### Parameters

<ParamField path="rawBody" type="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
</ParamField>

<ParamField path="signature" type="string" required>
  The value of the `x-paystack-signature` header from the webhook request
</ParamField>

#### 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

<AccordionGroup>
  <Accordion title="charge.success">
    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
  </Accordion>

  <Accordion title="charge.dispute.create">
    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
  </Accordion>

  <Accordion title="charge.dispute.remind">
    Triggered to remind you of a pending dispute.
  </Accordion>

  <Accordion title="charge.dispute.resolve">
    Triggered when a dispute is resolved.
  </Accordion>
</AccordionGroup>

### Transfer Events

<AccordionGroup>
  <Accordion title="transfer.success">
    Triggered when a transfer is successful.

    **Handler data includes:**

    * `reference` - Transfer reference
    * `amount` - Transfer amount
    * `recipient` - Recipient details
    * `status` - Transfer status
  </Accordion>

  <Accordion title="transfer.failed">
    Triggered when a transfer fails.
  </Accordion>

  <Accordion title="transfer.reversed">
    Triggered when a transfer is reversed.
  </Accordion>
</AccordionGroup>

### Subscription Events

<AccordionGroup>
  <Accordion title="subscription.create">
    Triggered when a subscription is created.

    **Handler data includes:**

    * `subscription_code` - Unique subscription code
    * `plan` - Plan details
    * `customer` - Customer details
    * `authorization` - Payment authorization
  </Accordion>

  <Accordion title="subscription.disable">
    Triggered when a subscription is disabled.
  </Accordion>

  <Accordion title="subscription.not_renew">
    Triggered when a subscription will not renew.
  </Accordion>

  <Accordion title="subscription.expiring_cards">
    Triggered to alert about expiring cards on subscriptions. Returns an array of subscriptions with expiring cards.
  </Accordion>
</AccordionGroup>

### Invoice Events

<AccordionGroup>
  <Accordion title="invoice.create">
    Triggered when an invoice is created.
  </Accordion>

  <Accordion title="invoice.update">
    Triggered when an invoice is updated.
  </Accordion>

  <Accordion title="invoice.payment_failed">
    Triggered when an invoice payment fails.
  </Accordion>
</AccordionGroup>

### Refund Events

<AccordionGroup>
  <Accordion title="refund.pending">
    Triggered when a refund is pending.
  </Accordion>

  <Accordion title="refund.processing">
    Triggered when a refund is being processed.
  </Accordion>

  <Accordion title="refund.processed">
    Triggered when a refund has been processed.
  </Accordion>

  <Accordion title="refund.failed">
    Triggered when a refund fails.
  </Accordion>
</AccordionGroup>

### Other Events

<AccordionGroup>
  <Accordion title="dedicatedaccount.assign.success">
    Triggered when a dedicated virtual account is successfully assigned to a customer.
  </Accordion>

  <Accordion title="dedicatedaccount.assign.failed">
    Triggered when dedicated virtual account assignment fails.
  </Accordion>

  <Accordion title="customeridentification.success">
    Triggered when customer identification is successful.
  </Accordion>

  <Accordion title="customeridentification.failed">
    Triggered when customer identification fails.
  </Accordion>

  <Accordion title="paymentrequest.pending">
    Triggered when a payment request is pending.
  </Accordion>

  <Accordion title="paymentrequest.success">
    Triggered when a payment request is successful.
  </Accordion>
</AccordionGroup>

## 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.

```typescript theme={null}
// The process method handles verification automatically
await paystack.webhook.process(rawBody, signature);
// If this doesn't throw, the signature is valid
```

### Best Practices

<AccordionGroup>
  <Accordion title="Always use the raw request body">
    The signature is computed against the raw request body. Parse the body to JSON only after verification fails.

    ```typescript theme={null}
    // ✅ 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);
    ```
  </Accordion>

  <Accordion title="Use environment variables for secrets">
    Never hardcode your Paystack secret key.

    ```typescript theme={null}
    const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
    ```
  </Accordion>

  <Accordion title="Implement idempotency">
    Paystack may send the same webhook multiple times. Use the event's reference/ID to prevent duplicate processing.

    ```typescript theme={null}
    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, /* ... */ } });
    });
    ```
  </Accordion>

  <Accordion title="Return 200 quickly">
    Process webhooks quickly or queue them for background processing. Paystack expects a 200 response within a reasonable time.

    ```typescript theme={null}
    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 });
      }
    });
    ```
  </Accordion>
</AccordionGroup>

## Error Handling

Handle webhook processing errors gracefully:

```typescript theme={null}
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

```bash theme={null}
# 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.

## Related Resources

<CardGroup cols={2}>
  <Card title="Paystack Webhook Docs" icon="link" href="https://paystack.com/docs/payments/webhooks/">
    Official Paystack webhook documentation
  </Card>

  <Card title="Webhook Events" icon="book" href="https://paystack.com/docs/api/webhook-events/">
    Complete list of webhook events and their payloads
  </Card>

  <Card title="Security" icon="shield" href="https://paystack.com/docs/payments/webhooks/#security">
    Learn more about webhook security
  </Card>

  <Card title="GitHub Examples" icon="code" href="https://github.com/efobi-dev/paystack/tree/main/examples">
    See complete webhook integration examples
  </Card>
</CardGroup>
