Public API
Webhooks
When Address Watch records activity and your webhook is enabled, RpcNode POSTs a flat JSON object to your URL. Allowlist the published egress IPs so your firewall or WAF accepts deliveries. Delivery is asynchronous over Kafka — the Public API and cabinet Test enqueue work; they do not wait for the merchant HTTP response.
Configure the webhook
PUT /v1/address-watch/webhook with url, enabled, and optional events. GET returns url, enabled, events, and egress_ips.
Event filters: payment = non-block hits with direction=in; payout = direction=out; block = type=block. Each type may include payment_networks / payout_networks / block_networks (network slugs). Boolean-only clients still work: true with an empty network list means all networks. Omit events on PUT to keep existing filters (defaults: all event types on, all networks).
egress_ips lists the public source IPs RpcNode uses for outbound POSTs (from server config WEBHOOK_EGRESS_IPS) so you can allowlist them.
Delivery pipeline
Address Watch activity and cabinet Test share the same outbound path. Production hits are consumed from Kafka by public-api-core (ConsumeWatchHits); cabinet Test goes through the connect BFF and enqueues a demo payload with bill=false. Redis holds watch indexes and KV (registry, Bloom, cursors) — it is not the messaging path for webhook delivery.
Kafka topics on this path: rpcnode-watch-blocks (ingest → match), rpcnode-watch-hits (match → public-api-core), rpcnode-webhooks-deliver (enqueue → Go webhook-deliver), rpcnode-webhooks-result (delivery outcome → core webhooks:consume-results; bill on success when bill=true).
Cabinet Test: pick a subscription (event × network), then receive a queued response immediately — not a sync HTTP round-trip to your URL. Delivery runs asynchronously on webhook workers with bill=false (no credit charge).
Ops: enqueue alone is not enough. If messages sit in rpcnode-webhooks-deliver with no consumer, start backend/webhook-deliver (Go). Then run PHP core webhooks:consume-results (supervisor webhooks-results) to bill/log outcomes. Prerequisites: Kafka up; KAFKA_BROKERS set in public-api-core, webhook-deliver, and core.
Service chain + what to run
Address watch / Test (cabinet)
→ connect BFF (test) OR public-api-core ConsumeWatchHits (prod)
→ Kafka rpcnode-webhooks-deliver
→ backend/webhook-deliver (Go) HTTP POST + HMAC
→ Kafka rpcnode-webhooks-result (ok / fail)
→ core webhooks:consume-results (bill on success when bill=true)
Upstream (prod hits only):
Go ingest → rpcnode-watch-blocks → Go match → rpcnode-watch-hits
→ public-api-core persist + fan-out
# 1) HTTP delivery — required when deliver topic has lag
cd backend/webhook-deliver
# KAFKA_BROKERS=host.docker.internal:9092
go run ./cmd/webhook-deliver
# or: docker compose up -d --build
# supervisor: webhook-deliver ×2
# 2) results billing / log
php artisan webhooks:consume-results
# supervisor (node-core-worker): webhooks-resultsDelivery request
Method: POST to your configured webhook.url.
Headers: Content-Type: application/json, X-Timestamp (Unix seconds), X-Signature (hex HMAC for transport integrity — not returned or managed via the API).
Body: flat JSON — no nested payload wrapper.
Address Watch wallet hits: type is payment | payout | other | block (direction on the watched address). Same fields appear on GET /v1/address-watch/events.
Payments product (merchant payment intents): separate payload with product="payments" and type="payment_order" (never type=payment/payout). event is seen | aml_check | paid | failed | underpaid | expired. After on-chain confirm the first webhook is aml_check (funds arrived, AML in progress). After MistTrack the second is paid with an aml object (score, risk_level, details, report_url). Includes payment_id, order_id, amount fields, status, and lifecycle. Delivered to payment.webhook_url when set, otherwise to the same Address Watch webhook URL(s).
Example bodies
// Address Watch — payment (incoming wallet hit)
{
"watch_id": "aw_…",
"network": "ethereum",
"address": "0x…",
"tx_hash": "0x…",
"block_height": 12345678,
"type": "payment",
"direction": "in"
}
// Address Watch — payout (outgoing)
{
"type": "payout",
"direction": "out",
"…": "…"
}
// Address Watch — block
{
"type": "block",
"network": "ethereum",
"block_height": 12345678,
"block_hash": "0x…",
"tx_count": 2,
"transactions": ["0x…", "0x…"]
}
// Payments product — order lifecycle (not Address Watch type=payment)
{
"product": "payments",
"type": "payment_order",
"event": "seen",
"payment_id": "…",
"order_id": "ord_123",
"watch_id": "…",
"network": "ethereum",
"address": "0x…",
"amount": "1.5",
"amount_raw": "1500000000000000000",
"status": "seen",
"tx_hash": "0x…",
"lifecycle": "seen"
}
// Payments — confirmed on-chain, AML in progress
{
"product": "payments",
"type": "payment_order",
"event": "aml_check",
"status": "aml_check",
"tx_hash": "0x…",
"lifecycle": "aml_check",
"aml": null
}
// Payments — AML done (terminal success)
{
"product": "payments",
"type": "payment_order",
"event": "paid",
"status": "paid",
"tx_hash": "0x…",
"lifecycle": "paid",
"aml": {
"provider": "misttrack",
"score": 0,
"risk_level": "Low",
"details": [],
"report_url": "https://…",
"checked_at": "2026-08-15T10:00:00+00:00"
}
}Allowlist source IPs
Treat egress_ips from GET /v1/address-watch/webhook as the authoritative list of RpcNode outbound addresses.
Allowlist those IPs on your firewall, reverse proxy, or WAF so webhook POSTs are not dropped.
You can also configure the webhook URL and event filters in the RpcNode cabinet under WebHooks.
Tips
- Respond quickly with 2xx; delivery uses a short HTTP timeout.
- You can poll GET /v1/address-watch/events as a backup if a delivery fails.
- Address Watch payload type is payment | payout | other | block (never tx). Payments product uses product=payments and type=payment_order with event=seen|aml_check|paid|failed|underpaid|expired — do not confuse with AW type=payment.
- Billing: successful deliveries (bill=true) spend the same credit pool as RPC — payment/payout webhooks ~30 credits; block webhooks = that network’s full get-block cost. Cabinet Test uses bill=false. Address slots stay a hard plan cap (Free = 0).