← Back to Viaclave

Webhooks

Receive real-time notifications when events happen in your Viaclave account. Webhooks are signed with HMAC-SHA256 and retried with exponential backoff on failure.

Signature Verification

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 signed

The 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.

Verification Examples

Node.js

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);
}

Python

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)

Retry Behaviour

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:

AttemptDelayCumulative Wait
1Immediate0 s
21 s1 s
34 s5 s
416 s21 s
564 s85 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.

Request Format

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

Webhook Events

Subscribe to any combination of events when creating a webhook endpoint.

EventDescription
payment.sentAn outgoing payment was broadcast
payment.receivedAn incoming payment was confirmed
payment.failedA payment could not be completed
payment.awaiting_approvalA high-value payment needs manual approval
payment.approvedA pending payment was approved
payment.rejectedA pending payment was rejected
payment.expiredAn unapproved payment expired
deposit.confirmedAn on-chain deposit settled
swap.completedA token swap completed
swap.failedA token swap failed
withdrawal.confirmedAn on-chain withdrawal settled
withdrawal.failedA withdrawal could not be completed
withdrawal.awaiting_approvalA high-value withdrawal needs approval
withdrawal.approvedA pending withdrawal was approved
withdrawal.rejectedA pending withdrawal was rejected
withdrawal.expiredAn unapproved withdrawal expired
account.suspendedThe account was suspended by an admin
account.unsuspendedThe account suspension was lifted
account.password_changedAccount password was changed
account.api_key_regeneratedAPI key was rotated
wallet.pausedWallet was paused
wallet.resumedWallet was resumed
wallet.auto_pausedWallet paused by a spending policy
wallet.key_exportedWallet private key was exported
account.tier_downgradedBilling tier was downgraded
subscription.createdA recurring subscription was created
subscription.paidA subscription payment succeeded
subscription.failedA subscription payment failed
subscription.pausedA subscription was paused
subscription.resumedA subscription was resumed
subscription.cancelledA subscription was cancelled