Signed, retried HTTP callbacks for business events — invoices, e-Factura status, orders, contracts and more.
curl -X POST https://api.brivio.ro/v1/webhooks \
-H "Authorization: Bearer brivio_sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/brivio",
"events": ["invoice.created", "invoice.paid", "efactura.validated", "efactura.rejected"]
}'The response includes a secret (shown once) used to sign every delivery. List available event types at GET /v1/webhooks/events.
Brivio implements the Standard Webhooks specification, so a verifier you already wrote for another provider works unchanged. Every delivery carries three headers:
webhook-id: evt_0f3c… unique per event — use it to deduplicate webhook-timestamp: 1788153475 unix SECONDS webhook-signature: v1,<base64> v1,<b64> space-separated list
The signature is HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{rawBody}. The timestamp is inside the signed content, so a captured delivery cannot be replayed by rewriting it — reject anything older than about five minutes.
Always verify against the raw body. Re-serialising parsed JSON changes the bytes and the signature will not match.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, headers: Record<string, string>, secrets: string[]) {
const id = headers["webhook-id"];
const ts = Number(headers["webhook-timestamp"]);
if (!id || !Number.isInteger(ts)) return false;
if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // replay window
const presented = (headers["webhook-signature"] ?? "").split(" ");
for (const secret of secrets) {
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const mac = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
const expected = Buffer.from(`v1,${mac}`);
for (const token of presented) {
const actual = Buffer.from(token);
if (expected.length === actual.length && timingSafeEqual(expected, actual)) return true;
}
}
return false;
}
// Or simply: import { verifyWebhookSignature } from "@brivio/sdk";Rotating opens a 24-hour overlap window. During it every delivery is signed with both the old and the new secret, and webhook-signature carries one v1,… token per secret. Accept the delivery if any token verifies — pass both secrets to your verifier — and you can redeploy at your own pace without dropping an event. After the window the old secret stops signing.
Return 2xx within 3 seconds. Anything else — a non-2xx status, a timeout, a connection error — counts as a failed attempt. Do your real work asynchronously and acknowledge immediately; a slow consumer is indistinguishable from a broken one.
This is the published retry schedule. 10 attempts in total (the first delivery plus 9 retries), spanning roughly 19 hours:
10s · 20s · 30s · 5m · 20m · 2h · 4h · 6h · 6h
After the last rung a delivery is marked dead and never retried automatically. 410 Gone is treated as a deliberate retirement and stops retries immediately. Endpoints that keep failing are disabled and can be re-enabled from the dashboard. Any delivery from the last 31 days can be replayed from the dashboard or with POST /v1/webhooks/:id/deliveries/:deliveryId/redeliver.
Do not treat webhooks as your only source of truth. They are a latency optimisation, not a guaranteed log. Deliveries are lost when your endpoint is down longer than the retry ladder, when it is auto-disabled after repeated failures, when a deploy drops in-flight requests, or when your handler returns 2xx and then crashes before committing.
Run a reconciliation sweep at least daily: list the resources you care about with an updated_after filter and apply anything you have not already processed. Deduplicate on webhook-id, which is stable across retries and redeliveries, and make your handler idempotent — the same event can legitimately arrive more than once.
addendum.created affiliate.commission.accrued affiliate.commission.available affiliate.enrolled affiliate.payout.paid affiliate.referral.created article.created article.deleted article.updated booking.cancelled booking.confirmed change_request.approved change_request.created contact.created contact.deleted contact.updated contract.created contract.deleted contract.signed contract.suspended contract.updated document.created document.deleted document.updated efactura.rejected efactura.submitted efactura.validated expense.created expense.deleted expense.updated fixed_asset.created fixed_asset.disposed invoice.cancelled invoice.created invoice.deleted invoice.overdue invoice.paid invoice.payment_recorded invoice.sent invoice.updated marketing.campaign.sent milestone.accepted milestone.delivered milestone.rejected order.created order.paid project.created project.deleted project.updated restaurant.order.placed stock.below_minimum trust.batch.completed trust.batch.failed trust.provider.health_changed trust.signature.completed trust.signature.failed trust.timestamp.completed trust.validation.completed
{
"id": "evt_...", // unique delivery id (X-Brivio-Delivery)
"type": "efactura.validated",
"created_at": "2026-08-01T10:00:00Z",
"data": { "invoice_id": "...", "efactura_id": "...", "stare": "ok" }
}Deliveries are at-least-once — deduplicate on the delivery id if your handler isn't idempotent.