Brivio Developers
Create accountSign in
OverviewQuickstartAuthenticationIdempotencyWebhooksErrors & sandboxSDKs & CLIMCP serverOAuth2 appsAPI reference

Webhooks

Signed, retried HTTP callbacks for business events — invoices, e-Factura status, orders, contracts and more.

Subscribe

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

Verify signatures

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.

TypeScript
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 a secret without downtime

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.

Delivery & retries

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.

Webhooks are lossy — reconcile daily

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.

All events

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

Payload shape

{
  "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.