Skip to main content

What are Webhooks?

Webhooks are HTTP callbacks that FossaPay sends to your server when events occur in your account. They enable you to receive real-time notifications about payments, payouts, settlements, and other important events.
Webhooks eliminate the need for polling and ensure your application stays in sync with FossaPay in real-time.

Why Use Webhooks?

Real-time Updates

Get notified immediately when events occur

Reduce Polling

No need to constantly check for updates

Automated Workflows

Trigger actions automatically based on events

Better UX

Update your users instantly about transaction status

Webhook Events

Deposit Events

EventDescriptionWhen it fires
deposit.completedDeposit completed successfullyWhen a deposit is successfully processed

Withdrawal Events

EventDescriptionWhen it fires
withdrawal.completedWithdrawal completed successfullyWhen a withdrawal is successfully processed

Transfer Events

EventDescriptionWhen it fires
transfer.completedTransfer completed successfullyWhen a transfer is successfully processed
transfer.failedTransfer failedWhen a transfer fails to process

Wallet Events

EventDescriptionWhen it fires
wallet.balance.updatedWallet balance updatedWhen a wallet balance is updated

Setting Up Webhooks

Configure Webhook URL

Set your webhook endpoint in the dashboard or via API: Via Dashboard:
  1. Go to Settings → Webhooks
  2. Enter your webhook URL
  3. Select events to subscribe to
  4. Save configuration
Via API:
await fossapay.webhooks.configure({
  url: 'https://your-app.com/webhooks/fossapay',
  events: [
    'deposit.completed',
    'withdrawal.completed',
    'transfer.completed',
    'transfer.failed',
    'wallet.balance.updated'
  ],
  secret: 'your-webhook-secret' // Optional: for signature verification
});

Webhook URL Requirements

Your webhook endpoint must:
  • Use HTTPS (HTTP not allowed in production)
  • Respond within 30 seconds
  • Return a 2xx status code for successful receipt
  • Handle duplicate events gracefully
Webhook URLs must be publicly accessible. Use services like ngrok for local development.

Webhook Payload

All webhooks follow this structure:
{
  "event": "deposit.completed",
  "event_id": "evt_abc123xyz",
  "timestamp": "2024-01-15T10:30:45Z",
  "data": {
    "transaction_id": "txn_001",
    "customer_id": "cus_123",
    "sender": {
      "address": "0x123...",
      "chain": "Ethereum"
    },
    "recipient": {
      "address": "0x456...",
      "chain": "Ethereum"
    },
    "amount": 1000000,
    "currency": "USDT",
    "reference": "dep-20240115-001",
    "status": "completed",
    "transaction_type": "deposit",
    "blockchain": "Ethereum",
    "transaction_hash": "0x789...",
    "explorer_link": "https://etherscan.io/tx/0x789...",
    "timestamp": "2024-01-15T10:30:40Z"
  },
  "business_id": "bus_your_business"
}

Implementing Webhook Handler

Basic Handler (Node.js/Express)

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

