A live WebSocket is great for a dashboard, but a poor fit for a serverless function or a backend that shouldn't hold an open socket. Webhooks flip it around: viz.cx watches the chain for you and POSTs matching operations to your URL — signed, with retries.
The delivery payload is identical to the live stream — { op_id, timestamp, op_type, body } — so code that handles one handles both.
Step 1 — Install the SDK
npm install @viz-cx/api @viz-cx/coreStep 2 — Register a webhook
Registration is authenticated by signature challenge: you sign a server-issued nonce with your regular key. The API returns an id and a secret — the secret is shown only once and is used to verify every delivery.
import { createApiClient } from '@viz-cx/api'
import { keys, wif } from '@viz-cx/core'
// Registering a webhook is an authenticated call. A Signer proves you
// control the account by signing a server-issued nonce with your key.
const signer = {
account: 'alice',
sign: (bytes: Uint8Array) => keys.sign(bytes, wif(process.env.VIZ_REGULAR_KEY!)),
}
const api = createApiClient({ auth: signer })
const { id, secret } = await api.webhooks.create({
url: 'https://myapp.example/hooks/viz',
filter: { op_type: 'transfer', account: 'alice' }, // both fields optional
})
// The secret is shown ONCE. Store it — you'll need it to verify deliveries.
console.log('Webhook id:', id)
console.log('Signing secret:', secret)Step 3 — Receive and verify deliveries
Every POST carries an X-Viz-Signature: sha256=<hex> header — an HMAC-SHA256 of the raw body keyed by your secret. Verify it against the raw bytes before trusting anything, then acknowledge quickly.
import express from 'express'
import crypto from 'node:crypto'
const SECRET = process.env.VIZ_WEBHOOK_SECRET!
const app = express()
// Verify against the RAW body — parsing first would change the bytes.
app.post('/hooks/viz', express.raw({ type: 'application/json' }), (req, res) => {
const got = req.header('X-Viz-Signature') ?? ''
const want = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
if (got.length !== want.length ||
!crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
return res.status(401).end()
}
// Same shape as the live stream: { op_id, timestamp, op_type, body }
const { op_type, body, timestamp } = JSON.parse(req.body.toString())
console.log(timestamp, op_type, body)
res.status(200).end() // ack fast — anything under 500 counts as delivered
})
app.listen(3000)Delivery retries three times with backoff on any 5xx or network error, then drops that event — so respond fast and keep your handler idempotent (dedupe on op_id).
Manage your webhooks
// List your webhooks (returns rows without the secret)
const hooks = await api.webhooks.list()
// Remove one when you're done
await api.webhooks.delete(id)Narrow deliveries at registration with the filter — by op_type, by account, or both — so you only get the ops you actually care about.