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

# Webhooks Guide

> Handle real-time payment events with the Paystack TypeScript SDK

Webhooks allow Paystack to send real-time notifications to your server when events occur on your account. This guide shows you how to implement webhook handlers securely.

## Overview

Webhooks are essential for:

* Real-time payment confirmations
* Automatic order fulfillment
* Subscription management
* Transfer status updates
* Dispute notifications

<Note>
  The SDK handles signature verification, payload parsing, and type-safe event handling automatically.
</Note>

## Setup Webhook Handler

Register event handlers for the events you want to process.

<Steps>
  <Step title="Initialize the SDK">
    ```typescript theme={null}
    import { Paystack } from '@efobi/paystack';

    const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);
    ```
  </Step>

  <Step title="Register event handlers">
    ```typescript theme={null}
    paystack.webhook
      .on('charge.success', (data) => {
        console.log('Payment successful:', data.reference);
        console.log('Amount:', data.amount / 100);
        console.log('Customer:', data.customer.email);
        
        // Fulfill order
        fulfillOrder(data.reference);
      })
      .on('transfer.success', (data) => {
        console.log('Transfer completed:', data.reference);
        updateTransferStatus(data.transfer_code, 'success');
      })
      .on('transfer.failed', (data) => {
        console.log('Transfer failed:', data.reference);
        handleTransferFailure(data);
      });
    ```
  </Step>

  <Step title="Create webhook endpoint">
    Set up an endpoint to receive webhook requests from Paystack.
  </Step>
</Steps>

## Framework Integration

### Express.js

```typescript theme={null}
import express, { Request, Response } from 'express';
import { Paystack } from '@efobi/paystack';

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

// Register handlers
paystack.webhook
  .on('charge.success', (data) => {
    console.log('Charge successful:', data.reference);
    fulfillOrder(data.reference, data.amount / 100);
  })
  .on('subscription.disable', (data) => {
    console.log('Subscription disabled:', data.subscription_code);
    deactivateSubscription(data.customer.email);
  });

// IMPORTANT: Use raw body parser for webhook route
app.post(
  '/api/webhooks/paystack',
  express.raw({ type: 'application/json' }),
  async (req: Request, res: Response) => {
    try {
      const signature = req.headers['x-paystack-signature'] as string;
      const rawBody = req.body.toString('utf-8');

      // Process webhook
      const payload = await paystack.webhook.process(rawBody, signature);

      console.log('Webhook processed:', payload.event);
      res.status(200).json({ message: 'Webhook received' });
    } catch (error: any) {
      console.error('Webhook error:', error.message);
      res.status(400).json({ message: 'Invalid webhook' });
    }
  }
);

// Other routes use JSON parser
app.use(express.json());

app.listen(3000, () => console.log('Server running on port 3000'));
```

<Warning>
  Use `express.raw()` middleware for the webhook route to preserve the raw body. This is required for signature verification.
</Warning>

### Next.js (App Router)

```typescript 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', async (data) => {
    console.log(`Payment successful: ${data.reference}`);
    
    // Update database
    await db.orders.update(
      { reference: data.reference },
      { 
        status: 'paid',
        paidAt: data.paid_at,
        amount: data.amount / 100
      }
    );
    
    // Send confirmation email
    await sendOrderConfirmation(data.customer.email, data.reference);
  })
  .on('transfer.success', async (data) => {
    console.log(`Transfer successful: ${data.reference}`);
    
    await db.transfers.update(
      { reference: data.reference },
      { status: 'completed' }
    );
  });

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

### Hono (Bun/Cloudflare Workers)

```typescript 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.amount / 100);
  })
  .on('refund.processed', (data) => {
    console.log('Refund processed:', data.refund_reference);
  });

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

