Webhook delivery
Set channel_type: "webhook" and channel_target to an HTTPS URL. SCMon POSTs a JSON body and signs it so you can prove it came from us.
Webhook URLs must be public HTTPS endpoints. Private, loopback and link-local addresses are rejected (SSRF protection), and all outbound calls leave from a single stable egress IP you can allowlist.
Payload
{
"event": "transfer",
"chain": "bsc",
"watch_id": "wch_…",
"token": "0x55d398326f99059ff775485246999027b3197955",
"direction": "in",
"from": "0xsender…",
"to": "0xyour_wallet…",
"counterparty": "0xsender…",
"amount": "1000000000000000000000",
"tx_hash": "0x…",
"log_index": 12,
"block": 38217640,
"ts": 1718700000
}
amount is raw base units (see Rules). counterparty is the other side relative to your watched address.
Verifying the signature
Each request carries an x-scmon-signature header:
x-scmon-signature: t=1718700000,v1=9f86d081884c7d659a2feaa0c55ad015a…
v1 is HMAC-SHA256(secret, "{t}.{rawBody}") as hex, where secret is your account’s webhook_secret (from GET /v1/account). Recompute it over the raw request body, compare in constant time, and reject a stale t to block replays.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}
Delivery & retries
- Respond
2xxquickly to acknowledge. Any non-2xx(or a timeout) is treated as failed and retried with backoff. - Delivery is paused if your credit balance can’t cover the alert — it resumes automatically after you top up.
- Requests arrive with
user-agent: scmon-webhook/1.0andcontent-type: application/json.