app.post('/webhooks/fossapay', async (req, res) => {
  // 1. Verify webhook signature
  const signature = req.headers['x-fossapay-signature'];
  const secret = process.env.FOSSAPAY_WEBHOOK_SECRET;
  if (!verifySignature(req.body, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  // 2. Respond quickly (within 30 seconds)
  res.status(200).send('Webhook received');

  // 3. Process webhook asynchronously
  processWebhook(req.body).catch(err => {
    console.error('Webhook processing failed:', err);
  });
});

async function processWebhook(payload) {
  const { event, data } = payload;

  switch (event) {
    case 'deposit.completed':
      await handleDepositCompleted(data);
      break;

    case 'withdrawal.completed':
      await handleWithdrawalCompleted(data);
      break;

    case 'transfer.completed':
      await handleTransferCompleted(data);
      break;

    case 'transfer.failed':
      await handleTransferFailed(data);
      break;

    case 'wallet.balance.updated':
      await handleWalletBalanceUpdated(data);
      break;

    default:
      console.log('Unhandled event:', event);
  }
}

async function handleDepositCompleted(data) {
  // Credit customer wallet
  await db.transactions.create({
    id: data.transaction_id,
    customer_id: data.customer_id,
    amount: data.amount,
    type: 'credit',
    status: 'completed'
  });

  await db.wallets.increment({
    customer_id: data.customer_id,
    amount: data.amount
  });

  // Notify customer
  await sendNotification(data.customer_id, {
    title: 'Deposit Received',
    message: `Your wallet has been credited with ${data.amount} ${data.currency}`
  });
}

function verifySignature(payload, signature, secret) {
  const hash = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(hash, 'hex')
  );
}

app.listen(3000);

Python/Flask Example

from flask import Flask, request, jsonify
import hashlib
import hmac
import json

app = Flask(__name__)

@app.route('/webhooks/fossapay', methods=['POST'])
def handle_webhook():
    # Verify signature
    signature = request.headers.get('X-FossaPay-Signature')
    secret = os.environ['FOSSAPAY_WEBHOOK_SECRET']
    if not verify_signature(request.data, signature, secret):
        return 'Invalid signature', 401

    # Respond quickly
    payload = request.json

    # Process asynchronously
    process_webhook.delay(payload)  # Using Celery

    return 'Webhook received', 200

def verify_signature(payload, signature, secret):
    hash = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(hash, signature)

def process_webhook(payload):
    event = payload['event']
    data = payload['data']

    if event == 'deposit.completed':
        handle_deposit_completed(data)
    elif event == 'withdrawal.completed':
        handle_withdrawal_completed(data)
    elif event == 'transfer.completed':
        handle_transfer_completed(data)
    elif event == 'transfer.failed':
        handle_transfer_failed(data)
    elif event == 'wallet.balance.updated':
        handle_wallet_balance_updated(data)

Security

Signature Verification

FossaPay signs all webhooks with HMAC SHA256:
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const hash = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(hash),
    Buffer.from(signature)
  );
}

IP Whitelisting

Optionally restrict webhooks to FossaPay IPs:
52.49.173.169
54.171.127.99
52.214.14.220
Use IP whitelisting as an additional layer of security, not as a replacement for signature verification.

Retry Logic

If your endpoint doesn’t respond with a 2xx status, FossaPay retries:
AttemptDelay
1st retry5 minutes
2nd retry30 minutes
3rd retry2 hours
4th retry6 hours
5th retry24 hours
After 5 failed attempts, the webhook is marked as failed and no more retries occur.
Check your webhook logs in the dashboard for failed deliveries and investigate issues promptly.

Idempotency

Webhooks may be delivered more than once. Implement idempotent handling:
async function handleDepositCompleted(data) {
  const existingTransaction = await db.transactions.findOne({
    id: data.transaction_id
  });

  if (existingTransaction) {
    console.log('Transaction already processed');
    return; // Skip duplicate
  }

  // Process deposit
  await db.transactions.create({
    id: data.transaction_id,
    // ... other fields
  });
}

Testing Webhooks

Local Testing with ngrok

# Start ngrok
ngrok http 3000

# Use the HTTPS URL in FossaPay dashboard
# Example: https://abc123.ngrok.io/webhooks/fossapay

Simulate Webhooks

Test your webhook handler with simulated events:
curl -X POST https://api-staging.fossapay.com/api/v1/webhooks/simulate \
  -H "Authorization: Bearer fp_test_sk_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "deposit.completed",
    "customer_id": "cus_123",
    "amount": 1000000,
    "currency": "USDT"
  }'

Webhook Logs

View webhook delivery logs in your dashboard:
  1. Go to Settings → Webhooks
  2. Click on “Webhook Logs”
  3. Filter by status, event type, or date
  4. Replay failed webhooks

Best Practices

Always return 200 immediately, then process asynchronously:
app.post('/webhooks', (req, res) => {
  res.status(200).send('OK');
  processAsync(req.body); // Don't await
});
Never trust webhooks without signature verification:
if (!verifySignature(payload, signature, secret)) {
  return res.status(401).send('Unauthorized');
}
Use event_id or transaction_id to detect duplicates:
const processed = await redis.get(`webhook:${event_id}`);
if (processed) return;

await redis.set(`webhook:${event_id}`, '1', 'EX', 86400);
Log all webhook receipts for debugging and auditing:
logger.info('Webhook received', {
  event_id: payload.event_id,
  event: payload.event,
  timestamp: payload.timestamp
});
Set up alerts for webhook failures:
if (failureCount > threshold) {
  await sendAlert('Webhook failures detected');
}

Webhook Payload Examples

Deposit Completed (Crypto)

