Webhooks
Notificări HTTP pentru evenimentele din platformă.
Cum funcționează
Înregistrezi o adresă și tipurile de evenimente care te interesează. La producerea unui eveniment, Brivio trimite un POST cu corpul JSON și o semnătură în antet, calculată cu secretul abonamentului.
Livrare și repetare
Livrarea este cel puțin o dată: un răspuns care nu este 2xx este reîncercat cu întârziere crescătoare. Fă handler-ul idempotent după identificatorul evenimentului — un eveniment poate ajunge de două ori și acesta nu este un defect.
Verificarea semnăturii
Fiecare livrare respectă Standard Webhooks și are trei antete:
webhook-id— identificatorul evenimentului (evt_…), aceeași valoare caiddin corp. Deduplică după el.webhook-timestamp— momentul semnării, în secunde Unix.webhook-signature— o listă de intrăriv1,<semnătură>separate prin spațiu, unde<semnătură>este HMAC-SHA256 în base64 al șirului{webhook-id}.{webhook-timestamp}.{corpul brut}. În timpul rotirii secretului lista conține câte o intrare pentru fiecare secret activ; acceptă livrarea dacă oricare se potrivește.
Secretul abonamentului arată ca whsec_<base64>; cheia HMAC este partea de după whsec_, decodată din base64. Respinge livrările al căror moment diferă cu mai mult de 5 minute de ceasul tău și calculează HMAC-ul peste octeții bruți ai corpului, înainte de orice parsare JSON. X-Brivio-Event și X-Brivio-Delivery sunt doar informative și nu sunt semnate.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBrivioWebhook(rawBody, headers, secret) {
const id = headers["webhook-id"], ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
return String(headers["webhook-signature"]).split(" ").some((entry) => {
const [version, value] = entry.split(",");
const got = Buffer.from(value ?? "", "base64");
return version === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
});
}Evenimente de plată
invoice.paid se declanșează când o factură emisă este încasată integral; invoice.partially_paid când se înregistrează o plată și rămâne un rest. Ambele se declanșează din orice cale prin care se înregistrează bani — o plată introdusă manual, o linie bancară potrivită la reconciliere, o plată cu cardul prin pagina de plată, un bilet la ordin onorat, o chitanță colectivă — astfel încât un abonat poate reflecta soldul deschis fără interogări repetate.
Ambele evenimente sunt idempotente per plată: id-ul livrării este evt_pay_<paymentId>, deci un procesator care retrimite notificarea, sau Brivio care relivrează după ce endpoint-ul tău a fost indisponibil, poartă același id. Deduplică după el.
{
"id": "evt_pay_3f2a1b4c000040008000000000000001",
"type": "invoice.partially_paid",
"created": 1790000000,
"environment": "live",
"data": {
"invoice_id": "…",
"number": "IMS42",
"currency": "RON",
"total": "1000.00",
"paid_amount": "400.00",
"remaining_amount": "600.00",
"status": "PARTIALLY_PAID",
"payment": { "id": "…", "amount": "400.00", "payment_date": "2026-09-23", "method": "BANK_TRANSFER", "reference": "BANK:…", "source": "bank_match" },
"paid_at": null,
"contact_id": "…"
}
}payment.source spune de unde au venit banii: manual, mark_paid, bank_match, provider (card/procesator), instrument, receipt, telegram, api (înregistrată prin POST /v1/invoices/{id}/payments; o linie de extras potrivită prin API raportează bank_match). Rambursările nu emit aceste evenimente. Când o plată înregistrată este stornată, invoice.payment_reversed se emite o singură dată per stornare (id evt_rev_<reversalId>), cu soldul redeschis și reversal.reverses_payment_id indicând plata de anulat; schema payload-ului este InvoicePaymentReversedWebhookData. Lista completă a tipurilor la care te poți abona este GET /v1/webhooks/events; schema payload-ului este InvoicePaymentWebhookData în documentul OpenAPI.
Fiecare payload de sold conține și credited_amount: suma notelor de credit emise care stornează factura. remaining_amount este întotdeauna total − credited_amount − paid_amount, niciodată negativ.
invoice.credited
O notă de credit (storno) anulează o datorie — nu este o plată, deci nu declanșează niciodată invoice.paid. invoice.credited se emite când o notă de credit care stornează o factură emisă este emisă sau anulată și se schimbă suma stornată a facturii sursă. data.invoice_id este factura sursă; credit_note_id este nota de credit care a declanșat evenimentul. status este noul status al sursei — CREDITED (stornată integral) când este stornată complet (fully_credited: true). Dacă clientul plătise deja mai mult decât totalul net, refund_due este suma de restituit.
Id-ul este evt_cred_<sourceId>_<creditedAmountCents>: o resincronizare care ajunge la aceeași sumă stornată poartă același id (deduplică după el), iar emiterea și apoi anularea unei note de credit produc două evenimente. Schema payload-ului este InvoiceCreditedWebhookData.
{
"id": "evt_cred_3f2a1b4c000040008000000000000001_40000",
"type": "invoice.credited",
"created": 1790000000,
"environment": "live",
"data": {
"invoice_id": "…",
"number": "IMS42",
"currency": "RON",
"total": "1000.00",
"credited_amount": "400.00",
"paid_amount": "0.00",
"remaining_amount": "600.00",
"refund_due": "0.00",
"status": "SENT",
"fully_credited": false,
"credit_note_id": "…",
"contact_id": "…"
}
}contact.fiscal_status_changed se emite când un client care are încă facturi emise neîncasate își schimbă statutul fiscal la ANAF sau la registrul comerțului. changes[].kind este unul dintre became_inactive (a devenit inactiv), reactivated, struck_off (radiat sau dizolvat), insolvency, vat_registered, vat_deregistered, cash_vat_on, cash_vat_off (TVA la încasare), cu effective_date când ANAF publică o dată. open_invoices conține numărul facturilor și soldul deschis în RON. Verificarea rulează zilnic; prima verificare a unui contact înregistrează starea de referință și nu trimite nimic. Id-ul este evt_fsc_<contactId>_<kind>_<date> pentru prima intrare din changes; schema payload-ului este ContactFiscalStatusWebhookData.
Trasee, flotă și dosare
Aceste evenimente se emit la fel indiferent dacă modificarea s-a făcut din aplicație sau prin API, după ce scrierea în baza de date s-a încheiat. Conțin doar identificatori, nu date personale.
delivery_route.dispatched— un traseu de livrare a fost trimis șoferului:id,number,driver_id,vehicle_id,stop_count,dispatched_at. O retrimitere este un eveniment nou. Schema:DeliveryRouteDispatchedWebhookData.fleet_vehicle.created,fleet_vehicle.updated,fleet_vehicle.deleted—idșiplate; laupdated,changed_fieldsconține doar numele câmpurilor modificate, niciodată valorile. O editare care nu schimbă nimic nu emite. Schema:FleetVehicleWebhookData.court_case.hearing_added— un termen nou într-un dosar:id(termenul),case_id,hearing_date,court. Fără părți, complet, soluție sau note. Idevt_court_hearing_<hearingId>. Schema:CourtHearingAddedWebhookData.
Email primit
email.received se emite o singură dată pentru fiecare mesaj primit într-o căsuță Brivio a organizației tale, în Inbox sau într-un dosar creat de tine. Nu se emite pentru spam, coș, ciorne sau mesajele trimise chiar de căsuța ta și nici pentru istoricul importat la prima sincronizare a unei căsuțe — doar pentru mesajele primite în ultimele 24 de ore. Căsuțele personale care nu aparțin unei organizații nu emit niciodată acest eveniment.
Evenimentul conține doar metadate, ca să poți direcționa sau număra corespondența fără ca Brivio să transmită conținutul:
id— mesajul stocat în Brivio;message_id— antetulMessage-ID(RFC 5322) saunull.mailbox— căsuța Brivio în care a ajuns;recipient— adresa căsuței sau a aliasului către care a fost livrat.from,subject,received_at.size_bytes(saunullcând furnizorul nu raportează dimensiunea) șihas_attachments.
Nu include niciodată corpul mesajului, un fragment din el, destinatarii cc/bcc sau numele atașamentelor. Dacă ai nevoie de ele, le citești din aplicație.
Id-ul livrării este stabil per mesaj: evt_<32 caractere hex> este derivat din id-ul mesajului stocat, deci o resincronizare a căsuței nu anunță niciodată același mesaj de două ori, iar o relivrare după ce endpoint-ul tău a fost indisponibil poartă același id. Schema: EmailReceivedWebhookData.
{
"id": "evt_3f2a1b4c…",
"type": "email.received",
"created": 1791200000,
"environment": "live",
"data": {
"object": "email_message",
"id": "…",
"message_id": "<CA+abc@mail.example.com>",
"mailbox": "contact@dragoscatalin.ro",
"recipient": "hello@dragoscatalin.ro",
"from": "visitor@example.com",
"subject": "Hello",
"received_at": "2026-10-05T10:00:00.000Z",
"size_bytes": 18234,
"has_attachments": false,
"organizationId": "…"
}
}organizationId este adăugat de stratul de livrare la fiecare eveniment de domeniu.