Webhook Cookbook

0watch fires HTTP webhooks when a watched wallet crosses a threshold you set. This guide covers the payload format, how to verify request authenticity, and ready-to-paste integration examples.

Base URL: https://watch.0agent.ai — all requests require X-API-Key: owk_...


Payload format

{
  "event": "high_value_transaction",
  "walletAddress": "0xyourwalletaddress",
  "thresholdEth": 0.5,
  "valueEth": 1.25,
  "transaction": {
    "hash": "0xdeadbeef...",
    "blockNumber": 27480000,
    "timestamp": 1741550000,
    "fromAddress": "0xyourwalletaddress",
    "toAddress": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    "valueWei": "1250000000000000000",
    "txType": "eth_transfer",
    "status": 1,
    "chainId": 8453
  }
}

Key points:


Signature verification

Every 0watch POST includes an X-0Watch-Signature header:

X-0Watch-Signature: sha256=<hex>

The signature is HMAC-SHA256 over the raw request body using the signing secret returned when you created the webhook.

Always verify signatures before processing payloads. This prevents replay attacks from untrusted sources.

Get your signing secret

When you create a webhook, the response includes a one-time signingSecret:

curl -X POST https://watch.0agent.ai/api/webhooks \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -d '{
    "url": "https://your-endpoint.example.com/hook",
    "wallet_address": "0xYourWalletAddress",
    "threshold_eth": 0.5,
    "signing_secret": "your-secret-here"
  }'

Omit signing_secret to have 0watch generate one. Either way, store the returned signingSecret — the list API never returns it again.

To rotate a secret:

curl -X PATCH https://watch.0agent.ai/api/webhooks/1 \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -d '{"rotate_signing_secret": true}'

Verify in Node.js (raw)

import { createHmac, timingSafeEqual } from 'node:crypto';

function verifySignature(rawBody, signingSecret, signatureHeader) {
  if (!signatureHeader) return false;

  const expected = Buffer.from(
    'sha256=' + createHmac('sha256', signingSecret).update(rawBody).digest('hex'),
    'utf8'
  );
  const actual = Buffer.from(signatureHeader, 'utf8');

  if (expected.length !== actual.length) return false;
  return timingSafeEqual(expected, actual);
}

// Express example
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
  const valid = verifySignature(
    req.body,
    process.env.ZEROWATCH_WEBHOOK_SECRET,
    req.headers['x-0watch-signature']
  );
  if (!valid) return res.status(401).send('Invalid signature');

  const payload = JSON.parse(req.body);
  // handle payload
  res.sendStatus(200);
});

Important: Always read the raw body before parsing JSON. Parsing first changes byte representation and breaks the signature check.

Verify using the SDK

import { verifyWebhookSignature } from '@0agent/0watch-sdk';

const rawBody = await request.text();
const valid = verifyWebhookSignature({
  payload: rawBody,
  signingSecret: process.env.ZEROWATCH_WEBHOOK_SECRET!,
  signatureHeader: request.headers.get('x-0watch-signature'),
});

if (!valid) {
  return new Response('Unauthorized', { status: 401 });
}

Verify in Python

import hashlib
import hmac

def verify_signature(raw_body: bytes, signing_secret: str, signature_header: str) -> bool:
    if not signature_header:
        return False
    expected = 'sha256=' + hmac.new(
        signing_secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# Flask example
from flask import Flask, request, abort
import json

app = Flask(__name__)

@app.route('/hook', methods=['POST'])
def handle_webhook():
    raw_body = request.get_data()
    sig = request.headers.get('X-0Watch-Signature', '')
    if not verify_signature(raw_body, os.environ['ZEROWATCH_WEBHOOK_SECRET'], sig):
        abort(401)
    payload = json.loads(raw_body)
    # handle payload
    return '', 200

Slack integration

Post a message to a Slack channel when a high-value transaction fires.

Option A — Slack Incoming Webhook (simple)

Register a Slack Incoming Webhook at https://api.slack.com/apps, then proxy 0watch payloads to it:

// Node.js + Express
app.post('/0watch-to-slack', express.raw({ type: 'application/json' }), async (req, res) => {
  // 1. Verify signature
  const valid = verifySignature(req.body, process.env.ZEROWATCH_WEBHOOK_SECRET, req.headers['x-0watch-signature']);
  if (!valid) return res.status(401).send('Invalid signature');

  const payload = JSON.parse(req.body);
  const { walletAddress, valueEth, thresholdEth, transaction } = payload;

  // 2. Forward to Slack
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `🚨 *0watch alert* — wallet \`${walletAddress}\` moved ${valueEth} ETH (threshold: ${thresholdEth} ETH)`,
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: `*High-value transaction detected*\nWallet: \`${walletAddress}\`\nValue: *${valueEth} ETH*\nTx: <https://basescan.org/tx/${transaction.hash}|${transaction.hash.slice(0, 12)}...>`,
          },
        },
      ],
    }),
  });

  res.sendStatus(200);
});

Option B — Dual thresholds

