Webhooks Quickstart

Receive real-time notifications when on-chain events happen. No polling required. Bootnode will POST a signed JSON payload to your endpoint whenever a matching event occurs.

Supported Event Types

Event TypeDescription
address_activityAny transaction involving specified addresses (sends, receives, contract interactions)
token_transferERC-20 token transfers to/from specified addresses or contracts
nft_transferERC-721 and ERC-1155 NFT transfers
mined_transactionSpecific transaction hash has been mined and confirmed
logContract event logs matching specific topics and addresses
1

Create a Webhook Subscription

Register your endpoint and specify which events you want to receive. Bootnode returns a signing_secret that you will use to verify webhook payloads.

curl -X POST https://api.bootnode.dev/v1/webhooks \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "url": "https://myapp.com/api/webhooks/bootnode",
    "chain": "ethereum",
    "event_type": "address_activity",
    "config": {
      "addresses": [
        "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
        "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
      ]
    }
  }'

Response:

{
  "id": "wh_a1b2c3d4e5f6",
  "url": "https://myapp.com/api/webhooks/bootnode",
  "chain": "ethereum",
  "event_type": "address_activity",
  "config": {
    "addresses": [
      "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
      "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
    ]
  },
  "signing_secret": "whsec_k7m9p2q4r6s8t0v2w4x6y8z0",
  "status": "active",
  "created_at": "2026-01-15T10:30:00Z"
}

Important

Save the signing_secret securely. It is only returned once during creation. You will need it to verify incoming webhook signatures.

2

Set Up Your Webhook Endpoint

Create an Express.js server (or any HTTP server) to receive webhook events. Bootnode sends POST requests with a JSON body and an HMAC signature in the headers.

// server.ts
import express from "express";
import crypto from "crypto";

const app = express();
const SIGNING_SECRET = process.env.WEBHOOK_SIGNING_SECRET!;
// "whsec_k7m9p2q4r6s8t0v2w4x6y8z0"

// IMPORTANT: Use raw body for signature verification
app.post(
  "/api/webhooks/bootnode",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.headers["x-bootnode-signature"] as string;
    const timestamp = req.headers["x-bootnode-timestamp"] as string;
    const body = req.body as Buffer;

    // 1. Verify the signature
    if (!verifySignature(body, signature, timestamp)) {
      console.error("Invalid webhook signature");
      return res.status(401).json({ error: "Invalid signature" });
    }

    // 2. Parse the event
    const event = JSON.parse(body.toString());
    console.log("Received event:", event.event_type, event.data);

    // 3. Handle the event
    switch (event.event_type) {
      case "address_activity":
        handleAddressActivity(event.data);
        break;
      case "token_transfer":
        handleTokenTransfer(event.data);
        break;
      case "nft_transfer":
        handleNftTransfer(event.data);
        break;
      default:
        console.log("Unhandled event type:", event.event_type);
    }

    // 4. Return 200 to acknowledge receipt
    res.status(200).json({ received: true });
  }
);

app.listen(3001, () => {
  console.log("Webhook server listening on port 3001");
});
3

Verify the HMAC Signature

Every webhook request includes two headers for verification: X-Bootnode-Signature and X-Bootnode-Timestamp. The signature is a HMAC-SHA256 hex digest of {timestamp}.{body}.

function verifySignature(
  body: Buffer,
  signature: string,
  timestamp: string
): boolean {
  // 1. Reject old timestamps (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  const ts = parseInt(timestamp, 10);
  if (Math.abs(now - ts) > 300) {
    // 5-minute tolerance
    return false;
  }

  // 2. Compute expected signature
  const payload = `${timestamp}.${body.toString()}`;
  const expected = crypto
    .createHmac("sha256", SIGNING_SECRET)
    .update(payload)
    .digest("hex");

  // 3. Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
4

Handle Event Types

Each event type has a specific payload structure. Here are handlers for the most common event types:

interface AddressActivityEvent {
  chain: string;
  block_number: number;
  tx_hash: string;
  from: string;
  to: string;
  value: string;           // wei as decimal string
  asset: string;           // "ETH" or token symbol
  contract: string | null; // null for native transfers
  direction: "incoming" | "outgoing";
}

function handleAddressActivity(data: AddressActivityEvent) {
  console.log(
    `[${data.chain}] ${data.direction} ${data.asset}: ` +
    `${data.from} -> ${data.to} (${data.value} wei)`
  );

  // Example: notify user of incoming transfer
  if (data.direction === "incoming") {
    notifyUser(data.to, `Received ${data.asset} from ${data.from}`);
  }
}

interface TokenTransferEvent {
  chain: string;
  block_number: number;
  tx_hash: string;
  contract: string;
  from: string;
  to: string;
  value: string;
  symbol: string;
  decimals: number;
}

function handleTokenTransfer(data: TokenTransferEvent) {
  const formatted = (
    Number(BigInt(data.value)) / Math.pow(10, data.decimals)
  ).toFixed(data.decimals);
  console.log(
    `[${data.chain}] Token transfer: ${formatted} ${data.symbol}`
  );
}

interface NftTransferEvent {
  chain: string;
  block_number: number;
  tx_hash: string;
  contract: string;
  from: string;
  to: string;
  token_id: string;
  standard: "ERC721" | "ERC1155";
  quantity: number; // always 1 for ERC721
}

function handleNftTransfer(data: NftTransferEvent) {
  console.log(
    `[${data.chain}] NFT transfer: #${data.token_id} ` +
    `(${data.standard}) from ${data.from} to ${data.to}`
  );
}

function notifyUser(address: string, message: string) {
  // Your notification logic (email, push, in-app, etc.)
  console.log(`Notify ${address}: ${message}`);
}

Example Webhook Payload

Here is a full example of what Bootnode sends to your endpoint:

// Headers:
// X-Bootnode-Signature: 5a2f8c9d1e3b4a6f7c8d9e0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
// X-Bootnode-Timestamp: 1706400000
// Content-Type: application/json

// Body:
{
  "id": "evt_z9y8x7w6v5u4",
  "event_type": "address_activity",
  "webhook_id": "wh_a1b2c3d4e5f6",
  "created_at": "2026-01-15T10:35:22Z",
  "data": {
    "chain": "ethereum",
    "block_number": 20574335,
    "tx_hash": "0xabc123def456789...",
    "from": "0x1234567890abcdef1234567890abcdef12345678",
    "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "value": "1000000000000000000",
    "asset": "ETH",
    "contract": null,
    "direction": "incoming"
  }
}

Retry Policy

If your endpoint returns a non-2xx status code or does not respond within 15 seconds, Bootnode retries the delivery with exponential backoff:

AttemptDelay
1st retry10 seconds
2nd retry1 minute
3rd retry10 minutes
4th retry1 hour
5th retry6 hours

After 5 failed retries, the webhook is paused and you receive an email notification. You can resume it from the dashboard or via the PATCH endpoint.

Next Steps