Skip to main content
Webhooks allow payment gateways to notify your GatePass server about payment status changes in real-time.

Overview

GatePass uses webhooks to receive instant notifications from payment providers (Paystack and Flutterwave) when transactions are completed. This ensures reliable order fulfillment even if users close their browser after payment.

Why Webhooks?

Reliability

Webhooks ensure payment confirmations are received even if users navigate away.

Real-time

Instant notifications allow immediate ticket delivery and order processing.

Security

Cryptographic signatures prevent fraudulent webhook requests.

Webhook Endpoints

GatePass exposes the following webhook endpoints:
ProviderEndpointMethod
Paystackhttps://api.gatepass.app/api/webhooks/paystackPOST
Flutterwavehttps://api.gatepass.app/api/webhooks/flutterwavePOST
All webhook endpoints must be publicly accessible and use HTTPS in production.

Paystack Webhook

Configuration

Set up the webhook URL in your Paystack dashboard:
1

Access Dashboard

Log in to your Paystack Dashboard and navigate to Settings → Webhooks.
2

Add Webhook URL

Enter your webhook URL: https://api.gatepass.app/api/webhooks/paystack
3

Save Configuration

Save the webhook configuration. Paystack will send test events to verify the endpoint.

Webhook Handler

The Paystack webhook verifies signatures and processes payment events:
// From: ~/workspace/source/src/packages/server/src/routes/webhooks.ts:79-102

import { Router } from 'express';
import crypto from 'crypto';
import { prisma } from '../../../database/client';

router.post('/paystack', async (req, res) => {
  const secret = process.env.PAYSTACK_SECRET_KEY;
  
  if (!secret) {
    return res.status(500).json({ error: 'Server configuration error' });
  }

  // 1. Verify webhook signature
  const signature = req.headers['x-paystack-signature'];
  
  if (!signature) {
    return res.status(401).json({ error: 'No signature' });
  }

  const hash = crypto
    .createHmac('sha512', secret)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (hash !== signature) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Process event
  const event = req.body;
  
  if (event.event === 'charge.success') {
    const order = await prisma.order.findFirst({
      where: { paystackReference: event.data.reference }
    });

    if (order) {
      await finalizeOrder(order.id, String(event.data.id));
    }
  }

  // 3. Acknowledge receipt
  res.json({ ok: true });
});

Signature Verification

Paystack signs all webhooks with HMAC SHA512:
Signature Verification
import crypto from 'crypto';

function verifyPaystackSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const hash = crypto
    .createHmac('sha512', secret)
    .update(payload)
    .digest('hex');
  
  return hash === signature;
}

// Usage in Express middleware
app.use('/api/webhooks/paystack', express.raw({ type: 'application/json' }));

