Webhooks
Assistiv sends webhook notifications when budget state changes. Register endpoints in Dashboard → Webhooks.
Event Types
| Event | Fires when | Typical use |
|---|---|---|
budget.topped_up | POST /v1/budget/topup succeeds | Update dashboard, send receipt |
budget.low_balance | A debit crosses low_balance_threshold (edge-triggered, auto re-arms after topup) | Email warning |
budget.suspended | PATCH /v1/budget with { is_suspended: true } | Show "account paused" banner |
budget.unsuspended | PATCH /v1/budget with { is_suspended: false } | Clear banner |
budget.debited | Every 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.
{
"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
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
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:
curl "https://api.assistiv.ai/v1/platforms/{pid}/end-users/{euid}/budget/transactions?since=..." \
-H "Authorization: Bearer sk-plat_your_key"