Webhooks allow your application to receive real-time notifications when events occur in Blnk. This enables you to build reactive workflows, update your UI, send notifications, and trigger business logic.
How webhooks work
Configure webhook URL - Set your endpoint in blnk.json
Events occur - Transactions, balance changes, reconciliations
Blnk sends HTTP POST - To your configured URL
Verify signature - Ensure webhook authenticity
Process event - Update your application state
Configuring webhooks
Add webhook configuration to your blnk.json:
{
"notification" : {
"webhook" : {
"url" : "https://your-app.com/webhooks/blnk" ,
"headers" : {
"Authorization" : "Bearer your-secret-token"
}
}
}
}
The webhook URL must be publicly accessible and accept POST requests.
Webhook events
Transaction events
transaction.queued
{
"event" : "transaction.queued" ,
"data" : {
"transaction_id" : "txn_abc123xyz" ,
"amount" : 100.00 ,
"reference" : "payment_001" ,
"currency" : "USD" ,
"source" : "bln_customer_wallet" ,
"destination" : "bln_merchant_wallet" ,
"status" : "QUEUED" ,
"created_at" : "2024-01-15T10:30:00Z"
}
}
Triggered when a transaction is queued for processing.
transaction.applied
{
"event" : "transaction.applied" ,
"data" : {
"transaction_id" : "txn_abc123xyz" ,
"amount" : 100.00 ,
"reference" : "payment_001" ,
"currency" : "USD" ,
"source" : "bln_customer_wallet" ,
"destination" : "bln_merchant_wallet" ,
"status" : "APPLIED" ,
"created_at" : "2024-01-15T10:30:00Z"
}
}
Triggered when a transaction is successfully processed.
transaction.inflight
{
"event" : "transaction.inflight" ,
"data" : {
"transaction_id" : "txn_hold_xyz789" ,
"amount" : 200.00 ,
"status" : "INFLIGHT" ,
"inflight" : true ,
"created_at" : "2024-01-15T11:00:00Z"
}
}
Triggered when an inflight (authorization hold) transaction is created.
transaction.void
{
"event" : "transaction.void" ,
"data" : {
"transaction_id" : "txn_void_abc123" ,
"parent_transaction" : "txn_hold_xyz789" ,
"amount" : 200.00 ,
"status" : "VOID" ,
"created_at" : "2024-01-15T12:00:00Z"
}
}
Triggered when an inflight transaction is voided.
transaction.rejected
{
"event" : "transaction.rejected" ,
"data" : {
"transaction_id" : "txn_rejected_def456" ,
"amount" : 500.00 ,
"status" : "REJECTED" ,
"meta_data" : {
"blnk_rejection_reason" : "insufficient balance"
},
"created_at" : "2024-01-15T13:00:00Z"
}
}
Triggered when a transaction is rejected (e.g., insufficient funds).
Balance events
balance.created
{
"event" : "balance.created" ,
"data" : {
"balance_id" : "bln_new_wallet" ,
"ledger_id" : "ldg_123456" ,
"currency" : "USD" ,
"balance" : 0 ,
"created_at" : "2024-01-15T09:00:00Z"
}
}
Triggered when a new balance is created.
balance.monitor
{
"event" : "balance.monitor" ,
"data" : {
"monitor_id" : "mon_abc123xyz" ,
"balance_id" : "bln_customer_wallet" ,
"description" : "Low balance alert" ,
"condition" : {
"field" : "balance" ,
"operator" : "<" ,
"value" : 100.00
},
"current_balance" : {
"balance" : "9500"
},
"triggered_at" : "2024-01-15T14:30:00Z"
}
}
Triggered when a balance monitor condition is met.
Webhook signature verification
Blnk signs all webhooks with HMAC SHA256 to ensure authenticity:
X-Blnk-Signature: abcdef1234567890...
X-Blnk-Timestamp: 1705320600
Verifying signatures (Node.js)
const crypto = require ( 'crypto' );
function verifyWebhookSignature ( req , secret ) {
const signature = req . headers [ 'x-blnk-signature' ];
const timestamp = req . headers [ 'x-blnk-timestamp' ];
const payload = JSON . stringify ( req . body );
// Construct signed payload
const signedPayload = ` ${ timestamp } . ${ payload } ` ;
// Compute HMAC
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( signedPayload )
. digest ( 'hex' );
// Compare signatures
return crypto . timingSafeEqual (
Buffer . from ( signature ),
Buffer . from ( expectedSignature )
);
}
// Usage in Express
app . post ( '/webhooks/blnk' , ( req , res ) => {
const secret = process . env . BLNK_SECRET_KEY ;
if ( ! verifyWebhookSignature ( req , secret )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Process webhook
const { event , data } = req . body ;
console . log ( 'Received event:' , event );
res . status ( 200 ). send ( 'OK' );
});
Verifying signatures (Python)
import hmac
import hashlib
import time
def verify_webhook_signature ( request , secret ):
signature = request.headers.get( 'X-Blnk-Signature' )
timestamp = request.headers.get( 'X-Blnk-Timestamp' )
payload = request.body.decode( 'utf-8' )
# Construct signed payload
signed_payload = f " { timestamp } . { payload } "
# Compute HMAC
expected_signature = hmac.new(
secret.encode( 'utf-8' ),
signed_payload.encode( 'utf-8' ),
hashlib.sha256
).hexdigest()
# Compare signatures (timing-safe)
return hmac.compare_digest(signature, expected_signature)
# Usage in Flask
@app.route ( '/webhooks/blnk' , methods = [ 'POST' ])
def handle_webhook ():
secret = os.environ[ 'BLNK_SECRET_KEY' ]
if not verify_webhook_signature(request, secret):
return 'Invalid signature' , 401
# Process webhook
event = request.json[ 'event' ]
data = request.json[ 'data' ]
print ( f 'Received event: { event } ' )
return 'OK' , 200
Verifying signatures (Go)
package main
import (
" crypto/hmac "
" crypto/sha256 "
" encoding/hex "
" io/ioutil "
" net/http "
)
func verifyWebhookSignature ( r * http . Request , secret string ) bool {
signature := r . Header . Get ( "X-Blnk-Signature" )
timestamp := r . Header . Get ( "X-Blnk-Timestamp" )
body , _ := ioutil . ReadAll ( r . Body )
signedPayload := timestamp + "." + string ( body )
mac := hmac . New ( sha256 . New , [] byte ( secret ))
mac . Write ([] byte ( signedPayload ))
expectedSignature := hex . EncodeToString ( mac . Sum ( nil ))
return hmac . Equal ([] byte ( signature ), [] byte ( expectedSignature ))
}
func handleWebhook ( w http . ResponseWriter , r * http . Request ) {
secret := os . Getenv ( "BLNK_SECRET_KEY" )
if ! verifyWebhookSignature ( r , secret ) {
http . Error ( w , "Invalid signature" , http . StatusUnauthorized )
return
}
// Process webhook
var webhook map [ string ] interface {}
json . NewDecoder ( r . Body ). Decode ( & webhook )
event := webhook [ "event" ].( string )
fmt . Println ( "Received event:" , event )
w . WriteHeader ( http . StatusOK )
}
Implementation code (from webhooks.go:67-124)
How Blnk sends webhooks:
func processHTTP ( data NewWebhook , client * http . Client ) error {
conf , err := config . Fetch ()
if err != nil {
return err
}
payloadBytes , err := json . Marshal ( data )
if err != nil {
return err
}
secret := conf . Server . SecretKey
timestamp := strconv . FormatInt ( time . Now (). Unix (), 10 )
// Create signature
signatureData := timestamp + "." + string ( payloadBytes )
mac := hmac . New ( sha256 . New , [] byte ( secret ))
mac . Write ([] byte ( signatureData ))
signature := hex . EncodeToString ( mac . Sum ( nil ))
req , err := http . NewRequest (
"POST" ,
conf . Notification . Webhook . Url ,
bytes . NewBuffer ( payloadBytes ),
)
if err != nil {
return err
}
req . Header . Set ( "Content-Type" , "application/json" )
req . Header . Set ( "X-Blnk-Signature" , signature )
req . Header . Set ( "X-Blnk-Timestamp" , timestamp )
// Add custom headers from config
for key , value := range conf . Notification . Webhook . Headers {
req . Header . Set ( key , value )
}
resp , err := client . Do ( req )
if err != nil {
return err
}
defer resp . Body . Close ()
if resp . StatusCode < 200 || resp . StatusCode >= 300 {
log . Printf ( "Webhook failed with status %d " , resp . StatusCode )
return nil
}
return nil
}
Common use cases
Send email on transaction
app . post ( '/webhooks/blnk' , async ( req , res ) => {
const { event , data } = req . body ;
if ( event === 'transaction.applied' ) {
await sendEmail ({
to: data . meta_data . user_email ,
subject: 'Payment Received' ,
body: `You received $ ${ data . amount } from ${ data . source } `
});
}
res . status ( 200 ). send ( 'OK' );
});
Update user balance in database
app . post ( '/webhooks/blnk' , async ( req , res ) => {
const { event , data } = req . body ;
if ( event === 'transaction.applied' ) {
// Update your database
await db . users . update ({
where: { wallet_id: data . destination },
data: {
balance: data . amount ,
last_transaction: data . transaction_id
}
});
}
res . status ( 200 ). send ( 'OK' );
});
Trigger payout on balance threshold
app . post ( '/webhooks/blnk' , async ( req , res ) => {
const { event , data } = req . body ;
if ( event === 'balance.monitor' ) {
if ( data . description === 'Ready for payout' ) {
// Trigger payout process
await initiatePayout ({
balance_id: data . balance_id ,
amount: data . current_balance . balance
});
}
}
res . status ( 200 ). send ( 'OK' );
});
Notify customer of low balance
app . post ( '/webhooks/blnk' , async ( req , res ) => {
const { event , data } = req . body ;
if ( event === 'balance.monitor' ) {
if ( data . description === 'Low balance alert' ) {
await sendPushNotification ({
user_id: data . meta_data . user_id ,
title: 'Low Balance' ,
message: `Your wallet balance is below $ ${ data . condition . value } . Please top up.`
});
}
}
res . status ( 200 ). send ( 'OK' );
});
Handle rejected transactions
app . post ( '/webhooks/blnk' , async ( req , res ) => {
const { event , data } = req . body ;
if ( event === 'transaction.rejected' ) {
const reason = data . meta_data . blnk_rejection_reason ;
// Log for debugging
console . error ( 'Transaction rejected:' , {
transaction_id: data . transaction_id ,
reason: reason
});
// Notify user
await notifyUser ({
user_id: data . meta_data . user_id ,
message: `Payment failed: ${ reason } `
});
}
res . status ( 200 ). send ( 'OK' );
});
Best practices
Verify signatures Always verify webhook signatures to prevent spoofing
Return 200 quickly Process webhooks asynchronously and return 200 immediately
Implement idempotency Handle duplicate webhooks gracefully using event IDs
Use HTTPS Only accept webhooks over HTTPS in production
Log all events Keep logs of webhook events for debugging
Monitor failures Set up alerts for webhook endpoint failures
Webhook retry logic
Blnk uses Asynq for reliable webhook delivery:
Automatic retries : Failed webhooks are retried with exponential backoff
Max retries : Configurable (default: 3 attempts)
Queue persistence : Webhooks are queued in Redis for reliability
Implementing idempotency
Handle duplicate webhooks:
const processedEvents = new Set ();
app . post ( '/webhooks/blnk' , async ( req , res ) => {
const { event , data } = req . body ;
const eventKey = ` ${ event } _ ${ data . transaction_id } _ ${ data . created_at } ` ;
// Check if already processed
if ( processedEvents . has ( eventKey )) {
console . log ( 'Duplicate webhook ignored:' , eventKey );
return res . status ( 200 ). send ( 'OK' );
}
// Mark as processed
processedEvents . add ( eventKey );
// Process webhook
await handleWebhookEvent ( event , data );
res . status ( 200 ). send ( 'OK' );
});
For production, use a database or cache instead of in-memory Set.
Troubleshooting
Webhooks not being received
Checklist :
Verify webhook URL is publicly accessible
Check firewall allows incoming POST requests
Ensure endpoint returns 200 status
Review Blnk logs for webhook errors
Test endpoint with curl:
curl -X POST https://your-app.com/webhooks/blnk \
-H "Content-Type: application/json" \
-d '{"event": "test", "data": {}}'
Signature verification failing
Common issues :
Using wrong secret key
Body parsing modifying payload
Timestamp drift (check server time)
Character encoding issues
Solution : Log the raw body and computed signature:
const rawSignature = req . headers [ 'x-blnk-signature' ];
const computedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( signedPayload )
. digest ( 'hex' );
console . log ( 'Received:' , rawSignature );
console . log ( 'Computed:' , computedSignature );
Webhook timeouts
Problem : Endpoint takes too long to respond
Solution : Process asynchronously:
app . post ( '/webhooks/blnk' , async ( req , res ) => {
// Return 200 immediately
res . status ( 200 ). send ( 'OK' );
// Process in background
processWebhookAsync ( req . body ). catch ( err => {
console . error ( 'Webhook processing error:' , err );
});
});
async function processWebhookAsync ( payload ) {
// Heavy processing here
await updateDatabase ( payload );
await sendNotifications ( payload );
}
Next steps
Balance Monitoring Set up balance monitors to trigger webhooks
Authentication Secure your webhook endpoints with API keys