What are Webhooks?
Webhooks are HTTP callbacks that KillB sends to your server when events occur. They enable real-time notifications about ramps, users, accounts, and transactions without constant polling.
Webhooks are the recommended way to monitor transaction status and keep your application synchronized with KillB.
How Webhooks Work
Webhook Event Structure
All webhook events follow this structure:
{
id : string , // UUID - unique event ID
event : string , // Event type: RAMP | USER | TRANSACTION | ACCOUNT | CUSTODIAL_ACCOUNT
action : string , // Action: CREATE | UPDATE | DELETE
data : object , // Event-specific data
createdAt : string , // ISO 8601 timestamp
updatedAt : string , // ISO 8601 timestamp
attempts : number // Delivery attempt count (0 to 5)
}
Webhook Events
KillB supports the following event types:
RAMP
USER
ACCOUNT
TRANSACTION
CUSTODIAL_ACCOUNT
Ramp transaction events Event Type: RAMPActions:
CREATE - New ramp created
UPDATE - Ramp status changed
DELETE - Ramp canceled (rare)
Payload Example: {
"id" : "evt_a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" ,
"event" : "RAMP" ,
"action" : "UPDATE" ,
"data" : {
"id" : "be4d353b-00a2-4309-9ef1-594f37dfb1fd" ,
"userId" : "2bc703f2-1c54-4cbb-a144-993e1d957688" ,
"accountId" : "f7df00af-5d12-4b33-b5ab-e2b32290ebad" ,
"quotationId" : "a656a475-0d53-479e-8f30-91582ecf450b" ,
"type" : "ON" ,
"status" : "COMPLETED" ,
"cashInMethod" : "PSE" ,
"cashOutMethod" : "POLYGON" ,
"fromCurrency" : "COP" ,
"toCurrency" : "USDC" ,
"fromAmount" : 400000 ,
"toAmount" : 98.50 ,
"transferProof" : "0x8e2b1f4645ebc8b6f658c860980567a63d283a42735be7401a7950306a2539b0" ,
"isPreFunded" : false ,
"active" : true ,
"accounts" : [
{
"id" : "f7df00af-5d12-4b33-b5ab-e2b32290ebad" ,
"amount" : 98
}
],
"createdAt" : "2025-01-15T23:46:10.226Z" ,
"updatedAt" : "2025-01-16T00:29:06.813Z"
},
"createdAt" : "2025-01-16T00:29:06.813Z" ,
"updatedAt" : "2025-01-16T00:29:06.813Z" ,
"attempts" : 0
}
Common Status Values:
QUOTE_CREATED - Quote created
PENDING_PAYMENT - Waiting for payment
CASH_IN_PROCESSING - Payment being processed
CASH_IN_COMPLETED - Payment confirmed
PROCESSING - Converting currency
CASH_OUT_PROCESSING - Sending funds
COMPLETED - Ramp completed
FAILED - Ramp failed
CANCELED - Ramp canceled
User and KYC events Event Type: USERActions:
CREATE - New user registered
UPDATE - User information or KYC status changed
DELETE - User deleted (rare)
Payload Example: {
"id" : "evt_f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d" ,
"event" : "USER" ,
"action" : "UPDATE" ,
"data" : {
"id" : "e3d5c4ca-839a-4067-af76-89b33b19696e" ,
"type" : "PERSON" ,
"customerId" : "c650925e-a4aa-4a5f-a0a9-6c97be6c6ce5" ,
"externalId" : "user-ext-12345" ,
"status" : "ACTIVE" ,
"accessLevel" : "L2" ,
"data" : {
"firstName" : "Carlos" ,
"lastName" : "Rodriguez" ,
"email" : "[email protected] " ,
"phone" : "+573001234567" ,
"dateOfBirth" : "1985-03-20" ,
"address" : {
"street1" : "Calle 45 #12-34" ,
"street2" : "Apt 501" ,
"city" : "Bogotá" ,
"state" : "Cundinamarca" ,
"zipCode" : "110111" ,
"countryCode" : "CO"
},
"document" : {
"type" : "NUIP" ,
"number" : "1234567890" ,
"issuedCountryCode" : "CO" ,
"expeditionDate" : "2015-01-15" ,
"cic" : null ,
"identificadorCiudadano" : null ,
"ocr" : null ,
"numeroEmision" : null
}
},
"note" : null
},
"createdAt" : "2025-01-15T10:30:00.000Z" ,
"updatedAt" : "2025-01-15T14:22:00.000Z" ,
"attempts" : 0
}
Status Values:
PENDING - Awaiting verification
ACTIVE - Verified and active
REJECTED - KYC rejected
SUSPENDED - Account suspended
Access Levels:
L0 - Unverified
L1 - Basic verification
L2 - Standard verification
L3 - Enhanced verification
L4 - Premium verification
Account verification events Event Type: ACCOUNTActions:
CREATE - New account added
UPDATE - Account status or details changed
DELETE - Account deleted
Payload Example: {
"id" : "evt_c9b8a7f6-d5e4-4321-9876-543210fedcba" ,
"event" : "ACCOUNT" ,
"action" : "UPDATE" ,
"data" : {
"id" : "543ab81d-0b1e-4b9d-88bc-58ba5a365f16" ,
"type" : "PSE" ,
"status" : "ACTIVE" ,
"externalId" : "bank-acc-7890" ,
"data" : {
"firstName" : "María" ,
"lastName" : "González" ,
"email" : "[email protected] " ,
"phone" : "+573009876543" ,
"countryCode" : "CO" ,
"accountNumber" : "9876543210" ,
"bankCode" : "0007" ,
"type" : "savings" ,
"document" : {
"type" : "NUIP" ,
"number" : "9876543210" ,
"issuedCountryCode" : "CO"
}
},
"complianceUrl" : "https://in.sumsub.com/websdk/p/AbCd1234EfGh5678"
},
"createdAt" : "2025-01-15T09:15:00.000Z" ,
"updatedAt" : "2025-01-15T09:45:00.000Z" ,
"attempts" : 0
}
Account Types:
PSE - Colombian bank account
SPEI - Mexican bank account
ACH - US bank account
WIRE - Wire transfer account
WALLET - Crypto wallet
PIX - Brazilian PIX account
TRANSFIYA - Colombian mobile wallet
Status Values:
PENDING - Awaiting verification
ACTIVE - Verified and active
REJECTED - Verification failed
INACTIVE - Disabled
Savings account transactions Event Type: TRANSACTIONActions:
CREATE - New transaction initiated
UPDATE - Transaction status changed
DELETE - Transaction canceled (rare)
Payload Example: {
"id" : "evt_1a2b3c4d-5e6f-7890-abcd-ef1234567890" ,
"event" : "TRANSACTION" ,
"action" : "UPDATE" ,
"data" : {
"id" : "txn_9876543210abcdef" ,
"userId" : "e3d5c4ca-839a-4067-af76-89b33b19696e" ,
"savingsAccountId" : "sav_1234567890abcdef" ,
"type" : "DEPOSIT" ,
"status" : "COMPLETED" ,
"amount" : "1000.00" ,
"currency" : "USD" ,
"method" : "ACH" ,
"reference" : "DEP-20250115-001" ,
"completedAt" : "2025-01-15T10:35:00.000Z"
},
"createdAt" : "2025-01-15T10:30:00.000Z" ,
"updatedAt" : "2025-01-15T10:35:00.000Z" ,
"attempts" : 0
}
Transaction Types:
DEPOSIT - Deposit to savings
WITHDRAWAL - Withdrawal from savings
INTEREST - Interest payment
FEE - Fee charge
Custodial account events Event Type: CUSTODIAL_ACCOUNTActions:
CREATE - Custodial account created
UPDATE - Balance or status updated
DELETE - Account closed (rare)
Payload Example: {
"id" : "evt_abcd1234-ef56-7890-1234-567890abcdef" ,
"event" : "CUSTODIAL_ACCOUNT" ,
"action" : "UPDATE" ,
"data" : {
"id" : "cust_1234567890abcdef" ,
"userId" : "e3d5c4ca-839a-4067-af76-89b33b19696e" ,
"type" : "SAVINGS" ,
"status" : "ACTIVE" ,
"balance" : "5250.00" ,
"currency" : "USD" ,
"previousBalance" : "5000.00" ,
"changeAmount" : "250.00" ,
"changeReason" : "DEPOSIT" ,
"lastTransactionId" : "txn_9876543210abcdef" ,
"interestRate" : "4.50" ,
"updatedAt" : "2025-01-15T10:35:00.000Z"
},
"createdAt" : "2025-01-01T00:00:00.000Z" ,
"updatedAt" : "2025-01-15T10:35:00.000Z" ,
"attempts" : 0
}
Change Reasons:
DEPOSIT - Funds deposited
WITHDRAWAL - Funds withdrawn
INTEREST - Interest credited
FEE - Fee charged
ADJUSTMENT - Manual adjustment
Setting Up Webhooks
Create Webhook Configuration
Request:
{
"url" : "https://api.yourapp.com/webhooks/killb" ,
"secret" : "your-secret-key-min-32-chars" ,
"events" : [ "RAMP" , "USER" , "ACCOUNT" ]
}
Response:
{
"id" : "webhook-id" ,
"customerId" : "customer-id" ,
"url" : "https://api.yourapp.com/webhooks/killb" ,
"events" : [ "RAMP" , "USER" , "ACCOUNT" ],
"active" : true ,
"createdAt" : "2024-01-15T10:30:00.000Z"
}
const setupWebhook = async () => {
const response = await fetch ( 'https://teste-94u93qnn.uc.gateway.dev/api/v2/webhooks' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ token } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
url: 'https://api.yourapp.com/webhooks/killb' ,
secret: process . env . WEBHOOK_SECRET ,
events: [ 'RAMP' , 'USER' , 'ACCOUNT' , 'TRANSACTION' ]
})
});
return await response . json ();
};
Webhook Security
Signature Verification
KillB signs all webhook requests with HMAC SHA-256. Always verify signatures to ensure authenticity.
Header: x-signature-sha256
Node.js (Express)
FastAPI
PHP
const crypto = require ( 'crypto' );
const express = require ( 'express' );
app . post ( '/webhooks/killb' , express . raw ({ type: 'application/json' }), ( req , res ) => {
const signature = req . headers [ 'x-signature-sha256' ];
const payload = req . body . toString ();
// Calculate expected signature
const expectedSignature = crypto
. createHmac ( 'sha256' , process . env . WEBHOOK_SECRET )
. update ( payload )
. digest ( 'hex' );
// Verify signature
if ( signature !== expectedSignature ) {
console . error ( 'Invalid webhook signature' );
return res . status ( 401 ). json ({ error: 'Invalid signature' });
}
// Parse and process event
const event = JSON . parse ( payload );
processWebhookEvent ( event );
res . status ( 200 ). json ({ received: true });
});
Never skip signature verification! Unverified webhooks could be spoofed by attackers.
Webhook Requirements
Your webhook endpoint must:
Return 200 OK within 5 seconds
Process events asynchronously
Don’t wait for slow operations
Use job queues for heavy processing
Be idempotent (handle duplicate events)
Use event ID to detect duplicates
Store processed event IDs
Skip already-processed events
Maintain >99% uptime
Use HTTPS only
Have valid SSL certificate
Respond with proper status codes
Always check x-signature-sha256
Use constant-time comparison
Reject invalid signatures
Log verification failures
Retry Logic
KillB automatically retries failed webhook deliveries with exponential backoff:
Retry Schedule:
Attempt 0: Immediate delivery
Attempt 1: After 1 minute
Attempt 2: After 5 minutes
Attempt 3: After 15 minutes
Attempt 4: After 1 hour
Attempt 5: After 6 hours (final attempt)
Failure Criteria:
Non-2xx response code
Connection timeout (> 5 seconds)
Network error
The attempts field in the webhook payload indicates the current delivery attempt (0 to 5).
// Check retry count
if ( webhookEvent . attempts > 3 ) {
console . warn ( `High retry count: ${ webhookEvent . attempts } ` );
// Alert monitoring system
}
After attempt 5 fails, the webhook is marked as permanently failed and requires manual intervention. Check your webhook logs and fix any issues.
Testing Webhooks
Local Testing with ngrok
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# Expose to internet
ngrok http 3000
# Use ngrok URL in webhook config
# https://abc123.ngrok.io/webhooks/killb
Manual Testing
Trigger test events in sandbox:
// Create test ramp
const ramp = await createRamp ( quoteId , userId , accountId );
// Simulate completion (triggers webhooks)
await fetch ( '/api/v2/faker/cash-in' , {
method: 'POST' ,
headers: { 'Authorization' : `Bearer ${ token } ` },
body: JSON . stringify ({ rampId: ramp . id })
});
// Your webhook should receive ramp.cash_in_completed event
Managing Webhooks
Get Webhook Config
Returns your current webhook configuration.
Update Webhook
Update:
URL
Secret
Events to subscribe
Active status
{
"url" : "https://api.yourapp.com/webhooks/killb-v2" ,
"active" : true ,
"events" : [ "RAMP" , "USER" ]
}
Delete Webhook
Removes webhook configuration. Events will no longer be sent.
Event Processing
Example Handler
const processWebhookEvent = ( webhookEvent ) => {
const { id , event , action , data , attempts } = webhookEvent ;
console . log ( `Processing ${ event } . ${ action } (attempt ${ attempts } )` );
// Route based on event type and action
switch ( event ) {
case 'RAMP' :
handleRampEvent ( action , data );
break ;
case 'USER' :
handleUserEvent ( action , data );
break ;
case 'ACCOUNT' :
handleAccountEvent ( action , data );
break ;
case 'TRANSACTION' :
handleTransactionEvent ( action , data );
break ;
case 'CUSTODIAL_ACCOUNT' :
handleCustodialEvent ( action , data );
break ;
default :
console . log ( `Unhandled event type: ${ event } ` );
}
};
const handleRampEvent = async ( action , data ) => {
if ( action === 'UPDATE' && data . status === 'COMPLETED' ) {
// Update database
await db . ramps . update ( data . id , {
status: data . status ,
updatedAt: data . updatedAt ,
transferProof: data . transferProof
});
// Notify user
await sendEmail ( data . userId , 'Transaction Completed' , {
amount: data . toAmount ,
currency: data . toCurrency ,
txHash: data . transferProof
});
// Update internal systems
await updateAccounting ( data );
}
};
const handleUserEvent = async ( action , data ) => {
if ( action === 'UPDATE' ) {
// Check if KYC level changed
await db . users . update ( data . id , {
status: data . status ,
accessLevel: data . accessLevel ,
updatedAt: data . updatedAt
});
// If KYC approved, enable features
if ( data . status === 'ACTIVE' && data . accessLevel !== 'L0' ) {
await enableUserFeatures ( data . id , data . accessLevel );
}
}
};
const handleAccountEvent = async ( action , data ) => {
if ( action === 'UPDATE' && data . status === 'ACTIVE' ) {
// Account verified
await db . accounts . update ( data . id , {
status: data . status ,
verified: true
});
// Notify user their account is ready
await sendNotification ( data . userId , 'Account Verified' );
}
};
Idempotency
Handle duplicate webhook deliveries using the event ID:
const handleWebhook = async ( webhookEvent ) => {
const { id , event , action , data , attempts } = webhookEvent ;
// Check if already processed (using unique event ID)
const existing = await db . webhookEvents . findOne ({ eventId: id });
if ( existing ) {
console . log ( `Duplicate event ${ id } , skipping` );
return { received: true , duplicate: true };
}
// Store event immediately to prevent duplicate processing
await db . webhookEvents . create ({
eventId: id ,
eventType: event ,
action: action ,
payload: data ,
attempts: attempts ,
receivedAt: new Date (),
processed: false
});
try {
// Process event
await processWebhookEvent ( webhookEvent );
// Mark as processed
await db . webhookEvents . update (
{ eventId: id },
{ processed: true , processedAt: new Date () }
);
return { received: true };
} catch ( error ) {
// Log error but still mark as received
await db . webhookEvents . update (
{ eventId: id },
{ error: error . message , failedAt: new Date () }
);
throw error ;
}
};
Best Practices
app . post ( '/webhooks/killb' , async ( req , res ) => {
// Verify signature
verifySignature ( req );
// Acknowledge immediately
res . status ( 200 ). json ({ received: true });
// Process asynchronously
processAsync ( req . body );
});
Return 200 OK immediately
Process events in background
Use job queues (Bull, Celery, etc.)
Don’t wait for external services
Handle Failures Gracefully
const processWebhook = async ( event ) => {
try {
await processEvent ( event );
} catch ( error ) {
console . error ( 'Webhook processing failed:' , error );
// Log to monitoring service
await logError ( error , event );
// Don't throw - we already processed
// KillB will retry anyway
}
};
Catch all errors
Log failures
Don’t throw after acknowledging
Monitor error rates
await db . webhookLogs . create ({
eventId: event . id ,
eventType: event . event ,
payload: event . data ,
receivedAt: new Date (),
processed: true
});
Keep audit trail
Enable replay if needed
Debug issues easily
Meet compliance requirements
Track delivery success rate
Alert on multiple failures
Monitor processing times
Set up uptime monitoring
Check signature verification failures
Webhook Security Checklist
Use HTTPS Only
Never use HTTP for webhook endpoints ✅ https://api.yourapp.com/webhooks
❌ http://api.yourapp.com/webhooks
Verify Every Request
Check x-signature-sha256 on every webhook
Use Strong Secrets
Minimum 32 characters, random, stored securely const secret = crypto . randomBytes ( 32 ). toString ( 'hex' );
Implement Rate Limiting
Protect against webhook flooding
Log Suspicious Activity
Track invalid signatures and unusual patterns
Troubleshooting
Check:
Webhook URL is publicly accessible
SSL certificate is valid
Firewall allows KillB IPs
Endpoint returns 200 OK quickly
No reverse proxy issues
Test: curl -X POST https://your-webhook-url \
-H "Content-Type: application/json" \
-d '{"test": "data"}'
Signature Verification Fails
Common Issues:
Using wrong secret
Parsing body before verification
Charset/encoding issues
Whitespace modifications
Solution: // Use raw body
app . use ( '/webhooks' , express . raw ({ type: 'application/json' }));
// Verify before parsing
verifySignature ( req . body . toString (), signature );
// Then parse
const event = JSON . parse ( req . body );
This is normal! Webhooks may be delivered multiple times.Handle with:
Event ID tracking
Database unique constraints
Idempotency keys
“Processed” flags
Advanced Configuration
Filtering Events
Subscribe only to events you need:
{
"url" : "https://api.yourapp.com/webhooks/killb" ,
"secret" : "your-secret" ,
"events" : [ "RAMP" ]
}
Multiple Webhooks
Create different endpoints for different event types:
// Ramp events → payment processor
await createWebhook ({
url: 'https://payments.yourapp.com/webhooks' ,
events: [ 'RAMP' ]
});
// User events → CRM
await createWebhook ({
url: 'https://crm.yourapp.com/webhooks' ,
events: [ 'USER' ]
});
You can only have one active webhook configuration per customer. Use a single endpoint and route internally.
Webhook Monitoring
// Track webhook health
const webhookMetrics = {
received: 0 ,
processed: 0 ,
failed: 0 ,
averageProcessingTime: 0
};
app . post ( '/webhooks/killb' , async ( req , res ) => {
const startTime = Date . now ();
webhookMetrics . received ++ ;
try {
await processWebhook ( req . body );
webhookMetrics . processed ++ ;
} catch ( error ) {
webhookMetrics . failed ++ ;
throw error ;
} finally {
const duration = Date . now () - startTime ;
webhookMetrics . averageProcessingTime =
( webhookMetrics . averageProcessingTime + duration ) / 2 ;
}
res . status ( 200 ). json ({ received: true });
});
Next Steps