Skip to main content

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 transaction eventsEvent 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

Setting Up Webhooks

Create Webhook Configuration

POST /api/v2/webhooks
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
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

GET /api/v2/webhooks
Returns your current webhook configuration.

Update Webhook

PATCH /api/v2/webhooks
Update:
  • URL
  • Secret
  • Events to subscribe
  • Active status
{
  "url": "https://api.yourapp.com/webhooks/killb-v2",
  "active": true,
  "events": ["RAMP", "USER"]
}

Delete Webhook

DELETE /api/v2/webhooks
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
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

1

Use HTTPS Only

Never use HTTP for webhook endpoints
✅ https://api.yourapp.com/webhooks
❌ http://api.yourapp.com/webhooks
2

Verify Every Request

Check x-signature-sha256 on every webhook
3

Use Strong Secrets

Minimum 32 characters, random, stored securely
const secret = crypto.randomBytes(32).toString('hex');
4

Implement Rate Limiting

Protect against webhook flooding
5

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"}'
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