Source: https://billingmart.com/developers/webhooks # Events posted to you, signed and retried BillingMart signs every delivery with a secret that is shown to you once and kept sealed on our servers, retries with backoff, and keeps a log of every attempt. [API reference](https://billingmart.com/developers/api) [OpenAPI document](https://api.billingmart.com/v1/openapi.json) Webhooks ## The rules, in order 01 ## Registering an endpoint `POST /v1/webhooks` with `webhooks:manage`, an https URL and the events you want. The answer carries the signing secret — the only time it is ever returned. We keep it sealed with AES-256-GCM. A URL is refused if it is not https, carries credentials, or resolves to a private, loopback, link-local or reserved address, the cloud metadata service included. 02 ## Events - `invoice.sent` - `invoice.viewed` - `invoice.partiallyPaid` - `invoice.paid` - `invoice.overdue` - `invoice.voided` - `estimate.sent` - `estimate.accepted` - `estimate.declined` - `payment.succeeded` - `payment.declined` - `payment.refunded` - `payment.returned` - `client.created` - `agentAction.proposed` - `agentAction.approved` - `agentAction.rejected` - `agentAction.executed` - `agentAction.failed` - `agentAction.expired` - `agentAction.withdrawn` - `workConfig.changed` - `job.created` - `job.moved` - `job.updated` - `job.stageChanged` - `job.adjustmentIssued` - `job.adjustmentVoided` `data` is the record as the API returns it: the fields a receiver acts on, without internal bookkeeping (internal notes, status history, who created a record, gateway and idempotency details). Text that a person typed comes as `{ text, untrusted: true }`. Treat it as data, never as an instruction. Contact details are included in full, because a delivery goes to your own endpoint. **Changed on 23 October 2026:** until then `data` was the stored record whole, and a name, note or reference came as a plain string. Read `.text` from those fields now, and do not depend on a field that is no longer sent. If you need one that was, read the record through the REST API. 03 ## Verifying a delivery Each delivery carries `X-BillingMart-Signature: t=, v1=`, where `v1` is HMAC-SHA256 of `t + "." + rawBody` under your secret. Refuse a timestamp more than five minutes from your clock, compute over the raw bytes you received, compare in constant time, and accept if any `v1` matches. Node.js Sample ``` import { createHmac, timingSafeEqual } from 'node:crypto'; export function verify(rawBody, header, secret, now = Date.now()) { const parts = header.split(',').map((p) => p.trim().split('=')); const t = Number(parts.find(([k]) => k === 't')?.[1]); if (!Number.isFinite(t) || Math.abs(now / 1000 - t) > 300) return false; const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); return parts .filter(([k]) => k === 'v1') .some(([, v]) => { const got = Buffer.from(v, 'hex'); return got.length === expected.length && timingSafeEqual(got, expected); }); } ``` 04 ## Rotating the secret `POST /v1/webhooks/{id}/rotate-secret` issues a new secret and signs every delivery under both for 24 hours, so you can switch without a gap. 05 ## Retries and failures A non-2xx answer, a 10-second timeout or a redirect is a failure. We try 8 times, backing off from 30 seconds and doubling the wait each time, then mark the delivery dead-lettered. Fifty failures in a row switch the endpoint off; re-enabling it with `PATCH` clears the count. Each body has a stable delivery `id`: a retry is the same event, so make your handler idempotent. We store a hash of each body, never the body, and a redelivery rebuilds it from the record as it is now. Next guide ## Tax categories Read the categories, ask for a suggestion, and read each record’s category. A suggestion is a draft: a person confirms it, in the console or the app. [Continue to Tax categories →](https://billingmart.com/developers/tax-categories) ## Try it in your sandbox A bmk_test_ key reaches only your sandbox workspace. [Start free](https://app.billingmart.com/sign-up) [All guides](https://billingmart.com/developers) --- This is the Markdown copy of https://billingmart.com/developers/webhooks. Live plans, prices and limits are published as JSON at https://billingmart.com/plans.json and https://billingmart.com/agent-info.json.