Skip to content
vokse.
Browse the reference
Back to the API reference
Guide

Receiving webhooks

Webhooks push events to you in real time instead of polling. Subscribe a URL, pick the events you care about, and verify each delivery with its HMAC signature.

Subscribing

Create a subscription through the Webhooks API with your endpoint URL and the eventTypes to receive. You will get back a signing secret. Store it; it verifies every delivery.

bash
curl -H "Authorization: Bearer vokse_sk_…" \     -H "Content-Type: application/json" \     -H "Idempotency-Key: $(uuidgen)" \     -X POST https://api.vokse.ai/households/current/webhooks/subscriptions \     -d '{"url":"https://example.com/hooks/vokse","eventTypes":["transaction.created","budget.overrun"]}'

Event types

The full, live list of event types is served from /meta/webhook-events. The catalogue today:

  • transaction.created: a transaction was recorded.
  • recurrence.materialised: a recurrence wrote its scheduled transaction.
  • bank.sync.completed: a bank connection finished syncing.
  • budget.overrun: a category went over its budget for the month.
  • spending.anomaly: unusual spending was detected.
  • goal.reached: a savings goal hit its target.
  • insight.generated: a new AI insight is ready.

Verifying the signature

Each delivery carries an X-Vokse-Signature header of the form t=<timestamp>,v1=<signature>. The signature is an HMAC-SHA256 keyed by your signing secret over the timestamp, a dot, and the raw request body. Rebuild that string from the exact bytes you received, recompute the HMAC, compare it against v1 in constant time, and reject timestamps that are too old.

js
import crypto from 'node:crypto';// X-Vokse-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>function verify(rawBody, signatureHeader, signingSecret) {  const parts = new Map(    signatureHeader.split(',').map((kv) => kv.split('=')),  );  const t = parts.get('t');  const v1 = parts.get('v1');  const expected = crypto    .createHmac('sha256', signingSecret)    .update(`${t}.${rawBody}`) // timestamp, a dot, the exact bytes received    .digest('hex');  if (!t || !v1 || v1.length !== expected.length) return false;  // freshness: reject stale timestamps to block replays  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}

Retries & delivery

Respond 2xx within a few seconds to acknowledge. 5xx responses and timeouts are retried with exponential backoff, up to five attempts; a 4xx is treated as final and never retried. After repeated consecutive failures the subscription is auto-paused and the household owner is notified.