Webhooks
Overview

Webhooks Overview

The Business Webhook System lets business clients receive real-time notifications when important events occur in their accounts. Webhook delivery is tightly integrated with business API keys and uses HMAC signatures for verification.

How Webhooks Work

High-level Flow

  1. Event Occurs

    • A transaction completes, a virtual wallet is created, or KYC verification is completed
    • The system processes the event and updates internal records
  2. Webhook Delivery

    • If the user is a business client with a configured webhook URL on their API key, the system enqueues a webhook job
    • A background service delivers the webhook to your configured URL
  3. Verification

    • Your server receives the webhook with an HMAC signature
    • You verify the signature using your API key's secret
    • Process the event in your system

Webhook Types

Transfaar provides the following webhook types:

  • Transaction Webhook - Notifications for transaction status changes (deposits, withdrawals, exchanges, transfers)
  • Virtual Wallet Creation - Notifications when virtual wallets are created for your customers
  • KYC Completion - Notifications when customer KYC verification is completed

Webhook Security

All webhooks are signed with HMAC-SHA256 using the secret key associated with your business API key.

Headers

Every webhook request includes these headers:

  • Content-Type: application/json
  • User-Agent: Transfaar-Business-API/1.0
  • X-Transfaar-Signature: <hmac_signature>

Verification Process

To verify a webhook:

  1. Read the raw request body - Read the raw request body as bytes (before any JSON parsing or modification). The body must be read exactly as received.
  2. Generate the signature - Compute HMAC-SHA256 over the raw body bytes using your stored secret key (the same secret key associated with your business API key).
  3. Hex-encode the result - Convert the HMAC result to a lowercase hexadecimal string.
  4. Compare signatures - Compare the computed hex-encoded signature with the value in the X-Transfaar-Signature header using a constant-time comparison function to prevent timing attacks.

Important: If the signatures don't match, you must reject the webhook. Never process webhooks with invalid signatures.

Example Verification (Python)

import hmac
import hashlib
 
def verify_webhook_signature(body: bytes, signature: str, secret_key: str) -> bool:
    """
    Verify webhook signature.
    
    Args:
        body: Raw request body as bytes (before JSON parsing)
        signature: Signature from X-Transfaar-Signature header
        secret_key: Your API key's secret key
    
    Returns:
        True if signature is valid, False otherwise
    """
    # Generate HMAC-SHA256 signature
    expected_signature = hmac.new(
        secret_key.encode('utf-8'),
        body,  # Use raw bytes, not encoded string
        hashlib.sha256
    ).hexdigest()  # hexdigest() produces lowercase hex
    
    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected_signature, signature.lower())

Usage example:

from flask import request
 
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    # Read raw body before parsing JSON
    body = request.get_data()
    signature = request.headers.get('X-Transfaar-Signature', '')
    secret_key = 'your_api_key_secret_here'
    
    if not verify_webhook_signature(body, signature, secret_key):
        return {'error': 'Invalid signature'}, 401
    
    # Now safe to parse JSON
    data = request.get_json()
    # Process webhook...
    return {'status': 'ok'}, 200

Example Verification (Node.js)

const crypto = require('crypto');
 
function verifyWebhookSignature(body, signature, secretKey) {
  /**
   * Verify webhook signature.
   * 
   * @param {Buffer|string} body - Raw request body (before JSON parsing)
   * @param {string} signature - Signature from X-Transfaar-Signature header
   * @param {string} secretKey - Your API key's secret key
   * @returns {boolean} True if signature is valid, false otherwise
   */
  const expectedSignature = crypto
    .createHmac('sha256', secretKey)
    .update(body)  // Works with Buffer or string
    .digest('hex');  // Produces lowercase hex
  
  // Normalize both signatures to lowercase for comparison
  const normalizedExpected = expectedSignature.toLowerCase();
  const normalizedReceived = signature.toLowerCase();
  
  // Use timing-safe comparison to prevent timing attacks
  // Both hex strings should be 64 characters (SHA256 hex digest)
  if (normalizedExpected.length !== normalizedReceived.length) {
    return false;
  }
  
  return crypto.timingSafeEqual(
    Buffer.from(normalizedExpected, 'utf8'),
    Buffer.from(normalizedReceived, 'utf8')
  );
}

Usage example (Express.js):

const express = require('express');
const app = express();
 
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const body = req.body; // Raw body as Buffer
  const signature = req.headers['x-transfaar-signature'] || '';
  const secretKey = 'your_api_key_secret_here';
  
  if (!verifyWebhookSignature(body, signature, secretKey)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // Now safe to parse JSON
  const data = JSON.parse(body.toString());
  // Process webhook...
  res.json({ status: 'ok' });
});

Important: Use express.raw() middleware to get the raw body. If you use express.json(), the body will be parsed and you won't be able to verify the signature correctly.

Delivery Semantics and Retries

Internally, webhooks are processed by a background service that:

  • Pushes webhook jobs to a queue for non-blocking delivery
  • Implements retries with exponential backoff (up to 10 attempts, configurable) when the response is not 200 OK
  • Logs all failed deliveries for later inspection, without blocking the main transaction flow
  • Retries are only triggered when the HTTP response status code is not 200 OK (including network errors, 4xx, and 5xx responses)

Best Practices

  1. Respond Quickly: Return a 200 OK response as soon as you've received and validated the webhook. Process the event asynchronously in your system.

  2. Idempotency: Implement idempotency handling in your webhook receiver. The same event may be sent multiple times if retries occur, so use unique identifiers (like transaction_id, wallet_id, or customer_id) to deduplicate.

  3. Error Handling: If you encounter an error processing a webhook, return a non-200 status code to trigger a retry. However, be careful not to retry indefinitely for permanent errors (e.g., invalid data).

  4. Logging: Log all received webhooks for audit and debugging purposes.

Configuration

To receive webhooks, you must:

  1. Create a business API key through the business API key management endpoints
  2. Configure a webhook URL when creating or updating your API key
  3. Ensure your webhook endpoint is publicly accessible and can accept POST requests
  4. Implement webhook signature verification in your endpoint

Testing

For sandbox/testing environments, Transfaar exposes endpoints that can be used to re-send or simulate webhook delivery for a given transaction reference. When you use these endpoints, standard business authentication (HMAC headers) applies, and your webhook URL must already be configured on a business API key.