Webhooks
Webhooks are HTTP callbacks that notify your application in real-time when payment events occur. Instead of constantly polling the API to check payment status, Reevit pushes events to your server as they happen.
Understanding Webhook Architecture
Reevit handles two types of webhooks . Understanding the difference is crucial:
Inbound Webhooks (PSP → Reevit)
These are webhooks from payment providers (Paystack, Flutterwave, M-Pesa, etc.) to Reevit .
Key Points:
Reevit handles these automatically - you don’t need to do anything
Configure the Reevit webhook URL in each PSP’s dashboard
Reevit processes provider events and updates payment status
PSP Webhook URLs:
Provider Webhook URL to configure in PSP Dashboard Paystack https://api.reevit.io/webhooks/paystackFlutterwave https://api.reevit.io/webhooks/flutterwaveHubtel https://api.reevit.io/webhooks/hubtelM-Pesa https://api.reevit.io/webhooks/mpesaStripe https://api.reevit.io/webhooks/stripe
You configure these URLs in the payment provider’s dashboard (e.g., Paystack Dashboard), NOT in Reevit.
Reevit automatically injects the correlation metadata needed for PSP webhooks. Use metadata for your own fields like order IDs or cart IDs.
Example: API Usage
curl -X POST https://api.reevit.io/v1/payments/intents \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_abc123" \
-d '{
"amount": 5000,
"currency": "GHS",
"method": "card",
"country": "GH",
"metadata": {
"order_id": "order_12345"
}
}'
Outbound Webhooks (Reevit → Your App)
These are webhooks from Reevit to your application .
Key Points:
You must create a webhook handler in your application
Configure your webhook URL in the Reevit Dashboard
Reevit sends you normalized events (same format regardless of PSP)
You verify signatures using your organization’s signing secret
This is the webhook you configure in Reevit Dashboard > Developers > Webhooks .
Why Webhooks Matter
The Problem with Polling
Without webhooks , you’d need to:
Poll the API every few seconds to check payment status
Waste server resources on unnecessary requests
Experience delays between payment completion and order fulfillment
Miss events if your polling interval is too long
Example : Customer completes payment at 10:00:00, but you only poll at 10:00:30 → 30-second delay before order fulfillment.
The Webhook Solution
With webhooks :
Instant notifications : Know immediately when payments succeed or fail
Efficient : No wasted API calls
Reliable : Events are delivered even if you’re not actively checking
Scalable : Works with thousands of payments per minute
Result : Faster order fulfillment, better customer experience, lower server costs.
How Outbound Webhooks Work
The Flow
1. Payment Event Occurs → 2. Reevit Sends Webhook → 3. Your Server Processes → 4. You Respond
Event Occurs : Payment succeeds, fails, or changes status
Reevit Sends : POST request to your webhook URL with event data
Your Server Processes : Verify signature, update database, fulfill order
You Respond : Return 2xx status code to acknowledge receipt
Key Concepts
Webhook URL : Your server endpoint that receives events
Signing Secret : Unique secret per organization for signature verification
Event Types : Different events for different payment states
Signatures : Cryptographic signatures to verify events are from Reevit
Idempotency : Events include IDs to prevent duplicate processing
Retries : Failed deliveries are automatically retried
Setting Up Webhooks
Via Dashboard (Recommended)
Go to Settings → Webhooks in your Reevit dashboard
Enter your webhook endpoint URL (must be HTTPS)
Select which events you want to receive
Copy the webhook signing secret
Save configuration
Benefits :
Visual interface
Easy to update
See delivery status
Via API
curl -X POST https://api.reevit.io/v1/webhooks/config \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123" \
-H "Idempotency-Key: webhook-config-001" \
-d '{
"url": "https://yourapp.com/webhooks/reevit",
"events": [
"payment.succeeded",
"payment.failed",
"payment.refunded",
"subscription.renewed"
],
"enabled": true
}'
Webhook URL Requirements
HTTPS only : HTTP endpoints are not supported for security
Publicly accessible : Must be reachable from the internet
Fast response : Return 2xx within 30 seconds
Idempotent : Handle duplicate events gracefully
Get Configuration
curl https://api.reevit.io/v1/webhooks/config \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123"
Response:
{
"url" : "https://yourapp.com/webhooks/reevit" ,
"events" : [ "payment.succeeded" , "payment.failed" ],
"enabled" : true ,
"signing_secret" : "whsec_xxx" ,
"created_at" : "2025-02-10T10:00:00Z"
}
Event Types
Reevit sends different events for different scenarios. Subscribe only to events you need.
Payment Events
Event When It Fires Use Case payment.createdPayment intent created Log payment attempts payment.succeededPayment completed successfully Fulfill order, send confirmation payment.failedPayment failed Notify customer, allow retry payment.canceledPayment canceled Release inventory, update order payment.refundedRefund completed Update order status, notify customer
Subscription Events
Event When It Fires Use Case subscription.createdNew subscription created Activate subscription, send welcome email subscription.updatedSubscription modified Update subscription status subscription.canceledSubscription canceled Deactivate access, send cancellation email subscription.pausedSubscription paused Suspend access temporarily subscription.resumedSubscription resumed Reactivate access
Invoice Events
Event When It Fires Use Case invoice.createdInvoice generated Notify customer of upcoming charge invoice.paidInvoice payment succeeded Extend subscription, grant access invoice.failedInvoice payment failed Notify customer, retry payment invoice.canceledInvoice canceled Update subscription status
Webhook Payload
Every webhook includes a consistent payload structure:
{
"id" : "evt_abc123" ,
"type" : "payment.succeeded" ,
"org_id" : "org_123" ,
"created_at" : "2025-02-10T10:05:00Z" ,
"data" : {
"id" : "pay_xyz789" ,
"amount" : 5000 ,
"currency" : "GHS" ,
"status" : "succeeded" ,
"provider" : "paystack" ,
"customer_id" : "cust_456" ,
"metadata" : {
"order_id" : "12345"
}
}
}
Payload Fields
id : Unique event identifier (use for idempotency)
type : Event type (e.g., payment.succeeded)
org_id : Your organization ID
created_at : When the event occurred (ISO 8601)
data : Event-specific data (varies by event type)
Using Event Data
Example: Fulfill Order on Payment Success
if ( event . type === 'payment.succeeded' ) {
const payment = event . data ;
const orderId = payment . metadata . order_id ;
// Fulfill order
await fulfillOrder ( orderId );
// Send confirmation email
await sendConfirmationEmail ( payment . customer_id );
// Update analytics
await trackPurchase ( payment );
}
🔒 Webhook Security
Webhooks deliver sensitive payment data to your application. Follow these security practices to protect your integration.
Signature Verification (Critical)
Never process webhooks without signature verification . This is the most critical security step.
Reevit signs each webhook with HMAC-SHA256. Your server must verify this signature before processing any event.
What happens without verification?
Attackers could send fake “payment succeeded” events
Orders could be fulfilled without actual payment
Financial losses and fraud
Anti-Replay Protection
Reebit’s webhooks include a timestamp field to prevent replay attacks:
const WEBHOOK_TOLERANCE = 300 ; // 5 minutes
function verifyWebhook ( payload : string , signature : string , secret : string ) : boolean {
// 1. Verify signature
if ( ! verifySignature ( payload , signature , secret )) {
return false ;
}
// 2. Check timestamp to prevent replay attacks
const parsed = JSON . parse ( payload );
const timestamp = parsed . timestamp || 0 ;
const now = Math . floor ( Date . now () / 1000 );
if ( Math . abs ( now - timestamp ) > WEBHOOK_TOLERANCE ) {
console . log ( 'Webhook timestamp outside tolerance window' );
return false ;
}
return true ;
}
Best Practices
Always use raw request body for verification
// ❌ Wrong - Express parses body, breaking signature
app . post ( '/webhooks' , ( req , res ) => {
verifySignature ( req . body , req . headers [ 'x-reevit-signature' ], secret );
});
// ✅ Correct - use raw body
app . post ( '/webhooks' , express . raw ({ type: 'application/json' }), ( req , res ) => {
const signature = req . headers [ 'x-reevit-signature' ];
verifySignature ( req . body . toString (), signature , secret );
});
Respond quickly (under 5 seconds)
Return 2xx immediately after verification
Process events asynchronously
Reevit will retry failed deliveries
Implement idempotent handlers
async function handleWebhook ( event ) {
const eventId = event . id ;
// Check if already processed
const processed = await db . webhookEvents . findUnique ({ where: { id: eventId } });
if ( processed ) {
return res . send ({ status: 'already_processed' });
}
// Mark as processing
await db . webhookEvents . create ({ id: eventId , status: 'processing' });
try {
await processEvent ( event );
await db . webhookEvents . update ({ where: { id: eventId }, data: { status: 'completed' } });
} catch ( err ) {
await db . webhookEvents . update ({ where: { id: eventId }, data: { status: 'failed' } });
throw err ;
}
}
Use HTTPS endpoints
Webhook URLs must use HTTPS
Reevit rejects HTTP endpoints
Use valid TLS certificates (not self-signed)
Limit webhook IP ranges
Reevit webhooks come from Cloudflare IPs
Configure firewall to allow only these ranges
Security Checklist
Review this checklist before going live:
Verifying Webhooks
Always verify webhook signatures . This ensures events are actually from Reevit and haven’t been tampered with.
Why Verification Matters
Without verification, attackers could:
Send fake payment success events
Trigger order fulfillment without payment
Cause financial losses
Compromise your system
How Verification Works
Reevit signs each webhook using HMAC-SHA256 with your organization’s signing secret:
Header : X-Reevit-Signature: sha256=<hex-signature>
Signature : HMAC-SHA256(request_body, signing_secret)
Verification : Compare using constant-time comparison
Getting Your Signing Secret
Your signing secret is automatically generated when you first configure a webhook endpoint:
Go to Reevit Dashboard > Developers > Webhooks
Add your webhook endpoint URL
Click the copy button next to “Signing Secret”
Add to your environment: REEVIT_WEBHOOK_SECRET=whsec_xxx...
The signing secret is unique per organization and starts with whsec_. It’s generated once and persists across webhook URL updates.
Next.js App Router Example (Recommended)
// app/api/webhooks/reevit/route.ts
import { NextRequest , NextResponse } from "next/server" ;
import crypto from "crypto" ;
function verifySignature ( payload : string , signature : string , secret : string ) : boolean {
if ( ! signature . startsWith ( "sha256=" )) return false ;
const expected = crypto
. createHmac ( "sha256" , secret )
. update ( payload )
. digest ( "hex" );
const received = signature . slice ( 7 ); // Remove "sha256=" prefix
if ( received . length !== expected . length ) return false ;
return crypto . timingSafeEqual ( Buffer . from ( received ), Buffer . from ( expected ));
}
export async function POST ( request : NextRequest ) {
const rawBody = await request . text ();
const signature = request . headers . get ( "x-reevit-signature" ) || "" ;
const secret = process . env . REEVIT_WEBHOOK_SECRET ! ;
// Verify signature
if ( ! verifySignature ( rawBody , signature , secret )) {
return NextResponse . json ({ error: "Invalid signature" }, { status: 401 });
}
const event = JSON . parse ( rawBody );
// Handle events
switch ( event . type ) {
case "reevit.webhook.test" :
console . log ( "Test webhook received" );
break ;
case "payment.succeeded" :
await handlePaymentSucceeded ( event . data );
break ;
case "payment.failed" :
await handlePaymentFailed ( event . data );
break ;
}
return NextResponse . json ({ received: true });
}
async function handlePaymentSucceeded ( data : any ) {
const orderId = data . metadata ?. order_id ;
// Update order status, send confirmation email, etc.
console . log ( `Payment ${ data . id } succeeded for order ${ orderId } ` );
}
async function handlePaymentFailed ( data : any ) {
// Notify customer, allow retry, etc.
console . log ( `Payment ${ data . id } failed` );
}
Express.js Example
const crypto = require ( 'crypto' );
const express = require ( 'express' );
function verifySignature ( payload , signature , secret ) {
if ( ! signature . startsWith ( 'sha256=' )) return false ;
const expected = crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
const received = signature . slice ( 7 );
if ( received . length !== expected . length ) return false ;
return crypto . timingSafeEqual (
Buffer . from ( received ),
Buffer . from ( expected )
);
}
// IMPORTANT: Use raw body for signature verification
app . post ( '/webhooks/reevit' , express . raw ({ type: 'application/json' }), ( req , res ) => {
const signature = req . headers [ 'x-reevit-signature' ];
const payload = req . body . toString ();
const secret = process . env . REEVIT_WEBHOOK_SECRET ;
if ( ! verifySignature ( payload , signature , secret )) {
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
const event = JSON . parse ( payload );
switch ( event . type ) {
case 'payment.succeeded' :
// Fulfill order
break ;
case 'payment.failed' :
// Notify customer
break ;
}
res . status ( 200 ). json ({ received: true });
});
Go Example
package main
import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" encoding/json "
" io "
" net/http "
" os "
" strings "
)
func verifySignature ( payload [] byte , signature , secret string ) bool {
if ! strings . HasPrefix ( signature , "sha256=" ) {
return false
}
mac := hmac . New ( sha256 . New , [] byte ( secret ))
mac . Write ( payload )
expected := hex . EncodeToString ( mac . Sum ( nil ))
received := signature [ 7 :] // Remove "sha256=" prefix
return hmac . Equal ([] byte ( received ), [] byte ( expected ))
}
func webhookHandler ( w http . ResponseWriter , r * http . Request ) {
body , _ := io . ReadAll ( r . Body )
signature := r . Header . Get ( "X-Reevit-Signature" )
secret := os . Getenv ( "REEVIT_WEBHOOK_SECRET" )
if ! verifySignature ( body , signature , secret ) {
http . Error ( w , "Invalid signature" , http . StatusUnauthorized )
return
}
var event map [ string ] interface {}
json . Unmarshal ( body , & event )
// Handle event based on type
switch event [ "type" ] {
case "payment.succeeded" :
// Fulfill order
case "payment.failed" :
// Notify customer
}
w . WriteHeader ( http . StatusOK )
json . NewEncoder ( w ). Encode ( map [ string ] bool { "received" : true })
}
Python (Flask) Example
import hmac
import hashlib
import os
from flask import Flask, request, jsonify
app = Flask( __name__ )
def verify_signature ( payload : bytes , signature : str , secret : str ) -> bool :
if not signature.startswith( 'sha256=' ):
return False
expected = hmac.new(
secret.encode( 'utf-8' ),
payload,
hashlib.sha256
).hexdigest()
received = signature[ 7 :] # Remove "sha256=" prefix
return hmac.compare_digest(received, expected)
@app.route ( '/webhooks/reevit' , methods = [ 'POST' ])
def webhook ():
payload = request.get_data()
signature = request.headers.get( 'X-Reevit-Signature' , '' )
secret = os.environ[ 'REEVIT_WEBHOOK_SECRET' ]
if not verify_signature(payload, signature, secret):
return jsonify({ 'error' : 'Invalid signature' }), 401
event = request.get_json()
if event[ 'type' ] == 'payment.succeeded' :
# Fulfill order
pass
elif event[ 'type' ] == 'payment.failed' :
# Notify customer
pass
return jsonify({ 'received' : True })
PHP Example
<? php
function verifySignature ( string $payload , string $signature , string $secret ) : bool {
if ( strpos ( $signature , 'sha256=' ) !== 0 ) {
return false ;
}
$expected = hash_hmac ( 'sha256' , $payload , $secret );
$received = substr ( $signature , 7 ); // Remove "sha256=" prefix
return hash_equals ( $expected , $received );
}
// Webhook handler
$payload = file_get_contents ( 'php://input' );
$signature = $_SERVER [ 'HTTP_X_REEVIT_SIGNATURE' ] ?? '' ;
$secret = getenv ( 'REEVIT_WEBHOOK_SECRET' );
if ( ! verifySignature ( $payload , $signature , $secret )) {
http_response_code ( 401 );
echo json_encode ([ 'error' => 'Invalid signature' ]);
exit ;
}
$event = json_decode ( $payload , true );
switch ( $event [ 'type' ]) {
case 'payment.succeeded' :
// Fulfill order
break ;
case 'payment.failed' :
// Notify customer
break ;
}
http_response_code ( 200 );
echo json_encode ([ 'received' => true ]);
Processing Webhooks
Best Practices
Respond Quickly : Return 2xx within 30 seconds
Process Async : Queue events for background processing
Handle Duplicates : Use event ID for idempotency
Verify Signatures : Always verify webhook authenticity
Log Events : Log all events for debugging and audit
Idempotency
Events include unique IDs. Use these to prevent duplicate processing:
// Check if event already processed
const existingEvent = await db . events . findOne ({ id: event . id });
if ( existingEvent ) {
return res . status ( 200 ). send ( 'Already processed' );
}
// Process event
await processEvent ( event );
// Store event ID
await db . events . insert ({ id: event . id , processed_at: new Date () });
Error Handling
Return 2xx for success : Even if processing fails internally, return 2xx to acknowledge receipt. Process failures in background.
Return 4xx/5xx for retry : Only return error codes if you want Reevit to retry:
4xx : Client error (won’t retry)
5xx : Server error (will retry)
Example :
app . post ( '/webhooks/reevit' , async ( req , res ) => {
try {
// Verify signature
if ( ! verifyWebhook ( req . body , signature , secret )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
const event = JSON . parse ( req . body );
// Queue for processing (don't block webhook response)
await queue . add ( 'process-webhook' , event );
// Return success immediately
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
// Log error but still return 200
console . error ( 'Webhook error:' , error );
res . status ( 200 ). send ( 'OK' );
}
});
Testing Webhooks
Test Events
Send a test event to verify your endpoint:
curl -X POST https://api.reevit.io/v1/webhooks/test \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123"
Use Cases :
Initial setup : Verify endpoint is working
After changes : Test after updating webhook URL
Debugging : Test signature verification
Local Development
For local development, use tools like:
ngrok : Expose local server to internet
webhook.site : Temporary webhook URLs for testing
Stripe CLI : Forward webhooks to local server
Example with ngrok :
# Start ngrok
ngrok http 3000
# Use ngrok URL in webhook config
# https://abc123.ngrok.io/webhooks/reevit
Viewing Webhook Events
List Events
curl "https://api.reevit.io/v1/webhooks/events?limit=20" \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123"
Use Cases :
Debugging : See what events were sent
Audit : Review event history
Monitoring : Check event delivery
Get Single Event
curl https://api.reevit.io/v1/webhooks/events/evt_abc123 \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123"
Use Cases :
Investigation : Review specific event details
Replay : Get event data for replaying
Replaying Events
Re-send a webhook event if delivery failed or you need to reprocess:
curl -X POST https://api.reevit.io/v1/webhooks/events/evt_abc123/replay \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123"
Use Cases :
Failed delivery : Retry after fixing endpoint
Reprocessing : Reprocess event after bug fix
Testing : Test event processing logic
Delivery Status
View Delivery Attempts
curl https://api.reevit.io/v1/webhooks/outbound \
-H "X-Reevit-Key: pfk_live_xxx.secret" \
-H "X-Org-Id: org_123"
Response:
{
"deliveries" : [
{
"id" : "del_123" ,
"event_id" : "evt_abc123" ,
"url" : "https://yourapp.com/webhooks/reevit" ,
"status" : "delivered" ,
"response_code" : 200 ,
"attempts" : 1 ,
"delivered_at" : "2025-02-10T10:05:01Z"
},
{
"id" : "del_124" ,
"event_id" : "evt_def456" ,
"url" : "https://yourapp.com/webhooks/reevit" ,
"status" : "failed" ,
"response_code" : 500 ,
"attempts" : 5 ,
"last_attempt_at" : "2025-02-10T10:10:00Z"
}
]
}
Delivery Statuses
delivered : Successfully delivered and acknowledged
pending : Queued for delivery
failed : Failed after all retry attempts
retrying : Currently retrying after failure
Retry Behavior
Reevit automatically retries failed webhook deliveries with exponential backoff:
Attempt Delay Total Time 1 Immediate 0s 2 1 minute 1m 3 5 minutes 6m 4 30 minutes 36m 5 2 hours 2h 36m
After 5 failed attempts, the event is marked as failed and won’t be retried automatically.
Why Retries Matter
Network issues : Temporary network problems resolve themselves
Server restarts : Your server may be restarting
Rate limiting : Temporary rate limits may clear
Transient errors : Some errors are temporary
Best Practice : Make your webhook endpoint idempotent so retries are safe.
Security Best Practices
Always Verify Signatures : Never process unverified webhooks
Use HTTPS : Only accept webhooks over HTTPS
Validate Event Data : Don’t trust event data blindly
Rate Limiting : Implement rate limiting on webhook endpoint
Monitor Failures : Alert on repeated webhook failures
Keep Secrets Secure : Never expose webhook secrets
Use Idempotency : Handle duplicate events gracefully
Common Patterns
Order Fulfillment
if ( event . type === 'payment.succeeded' ) {
const orderId = event . data . metadata . order_id ;
await fulfillOrder ( orderId );
await sendConfirmationEmail ( event . data . customer_id );
}
Subscription Management
if ( event . type === 'subscription.renewed' ) {
const subscription = event . data ;
await extendAccess ( subscription . customer_id );
await sendRenewalConfirmation ( subscription );
}
Refund Processing
if ( event . type === 'payment.refunded' ) {
const refund = event . data ;
await updateOrderStatus ( refund . metadata . order_id , 'refunded' );
await notifyCustomer ( refund . customer_id , 'refund_processed' );
}
Failed Payment Handling
if ( event . type === 'payment.failed' ) {
const payment = event . data ;
await notifyCustomer ( payment . customer_id , 'payment_failed' );
await allowRetry ( payment . id );
}
Troubleshooting
Webhooks Not Received
Check URL : Verify webhook URL is correct and accessible
Check HTTPS : Ensure URL uses HTTPS
Check Firewall : Ensure firewall allows Reevit IPs
Check Logs : Review delivery logs for errors
Test Endpoint : Use test endpoint to verify connectivity
Signature Verification Failing
Check Secret : Verify webhook secret is correct
Check Payload : Ensure you’re using raw request body
Check Encoding : Ensure proper string encoding
Check Headers : Verify signature header name
Duplicate Events
Use Event IDs : Check event ID before processing
Implement Idempotency : Make handlers idempotent
Check Retries : Verify retry logic isn’t causing duplicates
Next Steps
Payments See how payment intents, statuses, and refunds work end-to-end.
Subscriptions Set up automated recurring billing and dunning policies.
Workflows Automate actions based on payment and webhook events.
API Reference Review endpoint schemas and headers for webhook setup and retries.