{
  "event": "deposit.completed",
  "event_id": "evt_abc123",
  "timestamp": "2024-01-15T10:30:45Z",
  "data": {
    "transaction_id": "txn_001",
    "customer_id": "cus_123",
    "sender": {
      "address": "0x123...",
      "chain": "Ethereum"
    },
    "recipient": {
      "address": "0x456...",
      "chain": "Ethereum"
    },
    "amount": 1000000,
    "currency": "USDT",
    "reference": "dep-20240115-001",
    "status": "completed",
    "transaction_type": "deposit",
    "blockchain": "Ethereum",
    "transaction_hash": "0x789...",
    "explorer_link": "https://etherscan.io/tx/0x789...",
    "timestamp": "2024-01-15T10:30:40Z"
  }
}

Deposit Completed (Fiat)

{
  "event": "deposit.completed",
  "event_id": "evt_def456",
  "timestamp": "2024-01-15T11:00:00Z",
  "data": {
    "transaction_id": "txn_002",
    "customer_id": "cus_456",
    "sender": {
      "account_name": "John Doe",
      "account_number": "0987654321",
      "bank_name": "GTBank"
    },
    "recipient": {
      "account_name": "Jane Smith",
      "account_number": "0123456789",
      "bank_name": "GTBank"
    },
    "amount": 50000,
    "currency": "NGN",
    "reference": "dep-20240115-002",
    "status": "completed",
    "transaction_type": "deposit",
    "notes": "Bank transfer deposit",
    "timestamp": "2024-01-15T11:00:00Z"
  }
}

Withdrawal Completed

{
  "event": "withdrawal.completed",
  "event_id": "evt_xyz789",
  "timestamp": "2024-01-15T11:45:23Z",
  "data": {
    "transaction_id": "txn_003",
    "customer_id": "cus_789",
    "recipient": {
      "account_name": "Jane Smith",
      "account_number": "0123456789",
      "bank_name": "GTBank"
    },
    "amount": 25000,
    "currency": "NGN",
    "reference": "wth-20240115-001",
    "status": "completed",
    "transaction_type": "withdrawal",
    "timestamp": "2024-01-15T11:45:20Z"
  }
}

Transfer Completed

{
  "event": "transfer.completed",
  "event_id": "evt_transfer1",
  "timestamp": "2024-01-15T12:30:00Z",
  "data": {
    "transaction_id": "txn_004",
    "customer_id": "cus_123",
    "sender": {
      "address": "0x123...",
      "chain": "Ethereum"
    },
    "recipient": {
      "address": "0x456...",
      "chain": "Ethereum"
    },
    "amount": 500000,
    "currency": "USDT",
    "reference": "trf-20240115-001",
    "status": "completed",
    "transaction_type": "transfer",
    "blockchain": "Ethereum",
    "transaction_hash": "0xabc...",
    "timestamp": "2024-01-15T12:30:00Z"
  }
}

Transfer Failed

{
  "event": "transfer.failed",
  "event_id": "evt_transfer2",
  "timestamp": "2024-01-15T13:15:00Z",
  "data": {
    "transaction_id": "txn_005",
    "customer_id": "cus_456",
    "sender": {
      "address": "0x789...",
      "chain": "Ethereum"
    },
    "recipient": {
      "address": "0xabc...",
      "chain": "Ethereum"
    },
    "amount": 300000,
    "currency": "USDT",
    "reference": "trf-20240115-002",
    "status": "failed",
    "transaction_type": "transfer",
    "blockchain": "Ethereum",
    "transaction_hash": "0xdef...",
    "timestamp": "2024-01-15T13:15:00Z"
  }
}

Wallet Balance Updated

{
  "event": "wallet.balance.updated",
  "event_id": "evt_balance1",
  "timestamp": "2024-01-15T14:00:00Z",
  "data": {
    "from_account": {
      "id": "acc_001",
      "account_number": "0123456789",
      "account_name": "Main Wallet",
      "bank_name": "GTBank",
      "customer_id": "cus_123"
    },
    "to_account": {
      "id": "acc_002",
      "account_number": "0987654321",
      "account_name": "Savings Wallet",
      "bank_name": "GTBank",
      "customer_id": "cus_123"
    },
    "amount": 25000,
    "currency": "NGN",
    "reference": "bal-20240115-001",
    "narration": "Transfer between accounts",
    "timestamp": "2024-01-15T14:00:00Z"
  }
}

Next Steps

Configure Webhooks

Set up your webhook endpoint

View Webhook Logs

Monitor webhook deliveries

Collections Guide

Handle payment webhooks

Payouts Guide

Handle payout webhooks