app.post('/api/webhooks/paystack', (req, res) => {
  const signature = req.headers['x-paystack-signature'] as string;
  const payload = req.body.toString('utf8');
  
  if (!verifyPaystackSignature(payload, signature, PAYSTACK_SECRET_KEY)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Process webhook...
});
Important: Use express.raw() middleware for Paystack webhooks to preserve the original request body for signature verification.

Event Types

Common Paystack webhook events:
EventDescriptionAction
charge.successPayment successfulFinalize order, mint tickets
charge.failedPayment failedMark order as failed
transfer.successPayout successfulUpdate organizer balance
transfer.failedPayout failedRetry payout

Flutterwave Webhook

Configuration

Set up the webhook URL in your Flutterwave dashboard:
1

Access Dashboard

Log in to your Flutterwave Dashboard and navigate to Settings → Webhooks.
2

Add Webhook URL

Enter: https://api.gatepass.app/api/webhooks/flutterwave
3

Generate Secret Hash

Flutterwave provides a secret hash for webhook verification. Save it to your .env file.
4

Test Webhook

Use Flutterwave’s test event feature to verify your endpoint is working.

Webhook Handler

The Flutterwave webhook validates signatures and processes events:
// From: ~/workspace/source/src/packages/server/src/routes/webhooks.ts:104-123

import { Router } from 'express';
import { validateFlutterwaveWebhook } from '../utils/flutterwave';

router.post('/flutterwave', async (req, res) => {
  // 1. Verify webhook signature
  const signature = req.headers['verif-hash'] as string;
  
  if (!signature || !validateFlutterwaveWebhook(signature, req.body)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Process event
  const { event, data } = req.body;

  if (event === 'charge.completed' && data.status === 'successful') {
    const order = await prisma.order.findFirst({
      where: { flutterwaveReference: data.tx_ref }
    });

    if (order) {
      await finalizeOrder(order.id, String(data.id));
    }
  }

  // 3. Acknowledge receipt
  res.json({ ok: true });
});

Event Types

Common Flutterwave webhook events:
EventDescriptionAction
charge.completedPayment completed successfullyFinalize order
charge.failedPayment failedMark order as failed
transfer.completedPayout completedUpdate organizer balance
Flutterwave uses a simpler signature verification: the verif-hash header must match your secret hash from the dashboard.

Order Finalization

When a webhook confirms payment, the system executes these steps:
1

Update Order Status

Mark the order as COMPLETED and save the payment transaction ID.
2

Mint NFT Tickets

If the user has a connected wallet, mint NFT tickets on Polygon blockchain.
3

Create Ticket Records

Store ticket information with blockchain transaction hash.
4

Send Notification

Create an in-app notification for the user about successful purchase.
// From: ~/workspace/source/src/packages/server/src/routes/webhooks.ts:13-77

import { mintTicketsFor } from '../utils/blockchain';
import { prisma } from '../../../database/client';
import { logger } from '../utils/logger';

async function finalizeOrder(orderId: string, txId: string) {
  // 1. Find order
  const order = await prisma.order.findUnique({
    where: { id: orderId },
    include: { event: true }
  });

  if (!order || order.paymentStatus === 'COMPLETED') {
    return; // Already processed
  }

  // 2. Update payment status
  await prisma.order.update({
    where: { id: order.id },
    data: {
      paymentStatus: 'COMPLETED',
      paymentTxId: txId,
      updatedAt: new Date()
    }
  });

  // 3. Mint NFT tickets
  const user = await prisma.user.findUnique({
    where: { id: order.userId }
  });
  
  const walletAddress = user?.walletAddress;
  const event = order.event;

  if (walletAddress && event.contractAddress) {
    try {
      const abi = [
        'function mintFor(address to, uint256 quantity) external',
        'function tokenCounter() view returns (uint256)'
      ];
      
      const { txHash, tokenIds } = await mintTicketsFor(
        event.contractAddress,
        abi,
        walletAddress,
        order.quantity
      );

      // Save blockchain transaction
      await prisma.order.update({
        where: { id: order.id },
        data: { blockchainTxHash: txHash }
      });

      // Create ticket records
      for (const tokenId of tokenIds) {
        await prisma.ticket.create({
          data: {
            tokenId,
            contractAddress: event.contractAddress,
            chainId: event.chainId,
            txHash,
            eventId: order.eventId,
            orderId: order.id
          }
        });
      }
    } catch (err) {
      logger.error(`Blockchain minting failed for order ${order.id}:`, err);
      // Don't fail the order - tickets can be minted later
    }
  }

  // 4. Send notification
  await prisma.notification.create({
    data: {
      userId: order.userId,
      title: 'Ticket Purchase Successful',
      message: `You successfully purchased ${order.quantity} ticket(s) for ${event.title}.`,
      type: 'SUCCESS'
    }
  });

  logger.info(`Order ${order.id} finalized successfully`);
}

Security Best Practices

Always Verify Signatures

Never process webhooks without signature verification. This prevents malicious requests from unauthorized sources.

Use HTTPS

Webhook endpoints must use HTTPS to encrypt data in transit. Payment gateways may reject HTTP endpoints.

Idempotency

Handle duplicate webhooks gracefully. Payment providers may send the same event multiple times.

Fast Response

Respond to webhooks within 10 seconds. Process heavy tasks (like blockchain minting) asynchronously.

Idempotency Pattern

Idempotent Webhook Processing
async function processWebhook(eventId: string, data: any) {
  // Check if already processed
  const existing = await prisma.webhookLog.findUnique({
    where: { eventId }
  });

  if (existing) {
    console.log(`Event ${eventId} already processed`);
    return { alreadyProcessed: true };
  }

  // Log webhook event
  await prisma.webhookLog.create({
    data: {
      eventId,
      provider: 'paystack',
      payload: data,
      processedAt: new Date()
    }
  });

  // Process order
  await finalizeOrder(data.orderId, data.txId);

  return { success: true };
}

Testing Webhooks

Local Development

Use ngrok or similar tools to expose your local server:
1

Install ngrok

Download and install ngrok.
2

Start Local Server

Run your GatePass server locally on port 3000.
3

Expose with ngrok

Run ngrok http 3000 to get a public URL.
4

Configure Webhook

Use the ngrok URL (e.g., https://abc123.ngrok.io/api/webhooks/paystack) in your payment provider dashboard.
Terminal
# Start your server
npm run dev

# In another terminal, start ngrok
ngrok http 3000

# Output:
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000

Manual Testing

Test webhook handlers with curl:
# Generate signature
SECRET="sk_test_your_secret_key"
PAYLOAD='{"event":"charge.success","data":{"reference":"GP-test-123","id":12345}}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha512 -hmac "$SECRET" | awk '{print $2}')

# Send webhook
curl -X POST https://api.gatepass.app/api/webhooks/paystack \
  -H "Content-Type: application/json" \
  -H "x-paystack-signature: $SIGNATURE" \
  -d "$PAYLOAD"

Monitoring and Debugging

Logging

Log all webhook events for debugging:
Webhook Logging
import { logger } from './logger';

router.post('/paystack', async (req, res) => {
  logger.info('Paystack webhook received', {
    event: req.body.event,
    reference: req.body.data?.reference,
    timestamp: new Date().toISOString()
  });

  try {
    // Process webhook...
    logger.info('Webhook processed successfully');
  } catch (error) {
    logger.error('Webhook processing failed', { error });
    // Still return 200 to avoid retries
  }

  res.json({ ok: true });
});

Error Handling

Always return 200 OK to prevent retries:
Error Handling
router.post('/webhooks/paystack', async (req, res) => {
  try {
    // Verify signature
    if (!verifySignature(req)) {
      logger.warn('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Process event
    await processWebhookEvent(req.body);
    
    res.json({ ok: true });
  } catch (error) {
    logger.error('Webhook error:', error);
    
    // Return 200 to prevent payment provider retries
    // Log error for manual investigation
    res.status(200).json({ 
      ok: false, 
      error: 'Internal error logged' 
    });
  }
});

Environment Variables

Required environment variables for webhooks:
.env
# Paystack
PAYSTACK_SECRET_KEY=sk_test_your_paystack_secret_key

# Flutterwave
FLW_SECRET_KEY=FLWSECK_TEST-your_flutterwave_secret_key
FLW_SECRET_HASH=your_webhook_secret_hash

# Frontend URL for redirects
FRONTEND_URL=http://localhost:5173

# Blockchain configuration (for ticket minting)
RPC_URL=https://polygon-rpc.com
PRIVATE_KEY=your_deployer_private_key

Troubleshooting

Common webhook issues and solutions:
IssueCauseSolution
Signature mismatchIncorrect secret keyVerify .env configuration
Timeout errorsSlow processingRespond immediately, process async
Duplicate eventsProvider retriesImplement idempotency checks
Missing eventsEndpoint unreachableCheck firewall and HTTPS
  1. ✅ Endpoint is publicly accessible via HTTPS
  2. ✅ Secret keys match those in payment provider dashboard
  3. ✅ Webhook URL is correctly configured in provider settings
  4. ✅ Server responds within 10 seconds
  5. ✅ Signature verification is working correctly
  6. ✅ Order references match between system and provider
  7. ✅ Logs show webhook receipt and processing
For complete webhook implementation code, refer to:
  • ~/workspace/source/src/packages/server/src/routes/webhooks.ts
  • ~/workspace/source/src/packages/server/src/utils/flutterwave.ts

Build docs developers (and LLMs) love