export default app;
```

## Available Events

The SDK provides type-safe handlers for all Paystack webhook events:

### Transaction Events

<CodeGroup>
  ```typescript charge.success theme={null}
  paystack.webhook.on('charge.success', (data) => {
    // data.reference, data.amount, data.customer, etc.
    console.log('Payment successful:', data.reference);
  });
  ```

  ```typescript charge.dispute.create theme={null}
  paystack.webhook.on('charge.dispute.create', (data) => {
    // data.id, data.transaction, data.category
    console.log('Dispute created:', data.id);
  });
  ```

  ```typescript charge.dispute.resolve theme={null}
  paystack.webhook.on('charge.dispute.resolve', (data) => {
    console.log('Dispute resolved:', data.id);
  });
  ```
</CodeGroup>

### Transfer Events

<CodeGroup>
  ```typescript transfer.success theme={null}
  paystack.webhook.on('transfer.success', (data) => {
    console.log('Transfer completed:', data.reference);
    console.log('Recipient:', data.recipient.name);
  });
  ```

  ```typescript transfer.failed theme={null}
  paystack.webhook.on('transfer.failed', (data) => {
    console.log('Transfer failed:', data.reference);
    console.log('Failures:', data.failures);
  });
  ```

  ```typescript transfer.reversed theme={null}
  paystack.webhook.on('transfer.reversed', (data) => {
    console.log('Transfer reversed:', data.reference);
  });
  ```
</CodeGroup>

### Virtual Account Events

<CodeGroup>
  ```typescript dedicatedaccount.assign.success theme={null}
  paystack.webhook.on('dedicatedaccount.assign.success', (data) => {
    console.log('Virtual account assigned!');
    console.log('Account:', data.dedicated_account.account_number);
    console.log('Bank:', data.dedicated_account.bank.name);
  });
  ```

  ```typescript dedicatedaccount.assign.failed theme={null}
  paystack.webhook.on('dedicatedaccount.assign.failed', (data) => {
    console.log('Assignment failed for:', data.customer.email);
  });
  ```
</CodeGroup>

### Subscription Events

<CodeGroup>
  ```typescript subscription.create theme={null}
  paystack.webhook.on('subscription.create', (data) => {
    console.log('Subscription created:', data.subscription_code);
    console.log('Next payment:', data.next_payment_date);
  });
  ```

  ```typescript subscription.disable theme={null}
  paystack.webhook.on('subscription.disable', (data) => {
    console.log('Subscription disabled:', data.subscription_code);
  });
  ```

  ```typescript subscription.not_renew theme={null}
  paystack.webhook.on('subscription.not_renew', (data) => {
    console.log('Subscription will not renew:', data.subscription_code);
  });
  ```
</CodeGroup>

### Refund Events

<CodeGroup>
  ```typescript refund.pending theme={null}
  paystack.webhook.on('refund.pending', (data) => {
    console.log('Refund pending:', data.transaction_reference);
  });
  ```

  ```typescript refund.processed theme={null}
  paystack.webhook.on('refund.processed', (data) => {
    console.log('Refund processed:', data.refund_reference);
    console.log('Amount:', data.amount / 100);
  });
  ```

  ```typescript refund.failed theme={null}
  paystack.webhook.on('refund.failed', (data) => {
    console.log('Refund failed:', data.transaction_reference);
  });
  ```
</CodeGroup>

## Security

The SDK automatically verifies webhook signatures using HMAC SHA-512.

### How It Works

1. Paystack signs each webhook with your secret key
2. The SDK verifies the signature before processing
3. Invalid signatures are rejected automatically

```typescript theme={null}
// Automatic verification
try {
  const payload = await paystack.webhook.process(rawBody, signature);
  // Signature is valid, process the event
} catch (error) {
  // Signature verification failed
  console.error('Invalid signature:', error.message);
}
```

<Warning>
  Never disable signature verification in production. Always use the raw request body for verification.
</Warning>

## Best Practices

<Tip>
  1. **Return 200 immediately** - Process webhooks asynchronously
  2. **Implement idempotency** - Handle duplicate webhook deliveries
  3. **Log all webhooks** - Keep audit trails for debugging
  4. **Monitor failures** - Set up alerts for webhook errors
  5. **Use queues** - Process webhooks in background jobs
  6. **Verify transactions** - Always confirm with API calls when needed
  7. **Test thoroughly** - Use Paystack's webhook testing tool
  8. **Handle errors gracefully** - Don't let webhook failures break your app
</Tip>

## Common Patterns

### Asynchronous Processing

```typescript theme={null}
import { Queue } from 'bull';

const webhookQueue = new Queue('webhooks');

paystack.webhook.on('charge.success', async (data) => {
  // Queue for background processing
  await webhookQueue.add('payment-success', {
    reference: data.reference,
    amount: data.amount,
    customerEmail: data.customer.email
  });
});

