Skip to content

Webhook Integration

Complete guide to integrating Vigil webhooks into your applications.

Overview

Webhooks send HTTP POST requests to your server when events occur, enabling real-time integrations.

For basic webhook setup, see Webhook Notifications.

Integration Architecture

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Vigil     │────▶│ Your Server │────▶│  Your App   │
│   Event     │     │  Endpoint   │     │  Logic      │
└─────────────┘     └─────────────┘     └─────────────┘

Example Implementations

Node.js/Express

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

const app = express();

// Verify HMAC signature
function verifySignature(timestamp, payload, signature, secret) {
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature.replace('sha256=', '')),
    Buffer.from(expectedSig)
  );
}

app.post('/webhook/vigil', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-btcwatch-signature'];
  const timestamp = req.headers['x-btcwatch-timestamp'];
  const secret = process.env.VIGIL_WEBHOOK_SECRET;

  // Verify signature
  if (!verifySignature(timestamp, req.body, signature, secret)) {
    return res.status(401).send('Invalid signature');
  }

  // Check timestamp freshness
  const requestTime = parseInt(timestamp);
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - requestTime) > 300) { // 5 min tolerance
    return res.status(401).send('Request too old');
  }

  // Process event
  const event = JSON.parse(req.body);
  console.log(`Received ${event.event_type} for wallet ${event.wallet_id}`);

  // Handle different event types
  switch(event.event_type) {
    case 'transaction.deposit':
      handleDeposit(event);
      break;
    case 'transaction.withdrawal':
      handleWithdrawal(event);
      break;
    case 'theft_shield.incident_detected':
      handleTheftAlert(event);
      break;
  }

  res.status(200).send({status: 'received'});
});

Python/Flask

import hmac
import hashlib
import time
from flask import Flask, request

app = Flask(__name__)

def verify_signature(timestamp, payload, signature, secret):
    mac = hmac.new(secret.encode(), digestmod=hashlib.sha256)
    mac.update(str(timestamp).encode())
    mac.update(b".")
    mac.update(payload)
    expected_sig = mac.hexdigest()

    received_sig = signature.replace('sha256=', '')
    return hmac.compare_digest(received_sig, expected_sig)

@app.route('/webhook/vigil', methods=['POST'])
def vigil_webhook():
    signature = request.headers.get('X-BtcWatch-Signature')
    timestamp = request.headers.get('X-BtcWatch-Timestamp')
    secret = os.environ['VIGIL_WEBHOOK_SECRET']

    # Verify signature
    if not verify_signature(timestamp, request.data, signature, secret):
        return {'error': 'Invalid signature'}, 401

    # Check timestamp
    request_time = int(timestamp)
    if abs(time.time() - request_time) > 300:
        return {'error': 'Request too old'}, 401

    # Process event
    event = request.json
    print(f"Received {event['event_type']} for wallet {event.get('wallet_id')}")

    # Route to handlers
    if event['event_type'] == 'transaction.deposit':
        handle_deposit(event)
    elif event['event_type'] == 'transaction.withdrawal':
        handle_withdrawal(event)
    elif event['event_type'] == 'theft_shield.incident_detected':
        handle_theft_alert(event)

    return {'status': 'received'}

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strconv"
    "time"
)

func verifySignature(timestamp, payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(timestamp)
    mac.Write([]byte("."))
    mac.Write(payload)
    expectedSig := hex.EncodeToString(mac.Sum(nil))

    receivedSig := signature[7:] // Remove "sha256=" prefix
    return hmac.Equal([]byte(receivedSig), []byte(expectedSig))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-BtcWatch-Signature")
    timestampStr := r.Header.Get("X-BtcWatch-Timestamp")
    secret := os.Getenv("VIGIL_WEBHOOK_SECRET")

    payload, _ := ioutil.ReadAll(r.Body)

    // Verify signature
    if !verifySignature([]byte(timestampStr), payload, signature, secret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // Verify timestamp
    timestamp, _ := strconv.ParseInt(timestampStr, 10, 64)
    if time.Now().Unix()-timestamp > 300 {
        http.Error(w, "Request too old", http.StatusUnauthorized)
        return
    }

    // Parse and handle event
    var event map[string]interface{}
    json.Unmarshal(payload, &event)

    log.Printf("Received %s for wallet %v", event["event_type"], event["wallet_id"])

    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "received"})
}

Use Cases

Accounting Integration

Log all transactions to your accounting system:

function handleDeposit(event) {
  const entry = {
    date: event.timestamp,
    account: 'Bitcoin Receivable',
    amount: (event.event_data.amount_sats || 0) / 100000000,
    description: `Deposit event ${event.event_data.txid || ''}`,
    wallet: event.wallet_id
  };

  accountingDB.insert(entry);
}

Automated Processing

Process payments when confirmed:

def handle_confirmed(event):
    if event['event_type'] == 'payment.confirmed':
        order_id = lookup_order_by_payment(event['event_data'])
        mark_order_paid(order_id)
        send_confirmation_email(order_id)

Alert Forwarding

Forward critical alerts to monitoring:

function handleTheftAlert(event) {
  // Send to PagerDuty
  pagerduty.trigger({
    severity: 'critical',
    summary: `Theft detected on ${event.wallet.name}`,
    details: event
  });

  // Send to Slack
  slack.postMessage({
    channel: '#security-alerts',
    text: `🚨 THEFT SHIELD ALERT: ${event.wallet.name}`
  });
}

Testing

Local Testing with ngrok

# Start your local server
node server.js

# In another terminal, start ngrok
ngrok http 3000

# Use the ngrok HTTPS URL in Vigil webhook settings
# https://abc123.ngrok.io/webhook/vigil

Test Payload

Send a test webhook from Vigil dashboard or use curl:

curl -X POST https://your-server.com/webhook/vigil \
  -H "Content-Type: application/json" \
  -H "X-BtcWatch-Signature: sha256=..." \
  -H "X-BtcWatch-Timestamp: $(date +%s)" \
  -d '{"event_type":"transaction.deposit",...}'

Production Best Practices

  1. Always verify signatures - Prevent spoofing
  2. Check timestamps - Prevent replay attacks
  3. Handle idempotently - Same event may be sent multiple times
  4. Return quickly - Process asynchronously if needed
  5. Log everything - Debugging webhooks is hard
  6. Monitor failures - Set up alerting for webhook errors

Back to: Technical Reference →