> ## 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 transaction subscriptions (webhooks) to get notified when a wallet sends or receives tokens.

**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>
  ### 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

  ### 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 -u "YOUR_API_KEY:" \
      "https://api.zerion.io/v1/wallets/0xADDRESS/transactions/?currency=usd&filter[hash]=TX_HASH"
    ```
  </Warning>

  ### 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>

  ### 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"
  ```
</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