// Process in worker
webhookQueue.process('payment-success', async (job) => {
  const { reference, amount, customerEmail } = job.data;
  
  // Verify transaction (optional but recommended)
  const { data } = await paystack.transaction.verify(reference);
  
  if (data.data.status === 'success') {
    await fulfillOrder(reference);
    await sendReceiptEmail(customerEmail, amount / 100);
  }
});
```

### Idempotent Webhook Handling

```typescript theme={null}
paystack.webhook.on('charge.success', async (data) => {
  const reference = data.reference;
  
  // Check if already processed
  const existing = await db.processedWebhooks.findOne({ reference });
  
  if (existing) {
    console.log('Webhook already processed:', reference);
    return; // Skip duplicate
  }
  
  // Process webhook
  await fulfillOrder(reference);
  
  // Mark as processed
  await db.processedWebhooks.create({
    reference,
    event: 'charge.success',
    processedAt: new Date()
  });
});
```

### Error Handling with Retries

```typescript theme={null}
paystack.webhook.on('charge.success', async (data) => {
  const maxRetries = 3;
  let attempt = 0;
  
  while (attempt < maxRetries) {
    try {
      await fulfillOrder(data.reference);
      break; // Success, exit loop
    } catch (error) {
      attempt++;
      console.error(`Attempt ${attempt} failed:`, error);
      
      if (attempt === maxRetries) {
        // All retries failed, log for manual intervention
        await db.failedWebhooks.create({
          reference: data.reference,
          event: 'charge.success',
          error: error.message,
          attempts: maxRetries
        });
        
        await notifyAdmin('webhook_processing_failed', {
          reference: data.reference
        });
      } else {
        // Wait before retry (exponential backoff)
        await new Promise(resolve => 
          setTimeout(resolve, Math.pow(2, attempt) * 1000)
        );
      }
    }
  }
});
```

### Complete Order Fulfillment

```typescript theme={null}
paystack.webhook.on('charge.success', async (data) => {
  const reference = data.reference;
  
  try {
    // 1. Verify payment amount matches order
    const order = await db.orders.findOne({ reference });
    
    if (!order) {
      console.error('Order not found:', reference);
      return;
    }
    
    if (order.amount !== data.amount / 100) {
      console.error('Amount mismatch:', { 
        expected: order.amount,
        received: data.amount / 100
      });
      return;
    }
    
    // 2. Update order status
    await db.orders.update(
      { reference },
      {
        status: 'paid',
        paidAt: data.paid_at,
        transactionId: data.id,
        paymentChannel: data.channel
      }
    );
    
    // 3. Grant access to product/service
    await grantAccess(order.customerId, order.productId);
    
    // 4. Send confirmation email
    await sendOrderConfirmation(data.customer.email, {
      orderId: order.id,
      amount: data.amount / 100,
      reference
    });
    
    // 5. Notify relevant parties
    await notifyVendor(order.vendorId, 'new_order', order);
    
    console.log('Order fulfilled successfully:', reference);
  } catch (error) {
    console.error('Order fulfillment failed:', error);
    // Queue for manual review
    await db.pendingOrders.create({ reference, error: error.message });
  }
});
```

## Testing Webhooks

Test your webhook handlers locally:

```typescript theme={null}
// Create a test webhook payload
const testPayload = {
  event: 'charge.success',
  data: {
    id: 123456,
    reference: 'test-ref-123',
    amount: 50000,
    currency: 'NGN',
    status: 'success',
    customer: {
      email: 'test@example.com',
      first_name: 'John',
      last_name: 'Doe'
    },
    paid_at: new Date().toISOString()
  }
};

// Manually trigger the handler
const handler = paystack.webhook['handlers']['charge.success'];
if (handler) {
  await handler(testPayload.data);
}
```

<Tip>
  Use tools like [ngrok](https://ngrok.com) to expose your local server for testing with real Paystack webhooks.
</Tip>

## Troubleshooting

### Webhook Not Received

1. Check your webhook URL in Paystack Dashboard
2. Ensure your server is publicly accessible
3. Verify your server returns 200 OK
4. Check firewall settings

### Signature Verification Failed

1. Use the raw request body (not parsed JSON)
2. Verify you're using the correct secret key
3. Check for middleware that modifies the body
4. Ensure the signature header is being read correctly

### Duplicate Webhooks

1. Implement idempotency checks
2. Use unique references to track processed webhooks
3. Return 200 OK even for duplicates

## Related Resources

* [Transactions Guide](/guides/transactions) - Process payments
* [Transfers Guide](/guides/transfers) - Handle transfer events
* [Virtual Accounts Guide](/guides/virtual-accounts) - Manage virtual account events
* [Webhook API Reference](/api-reference/webhook) - Full event documentation
