Skip to main content
What you’ll build:
  • Create a transaction subscription for one or more wallets
  • Handle and parse incoming webhook payloads
  • Verify webhook signatures for security
  • Manage (list, update, delete) your subscriptions
Time: ~15 minutes

Prerequisites

  • A Zerion API key (get one here)
  • A publicly accessible callback URL to receive webhook notifications (you can use webhook.site for testing)
For production use, contact api@zerion.io to whitelist your callback URL. See Webhooks for plan limits and delivery guarantees.

Steps

1
Create a subscription
2
Subscribe to transactions for one or more wallet addresses using the create subscription endpoint.
3
JavaScript
const API_KEY = process.env.ZERION_API_KEY;

const response = await fetch("https://api.zerion.io/v1/tx-subscriptions", {
  method: "POST",
  headers: {
    authorization: `Basic ${btoa(API_KEY + ":")}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    callback_url: "https://webhook.site/your-unique-id",
    addresses: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
    chain_ids: ["ethereum", "optimism", "base"],
  }),
});

const { data } = await response.json();
console.log("Subscription ID:", data.id);
Python
import os, requests

api_key = os.environ["ZERION_API_KEY"]

response = requests.post(
    "https://api.zerion.io/v1/tx-subscriptions",
    json={
        "callback_url": "https://webhook.site/your-unique-id",
        "addresses": ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
        "chain_ids": ["ethereum", "optimism", "base"],
    },
    auth=(api_key, ""),
)

data = response.json()["data"]
print("Subscription ID:", data["id"])
cURL
curl -u "YOUR_API_KEY:" \
  -X POST "https://api.zerion.io/v1/tx-subscriptions" \
  -H "Content-Type: application/json" \
  -d '{
    "callback_url": "https://webhook.site/your-unique-id",
    "addresses": [
      "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
    ],
    "chain_ids": ["ethereum", "optimism", "base"]
  }'
4
  • chain_ids is optional — omit it to subscribe to all supported chains
  • Save the subscription id from the response for management later
  • 5
    Handle incoming webhooks
    6
    When a transaction occurs, Zerion sends a POST request to your callback URL. Here’s what the payload looks like and how to process it.
    7
    const express = require("express");
    const crypto = require("crypto");
    const app = express();
    
    // Capture the exact raw body so signature verification uses the original bytes.
    app.use(
      express.json({
        verify: (req, _res, buf) => {
          req.rawBody = buf.toString("utf8");
        },
      })
    );
    
    // Cache fetched certificates to avoid re-downloading on every request
    const certCache = new Map();
    
    async function fetchCertificate(certUrl) {
      if (certCache.has(certUrl)) return certCache.get(certUrl);
    
      // Validate the certificate URL domain to prevent SSRF attacks
      const url = new URL(certUrl);
      if (!url.hostname.endsWith(".zerion.io")) {
        throw new Error(`Untrusted certificate domain: ${url.hostname}`);
      }
    
      const res = await fetch(certUrl);
      if (!res.ok) throw new Error(`Failed to fetch certificate: ${res.status}`);
      const pem = await res.text();
      certCache.set(certUrl, pem);
      return pem;
    }
    
    async function verifyWebhook(req) {
      const timestamp = req.headers["x-timestamp"];
      const signature = req.headers["x-signature"];
      const certUrl = req.headers["x-certificate-url"];
    
      const pem = await fetchCertificate(certUrl);
      const message = `${timestamp}\n${req.rawBody}\n`;
    
      const verifier = crypto.createVerify("SHA256");
      verifier.update(message);
      return verifier.verify(pem, signature, "base64");
    }
    
    app.post("/webhook", async (req, res) => {
      try {
        if (!(await verifyWebhook(req))) {
          return res.sendStatus(403);
        }
    
        const { data, included } = req.body;
    
        // data contains the notification metadata
        console.log("Notification ID:", data.id);
        console.log("Wallet:", data.attributes.address);
        console.log("Timestamp:", data.attributes.timestamp);
    
        // included contains the full transaction object(s)
        for (const tx of included) {
          const { operation_type, transfers, fee, mined_at } = tx.attributes;
          const chain = tx.relationships.chain.data.id;
    
          console.log(`${operation_type} on ${chain} at ${mined_at}`);
          for (const transfer of transfers) {
            const symbol = transfer.fungible_info?.symbol || "NFT";
            const direction = transfer.direction === "out" ? "Sent" : "Received";
            console.log(`  ${direction} ${transfer.quantity.float} ${symbol}`);
          }
        }
    
        res.sendStatus(200);
      } catch (error) {
        console.error("Webhook processing error:", error);
        res.sendStatus(400);
      }
    });
    
    app.listen(3000);
    
    8
    Transaction prices in webhook notifications are always null. To get USD values, fetch the full transaction using the wallet transactions endpoint after receiving the notification:
    curl -u "YOUR_API_KEY:" \
      "https://api.zerion.io/v1/wallets/0xADDRESS/transactions/?currency=usd&filter[hash]=TX_HASH"
    
    9
    Verify webhook signatures
    10
    The Express handler above already includes full signature verification. The key points:
    11
  • Each webhook includes three headers: x-timestamp, x-signature, and x-certificate-url
  • Build the signed message as `${timestamp}\n${rawBody}\n` — use the raw request body, not JSON.stringify(req.body)
  • Verify using crypto.createVerify("SHA256") with the certificate’s public key
  • Cache fetched certificates to avoid re-downloading on every request
  • 12
    Always validate the x-certificate-url domain before fetching — ensure it points to a trusted Zerion domain to prevent SSRF attacks.
    13
    Manage your subscription
    14
    List, update, or delete subscriptions as needed: list subscriptions, update wallets, or delete a subscription.
    15
    # List all subscriptions
    curl -u "YOUR_API_KEY:" \
      "https://api.zerion.io/v1/tx-subscriptions"
    
    # Add or remove wallets from a subscription
    curl -u "YOUR_API_KEY:" \
      -X PATCH "https://api.zerion.io/v1/tx-subscriptions/SUBSCRIPTION_ID/wallets" \
      -H "Content-Type: application/json" \
      -d '{
        "addresses": ["0xNEW_ADDRESS_TO_ADD"]
      }'
    
    # Delete a subscription
    curl -u "YOUR_API_KEY:" \
      -X DELETE "https://api.zerion.io/v1/tx-subscriptions/SUBSCRIPTION_ID"
    

    Important notes

    See Delivery guarantees and Subscription limits on the Webhooks reference page.

    Next steps

    • Combine with the transaction history recipe to fetch full details (including prices) for flagged transactions
    • Use chain_ids to focus on specific chains and reduce noise
    • Build a notification service that forwards alerts to Slack, Discord, email, or push notifications