Route by severity: low-value alerts to Slack for awareness, high-value to PagerDuty for escalation.

# Low threshold → Slack
curl -X POST https://watch.0agent.ai/api/webhooks \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-server.example.com/0watch-to-slack","wallet_address":"0x...","threshold_eth":0.5}'

# High threshold → PagerDuty
curl -X POST https://watch.0agent.ai/api/webhooks \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-server.example.com/0watch-to-pagerduty","wallet_address":"0x...","threshold_eth":10.0}'

Each webhook is evaluated independently. A 15 ETH transaction triggers both.


Discord integration

Discord supports incoming webhooks natively. Post 0watch alerts to a channel:

app.post('/0watch-to-discord', express.raw({ type: 'application/json' }), async (req, res) => {
  const valid = verifySignature(req.body, process.env.ZEROWATCH_WEBHOOK_SECRET, req.headers['x-0watch-signature']);
  if (!valid) return res.status(401).send('Invalid signature');

  const { walletAddress, valueEth, thresholdEth, transaction } = JSON.parse(req.body);

  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      embeds: [
        {
          title: '0watch — High-value transaction',
          color: 0xff4444,
          fields: [
            { name: 'Wallet', value: `\`${walletAddress}\``, inline: true },
            { name: 'Value', value: `${valueEth} ETH`, inline: true },
            { name: 'Threshold', value: `${thresholdEth} ETH`, inline: true },
            { name: 'Tx hash', value: `[${transaction.hash.slice(0, 12)}...](https://basescan.org/tx/${transaction.hash})` },
          ],
          timestamp: new Date(transaction.timestamp * 1000).toISOString(),
        },
      ],
    }),
  });

  res.sendStatus(200);
});

Register https://your-server.example.com/0watch-to-discord as your webhook URL.


PagerDuty integration

Trigger a PagerDuty incident via the Events API v2:

app.post('/0watch-to-pagerduty', express.raw({ type: 'application/json' }), async (req, res) => {
  const valid = verifySignature(req.body, process.env.ZEROWATCH_WEBHOOK_SECRET, req.headers['x-0watch-signature']);
  if (!valid) return res.status(401).send('Invalid signature');

  const { walletAddress, valueEth, transaction } = JSON.parse(req.body);

  await fetch('https://events.pagerduty.com/v2/enqueue', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      routing_key: process.env.PAGERDUTY_INTEGRATION_KEY,
      event_action: 'trigger',
      dedup_key: transaction.hash,
      payload: {
        summary: `0watch: ${walletAddress} moved ${valueEth} ETH`,
        source: '0watch',
        severity: valueEth > 10 ? 'critical' : 'warning',
        custom_details: {
          wallet: walletAddress,
          valueEth,
          txHash: transaction.hash,
          chainId: transaction.chainId,
          block: transaction.blockNumber,
        },
      },
      links: [
        {
          href: `https://basescan.org/tx/${transaction.hash}`,
          text: 'View on Basescan',
        },
      ],
    }),
  });

  res.sendStatus(200);
});

dedup_key: transaction.hash prevents duplicate incidents for the same transaction.


Local testing with ngrok

ngrok creates a public HTTPS tunnel to your local server, so 0watch can reach it during development.

Setup

# Install ngrok
brew install ngrok  # macOS
# or download from https://ngrok.com/download

# Authenticate (one-time)
ngrok config add-authtoken <YOUR_NGROK_TOKEN>

# Start a tunnel on port 3000
ngrok http 3000

ngrok prints something like:

Forwarding  https://a1b2c3d4.ngrok-free.app -> http://localhost:3000

Register the ngrok URL as your webhook

curl -X POST https://watch.0agent.ai/api/webhooks \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $API_KEY" \
  -d '{
    "url": "https://a1b2c3d4.ngrok-free.app/hook",
    "wallet_address": "0xYourWalletAddress",
    "threshold_eth": 0.01
  }'

Set a low threshold (0.01 ETH) so test transactions trigger it.

Trigger a test delivery

# Fire a test delivery without waiting for a real transaction
curl -X POST https://watch.0agent.ai/api/webhooks/1/test \
  -H "X-API-Key: $API_KEY"

This sends a real signed payload to your endpoint and logs the attempt. Check the ngrok dashboard at http://localhost:4040 to inspect the request/response.

Check delivery history

curl -H "X-API-Key: $API_KEY" https://watch.0agent.ai/api/deliveries
{
  "deliveries": [
    {
      "id": 42,
      "webhookId": 1,
      "status": "delivered",
      "error": null,
      "attemptCount": 1,
      "deliveredAt": 1741550000000
    }
  ],
  "count": 1
}

status: "failed" means both the initial attempt and the 5-second retry failed. The error field has the reason.

Clean up

Delete the webhook when done:

curl -X DELETE -H "X-API-Key: $API_KEY" https://watch.0agent.ai/api/webhooks/1

ngrok URLs change on restart (free tier). If you reset ngrok, re-register the new URL.


Telegram (built-in)

0watch has native Telegram support — no server required. See the webhook guide for the setup flow.


Next steps