Assistiv Docs

Webhooks

Assistiv sends webhook notifications when budget state changes. Register endpoints in Dashboard → Webhooks.

Event Types

EventFires whenTypical use
budget.topped_upPOST /v1/budget/topup succeedsUpdate dashboard, send receipt
budget.low_balanceA debit crosses low_balance_threshold (edge-triggered, auto re-arms after topup)Email warning
budget.suspendedPATCH /v1/budget with { is_suspended: true }Show "account paused" banner
budget.unsuspendedPATCH /v1/budget with { is_suspended: false }Clear banner
budget.debitedEvery successful debit (firehose, opt-in only, off by default)Ledger mirroring

budget.low_balance is edge-triggered

This event fires once when a debit crosses the low_balance_threshold. It will not fire again until the budget is topped up and a subsequent debit crosses the threshold again.

budget.debited is off by default

This event fires on every successful debit, which at LLM inference cadence can produce very high volume. It is disabled by default — opt in only if you need real-time ledger mirroring.

Ordering is not guaranteed

Webhook deliveries may arrive out of order. Use event_id and created_at to reconcile and deduplicate events on your end.

Registering an Endpoint

Go to Dashboard → Webhooks. Click "Add endpoint", paste your URL, and check the events you want to receive.

Your signing secret is shown in the webhooks table — click "Reveal" to copy it.

Payload Shape

All webhook payloads follow the stable v1 envelope below.

json
{
  "event_type": "budget.low_balance",
  "event_id": "<transaction_id>:budget.low_balance",
  "api_version": "2026-04-11",
  "created_at": "2026-04-11T15:00:00Z",
  "data": {
    "platform_id": "uuid",
    "end_user_id": "uuid",
    "budget_id": "uuid",
    "transaction_id": "uuid",
    "type": "debit",
    "amount_usd": 5.00,
    "max_usd_after": 100.00,
    "used_usd_after": 95.00,
    "remaining_usd_after": 5.00,
    "reason": null,
    "metadata": {}
  }
}

Signature Verification

Every webhook request includes three headers for HMAC-SHA256 verification: webhook-id, webhook-timestamp, and webhook-signature. Always verify signatures before processing events.

Node.js

javascript
import express from "express";
import crypto from "crypto";

const WEBHOOK_SECRET = process.env.ASSISTIV_WEBHOOK_SECRET;

app.post("/webhooks/assistiv", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["webhook-signature"];
  const timestamp = req.headers["webhook-timestamp"];
  const body = req.body.toString();

  const toSign = `${req.headers["webhook-id"]}.${timestamp}.${body}`;
  const expected = crypto
    .createHmac("sha256", Buffer.from(WEBHOOK_SECRET.split("_")[1], "base64"))
    .update(toSign)
    .digest("base64");

  const signatures = signature.split(" ");
  const valid = signatures.some(sig => {
    const sigValue = sig.split(",")[1];
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigValue));
  });

  if (!valid) return res.status(400).send("invalid signature");

  const evt = JSON.parse(body);
  switch (evt.event_type) {
    case "budget.low_balance":
      notifyLowBalance(evt.data.end_user_id, evt.data.remaining_usd_after);
      break;
    case "budget.suspended":
      showSuspendedBanner(evt.data.end_user_id);
      break;
    case "budget.topped_up":
      refreshBalanceCache(evt.data.end_user_id);
      break;
  }
  res.sendStatus(200);
});

Python

python
import hashlib, hmac, base64, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
WEBHOOK_SECRET = os.environ["ASSISTIV_WEBHOOK_SECRET"]

@app.post("/webhooks/assistiv")
async def assistiv_webhook(request: Request):
    body = await request.body()
    msg_id = request.headers.get("webhook-id", "")
    timestamp = request.headers.get("webhook-timestamp", "")
    signature_header = request.headers.get("webhook-signature", "")

    secret_bytes = base64.b64decode(WEBHOOK_SECRET.split("_")[1])
    to_sign = f"{msg_id}.{timestamp}.{body.decode()}".encode()
    expected = base64.b64encode(
        hmac.new(secret_bytes, to_sign, hashlib.sha256).digest()
    ).decode()

    signatures = signature_header.split(" ")
    valid = any(
        hmac.compare_digest(expected, sig.split(",")[1])
        for sig in signatures if "," in sig
    )
    if not valid:
        raise HTTPException(status_code=400, detail="invalid signature")

    evt = await request.json()
    return {"ok": True}

Delivery Guarantees

Webhooks are delivered at-least-once. Failed deliveries are retried with exponential backoff over approximately 24 hours. Use event_id as a dedupe key to handle duplicate deliveries idempotently.

Reconciling Missed Events

If your receiver was down and you missed webhook deliveries, backfill from the budget transaction ledger:

bash
curl "https://api.assistiv.ai/v1/platforms/{pid}/end-users/{euid}/budget/transactions?since=..." \
  -H "Authorization: Bearer sk-plat_your_key"