> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zerion.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up wallet activity alerts

> Use Zerion API transaction subscriptions and webhooks to get real-time notifications when a watched wallet sends or receives tokens across supported chains.

**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](https://dashboard.zerion.io))
* A publicly accessible callback URL to receive webhook notifications (you can use [webhook.site](https://webhook.site) for testing)

<Note>
  For production use, contact [api@zerion.io](mailto:api@zerion.io) to whitelist your callback URL. See [Webhooks](/webhooks#subscription-limits) for plan limits and delivery guarantees.
</Note>

## Steps

<Steps>
  <Step title="Create a subscription">
    Subscribe to transactions for one or more wallet addresses using the [create subscription](/api-reference/subscriptions-to-transactions/create-subscription) endpoint.

    <CodeGroup>
      ```javascript JavaScript theme={null}
      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 Python theme={null}
      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"])
      ```

      ```bash cURL theme={null}
      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"]
        }'
      ```
    </CodeGroup>

    * `chain_ids` is optional. Omit it to subscribe to all supported chains
    * Save the subscription `id` from the response for management later
  </Step>

  <Step title="Handle incoming webhooks">
    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.

    ```javascript Express.js theme={null}
    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);
    ```

    <Warning>
      Transaction prices in webhook notifications are always `null`. To get USD values, fetch the full transaction using the [wallet transactions](/api-reference/wallets/get-wallet-transactions) endpoint after receiving the notification:

      ```bash theme={null}
      curl -g -u "YOUR_API_KEY:" \
        "https://api.zerion.io/v1/wallets/0xADDRESS/transactions/?currency=usd&filter[hash]=TX_HASH"
      ```
    </Warning>
  </Step>

  <Step title="Verify webhook signatures">
    The Express handler above already includes full signature verification. The key points:

    * 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

    <Warning>
      Always validate the `x-certificate-url` domain before fetching. Ensure it points to a trusted Zerion domain to prevent SSRF attacks.
    </Warning>
  </Step>

  <Step title="Manage your subscription">
    List, update, or delete subscriptions as needed: [list subscriptions](/api-reference/subscriptions-to-transactions/find-subscriptions), [update wallets](/api-reference/subscriptions-to-transactions/patch-wallets-within-subscription), or [delete a subscription](/api-reference/subscriptions-to-transactions/delete-subscription-by-id).

    ```bash theme={null}
    # 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"
    ```
  </Step>
</Steps>

## Important notes

See [Delivery guarantees](/webhooks#delivery-guarantees) and [Subscription limits](/webhooks#subscription-limits) on the Webhooks reference page.

## Next steps

* Combine with the [transaction history recipe](/recipes/transaction-history) 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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.