Receive real-time notifications when events happen in your Viaclave account. Webhooks are signed with HMAC-SHA256 and retried with exponential backoff on failure.
Every webhook request includes two headers used for verification:
X-Viaclave-Signature — HMAC-SHA256 hex digest of {timestamp}.{body}X-Viaclave-Timestamp — Unix epoch seconds when the request was signedThe signing key is the webhook secret displayed when you create a webhook endpoint. Compare timestamps to reject requests older than 5 minutes to prevent replay attacks.
const crypto = require("crypto");
function verifyWebhook(req, secret) {
const signature = req.headers["x-viaclave-signature"];
const timestamp = req.headers["x-viaclave-timestamp"];
const body = JSON.stringify(req.body);
// Reject requests older than 5 minutes to prevent replay attacks
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (Math.abs(age) > 300) {
throw new Error("Timestamp too old");
}
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
throw new Error("Invalid signature");
}
return JSON.parse(body);
}import hmac, hashlib, time, json
def verify_webhook(headers: dict, body: bytes, secret: str):
signature = headers["x-viaclave-signature"]
timestamp = headers["x-viaclave-timestamp"]
# Reject requests older than 5 minutes
age = abs(int(time.time()) - int(timestamp))
if age > 300:
raise ValueError("Timestamp too old")
message = f"{timestamp}.{body.decode()}"
expected = hmac.new(
secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("Invalid signature")
return json.loads(body)If your endpoint returns a non-2xx status or the request fails due to a network error, Viaclave retries with exponential backoff up to 5 total attempts:
| Attempt | Delay | Cumulative Wait |
|---|---|---|
| 1 | Immediate | 0 s |
| 2 | 1 s | 1 s |
| 3 | 4 s | 5 s |
| 4 | 16 s | 21 s |
| 5 | 64 s | 85 s |
After all 5 attempts are exhausted the delivery is recorded in the dead-letter queue (DLQ). Admins can inspect and retry failed deliveries through the ops API.
Every webhook is delivered as an HTTP POST with a JSON body:
{
"id": "whe_abc123",
"event": "payment.received",
"data": { ... },
"timestamp": 1714000000000
}Additional headers: X-Viaclave-Event, X-Viaclave-Event-Id, X-Viaclave-Attempt
Subscribe to any combination of events when creating a webhook endpoint.
| Event | Description |
|---|---|
| payment.sent | An outgoing payment was broadcast |
| payment.received | An incoming payment was confirmed |
| payment.failed | A payment could not be completed |
| payment.awaiting_approval | A high-value payment needs manual approval |
| payment.approved | A pending payment was approved |
| payment.rejected | A pending payment was rejected |
| payment.expired | An unapproved payment expired |
| deposit.confirmed | An on-chain deposit settled |
| swap.completed | A token swap completed |
| swap.failed | A token swap failed |
| withdrawal.confirmed | An on-chain withdrawal settled |
| withdrawal.failed | A withdrawal could not be completed |
| withdrawal.awaiting_approval | A high-value withdrawal needs approval |
| withdrawal.approved | A pending withdrawal was approved |
| withdrawal.rejected | A pending withdrawal was rejected |
| withdrawal.expired | An unapproved withdrawal expired |
| account.suspended | The account was suspended by an admin |
| account.unsuspended | The account suspension was lifted |
| account.password_changed | Account password was changed |
| account.api_key_regenerated | API key was rotated |
| wallet.paused | Wallet was paused |
| wallet.resumed | Wallet was resumed |
| wallet.auto_paused | Wallet paused by a spending policy |
| wallet.key_exported | Wallet private key was exported |
| account.tier_downgraded | Billing tier was downgraded |
| subscription.created | A recurring subscription was created |
| subscription.paid | A subscription payment succeeded |
| subscription.failed | A subscription payment failed |
| subscription.paused | A subscription was paused |
| subscription.resumed | A subscription was resumed |
| subscription.cancelled | A subscription was cancelled |