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

# Webhooks Quickstart

> Provision a signing secret, stand up a verifying receiver, subscribe a watched address, and receive your first event end-to-end.

## Prerequisites

* A WalletSuite API key, passed as the `x-api-key` header on every management request (same scheme as the rest of the [API](/getting-started/api-reference/overview)).
* A publicly reachable HTTPS URL for delivery. Private, internal, and link-local addresses are rejected with `WEBHOOK_URL_NOT_ALLOWED`.

```bash theme={null}
export WALLETSUITE_API_KEY="your-api-key"
```

## Steps

<Steps>
  <Step title="Provision your signing secret">
    Every delivery is signed with a per-account secret using the [Standard Webhooks](https://www.standardwebhooks.com) scheme. Provision it once:

    ```bash Provision signing secret theme={null}
    curl -X POST "https://api.walletsuite.io/api/notifications/signing-secret" \
      -H "x-api-key: $WALLETSUITE_API_KEY"
    ```

    The response returns the secret inside the standard `{ok, code, message, data}` envelope:

    ```json Response theme={null}
    {
      "ok": true,
      "code": "OK",
      "message": "ok",
      "data": {
        "secret": "whsec_MfKQ9r8g...base64...=="
      }
    }
    ```

    <Warning>
      The secret is shown **once** and never returned again. Copy it now and store it in your secrets manager. If you lose it, you must [rotate](/webhooks/verify#secret-rotation) it - there is no read-back endpoint.
    </Warning>

    <Note>
      Calling this endpoint again when a secret already exists returns `409 SIGNING_SECRET_EXISTS`. To replace a lost or compromised secret, use `POST /api/notifications/signing-secret/rotate` instead.
    </Note>

    ```bash theme={null}
    export WALLETSUITE_WEBHOOK_SECRET="whsec_MfKQ9r8gMfKQ9r8g..."
    ```
  </Step>

  <Step title="Stand up a verifying receiver">
    Your endpoint receives a `POST`, verifies the signature, and [acknowledges with a `2xx`](/webhooks/delivery#acknowledgement). With the official [`standardwebhooks`](https://www.standardwebhooks.com) library that is a few lines of code: pass it the raw request body and the `Webhook-*` headers, and it returns the verified, parsed event.

    <Tabs>
      <Tab title="Node / Express">
        ```bash Install theme={null}
        npm install express standardwebhooks
        ```

        ```js server.js theme={null}
        const express = require("express");
        const { Webhook } = require("standardwebhooks");

        const app = express();
        const wh = new Webhook(process.env.WALLETSUITE_WEBHOOK_SECRET);

        app.post(
          "/webhooks/walletsuite",
          express.raw({ type: "application/json" }),
          (req, res) => {
            let event;
            try {
              event = wh.verify(req.body, {
                "webhook-id": req.header("webhook-id"),
                "webhook-timestamp": req.header("webhook-timestamp"),
                "webhook-signature": req.header("webhook-signature"),
              });
            } catch (err) {
              return res.status(400).send("invalid signature");
            }

            res.status(204).end();

            handleEvent(event).catch((e) =>
              console.error("processing failed", event.event_id, e)
            );
          }
        );

        async function handleEvent(event) {
          console.log(event.event_type, event.event_id, event.data);
        }

        app.listen(3000, () => console.log("listening on :3000"));
        ```

        ```bash Run theme={null}
        WALLETSUITE_WEBHOOK_SECRET="$WALLETSUITE_WEBHOOK_SECRET" node server.js
        ```
      </Tab>

      <Tab title="Python / FastAPI">
        ```bash Install theme={null}
        pip install fastapi "uvicorn[standard]" standardwebhooks
        ```

        ```python main.py theme={null}
        import os
        import json
        import logging

        from fastapi import FastAPI, Request, Response
        from standardwebhooks import Webhook

        logging.basicConfig(level=logging.INFO)
        app = FastAPI()
        wh = Webhook(os.environ["WALLETSUITE_WEBHOOK_SECRET"])


        @app.post("/webhooks/walletsuite")
        async def receive(request: Request):
            body = await request.body()
            headers = {
                "webhook-id": request.headers.get("webhook-id", ""),
                "webhook-timestamp": request.headers.get("webhook-timestamp", ""),
                "webhook-signature": request.headers.get("webhook-signature", ""),
            }

            try:
                wh.verify(body, headers)
            except Exception:
                return Response(status_code=400, content="invalid signature")

            event = json.loads(body)
            logging.info("%s %s %s", event["event_type"], event["event_id"], event["data"])

            return Response(status_code=204)
        ```

        ```bash Run theme={null}
        WALLETSUITE_WEBHOOK_SECRET="$WALLETSUITE_WEBHOOK_SECRET" \
          uvicorn main:app --port 3000
        ```
      </Tab>

      <Tab title="Next.js (App Router)">
        ```bash Install theme={null}
        npm install standardwebhooks
        ```

        ```ts app/api/webhooks/walletsuite/route.ts theme={null}
        import { Webhook } from "standardwebhooks";

        const wh = new Webhook(process.env.WALLETSUITE_WEBHOOK_SECRET!);

        export async function POST(req: Request) {
          const body = await req.text();

          let event: any;
          try {
            event = wh.verify(body, {
              "webhook-id": req.headers.get("webhook-id") ?? "",
              "webhook-timestamp": req.headers.get("webhook-timestamp") ?? "",
              "webhook-signature": req.headers.get("webhook-signature") ?? "",
            });
          } catch {
            return new Response("invalid signature", { status: 400 });
          }

          queueMicrotask(() => handleEvent(event));

          return new Response(null, { status: 204 });
        }

        async function handleEvent(event: any) {
          console.log(event.event_type, event.event_id, event.data);
        }
        ```

        ```bash Run theme={null}
        WALLETSUITE_WEBHOOK_SECRET="$WALLETSUITE_WEBHOOK_SECRET" npm run dev
        ```
      </Tab>
    </Tabs>

    <Tip>
      Want the full verification contract - constant-time comparison, replay defense, and a from-scratch reference implementation - see [Verify Signatures](/webhooks/verify).
    </Tip>
  </Step>

  <Step title="Create the subscription">
    This quickstart subscribes to incoming transfers (`transfer.received`) as a worked example - the [event catalog](/webhooks/events#event-catalog) lists every event type and its payload. Use an address you control so you can trigger one in the next step. Set `chain`, the `address`, both asset-scope filters in `eventKinds`, and your public `webhookUrl`:

    ```bash Create subscription theme={null}
    curl -X POST "https://api.walletsuite.io/api/notifications/subscriptions" \
      -H "x-api-key: $WALLETSUITE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "chain": "ethereum",
        "address": "0xYourWatchedAddressHere",
        "eventKinds": ["INCOMING_NATIVE", "INCOMING_FUNGIBLE"],
        "webhookUrl": "https://yourapp.com/webhooks/walletsuite"
      }'
    ```

    The API responds `202 Accepted` with the subscription. It registers and flips from `pending` to `active` within moments:

    ```json Response theme={null}
    {
      "ok": true,
      "code": "OK",
      "message": "accepted",
      "data": {
        "id": "9b7c1e2a-4f6d-4c3b-8a1e-2d5f7a9c0b13",
        "chain": "ethereum",
        "address": "0xYourWatchedAddressHere",
        "eventKinds": ["INCOMING_NATIVE", "INCOMING_FUNGIBLE"],
        "webhookUrl": "https://yourapp.com/webhooks/walletsuite",
        "status": "pending",
        "createdAt": "2026-05-26T14:22:48.123456Z"
      }
    }
    ```

    Confirm it is `active`:

    ```bash Check status theme={null}
    curl "https://api.walletsuite.io/api/notifications/subscriptions/9b7c1e2a-4f6d-4c3b-8a1e-2d5f7a9c0b13" \
      -H "x-api-key: $WALLETSUITE_API_KEY"
    ```

    If the subscription does not go `active`, see [Troubleshooting](/webhooks/troubleshooting).
  </Step>

  <Step title="Trigger and observe">
    Trigger a real event by sending a small transfer to the watched address on the same chain. On the **first confirmation** of an incoming transfer, WalletSuite delivers a `transfer.received` event to your endpoint.

    The delivered request looks like this:

    ```http Delivered request theme={null}
    POST /webhooks/walletsuite HTTP/1.1
    Host: yourapp.com
    Content-Type: application/json; charset=utf-8
    User-Agent: WalletSuite-Webhooks/1.0
    Webhook-Id: 3f0a8d52-7e14-4b9c-a6d2-c8e1f4b09a7d
    Webhook-Timestamp: 1779805371
    Webhook-Signature: v1,g0hM9SsE+OTbJCwQ8...base64...=

    {
      "schema_version": "1.0",
      "event_id": "3f0a8d52-7e14-4b9c-a6d2-c8e1f4b09a7d",
      "event_type": "transfer.received",
      "subscription_id": "9b7c1e2a-4f6d-4c3b-8a1e-2d5f7a9c0b13",
      "occurred_at": "2026-05-26T14:22:48.123Z",
      "data": {
        "chain": "ethereum",
        "direction": "incoming",
        "tx_hash": "0x9f3...",
        "block_number": 18573245,
        "from": "0x123...",
        "to": "0xabc...",
        "amount": "1.5",
        "asset_type": "erc20",
        "asset_contract": "0xa0b8..."
      }
    }
    ```
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Verify Signatures" icon="shield-check" href="/webhooks/verify">
    The full Standard Webhooks verification contract - raw-body handling, replay protection, and from-scratch code.
  </Card>

  <Card title="Delivery & Retries" icon="repeat" href="/webhooks/delivery">
    The 18-second ack budget, the retry schedule, at-least-once semantics, and failure handling.
  </Card>

  <Card title="Events & Payloads" icon="brackets-curly" href="/webhooks/events">
    Field-by-field payload reference, `transfer.received`, Tron address encoding, and payload versioning.
  </Card>

  <Card title="Best Practices" icon="list-check" href="/webhooks/best-practices">
    Idempotency, async processing, secret rotation, and production hardening for your receiver.
  </Card>
</CardGroup>
