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:
Provider Endpoint Method Paystack https://api.gatepass.app/api/webhooks/paystackPOST Flutterwave https://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:
Add Webhook URL
Enter your webhook URL: https://api.gatepass.app/api/webhooks/paystack
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:
Implementation
Webhook Payload Example
// 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:
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:
Event Description Action charge.successPayment successful Finalize order, mint tickets charge.failedPayment failed Mark order as failed transfer.successPayout successful Update organizer balance transfer.failedPayout failed Retry payout
Flutterwave Webhook
Configuration
Set up the webhook URL in your Flutterwave dashboard:
Add Webhook URL
Enter: https://api.gatepass.app/api/webhooks/flutterwave
Generate Secret Hash
Flutterwave provides a secret hash for webhook verification. Save it to your .env file.
Test Webhook
Use Flutterwave’s test event feature to verify your endpoint is working.
Webhook Handler
The Flutterwave webhook validates signatures and processes events:
Implementation
Signature Validation
Webhook Payload Example
// 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:
Event Description Action charge.completedPayment completed successfully Finalize order charge.failedPayment failed Mark order as failed transfer.completedPayout completed Update 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:
Update Order Status
Mark the order as COMPLETED and save the payment transaction ID.
Mint NFT Tickets
If the user has a connected wallet, mint NFT tickets on Polygon blockchain.
Create Ticket Records
Store ticket information with blockchain transaction hash.
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:
Install ngrok
Download and install ngrok .
Start Local Server
Run your GatePass server locally on port 3000.
Expose with ngrok
Run ngrok http 3000 to get a public URL.
Configure Webhook
Use the ngrok URL (e.g., https://abc123.ngrok.io/api/webhooks/paystack) in your payment provider dashboard.
# 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:
Paystack Test
Flutterwave Test
# 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:
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:
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:
# 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:
Issue Cause Solution Signature mismatch Incorrect secret key Verify .env configuration Timeout errors Slow processing Respond immediately, process async Duplicate events Provider retries Implement idempotency checks Missing events Endpoint unreachable Check firewall and HTTPS
✅ Endpoint is publicly accessible via HTTPS
✅ Secret keys match those in payment provider dashboard
✅ Webhook URL is correctly configured in provider settings
✅ Server responds within 10 seconds
✅ Signature verification is working correctly
✅ Order references match between system and provider
✅ 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