Skip to content
In this section

Webhooks

The event catalog and signing.

Billing emits signed webhook events through the same delivery pipe as the Payments API: one endpoint registration, one signature scheme, one retry policy — a receiver built for payment events handles billing events with no new integration.

Events are recorded in the same database transaction as the state change they describe. A delivered event can never describe a state that was rolled back, and each transition emits exactly once — retries of your endpoint receive the same event, never a second one.

Event catalog

Event Fires when
InvoiceIssued A document is issued — numbered, legally real.
InvoiceSent The collection link was created (POST /invoices/{invoiceId}/send).
InvoicePaid The document settled — by hosted payment, matched bank transfer, or stored credit covering it in full. paidVia says which.
InvoiceOverdue A sent document passed its due date unpaid.
InvoiceVoided The document was cancelled before money moved.
InvoiceWrittenOff Collection was abandoned.
CreditApplied An issued invoice drew stored credit down.
CreditDepleted That drawdown consumed the contract's last available credit in the invoice's currency — top up, or the next invoice asks the customer for cash.

Subscription lifecycle

Event Fires when
BillingSubscriptionStarted A subscription was created on a contract.
BillingSubscriptionActivated It became active — a trial converted, or a term began.
BillingSubscriptionRenewed A term rolled over into the next one.
BillingSubscriptionPastDue Collection failed, or a trial ended with no payment method.
BillingSubscriptionRecovered It came back from past due.
BillingSubscriptionPaused Billing is suspended; no invoices are raised while it is.
BillingSubscriptionResumed Billing continues on the original schedule — the period is not restarted.
BillingSubscriptionCancelScheduled A cancellation was requested and takes effect after its notice period.
BillingSubscriptionCanceled The cancellation took effect.
BillingSubscriptionExpired A fixed end date was reached.

Why the Billing prefix. The unprefixed Subscription* events on this same pipe — SubscriptionCreated, SubscriptionCharged, SubscriptionCancelled, SubscriptionPastDue — describe payment-link recurring subscriptions, a different object with its own lifecycle. Two products, one word. Subscribing to those will not tell you anything about a billing subscription or its contract; subscribe to the prefixed family instead.

One exception, and you have to know it: SubscriptionTrialWillEnd. The prefixed family has no trial-ending event — a billing subscription's trial reminder is emitted under that unprefixed name instead, one day before the trial ends, with a payload of its own:

{
  "source": "billing",
  "subscriptionId": 15,
  "contractId": 74,
  "trialEnd": "2026-09-17",
  "amountMinor": 820000,
  "currency": "EUR"
}

source is how you tell it apart: billing on the shape above, payment_links on the larger shape a payment-link subscription's own trial reminder carries. It is the OpenAPI discriminator for the two, so a generated client picks the right type from it. (contractId is present on the billing shape only and remains a valid tell for a receiver written before source existed — a payment-link subscription has no contract.) The Payments API's webhook catalog publishes both under the one event name. If you want a trial-ending warning for a billing subscription, you must subscribe to the unprefixed event — the prefixed family does not carry one.

Metered-usage threshold alerts (UsageAlertFired) were already delivered on this pipe and are unchanged.

The delivery body

Every delivery — whatever fired — is an envelope with the event's payload nested inside it. The four envelope fields are the same for every family:

{
  "eventId": 918204,
  "eventType": "InvoicePaid",
  "sellerAccountUuid": "6f2a1c1e-6a1e-4f10-9f2b-9c1d0b3a7e55",
  "occurredAt": "2026-08-20T14:02:11Z",
  "payload": { }
}

eventId is the delivery's stable identity: deduplicate on it. occurredAt is when the state changed, not when the delivery was attempted — a retry repeats the original value. sellerAccountUuid is your Fynex account, never your customer.

Every event is also published in the OpenAPI document under its top-level webhooks key, so a generated client carries the body type rather than map[string]any.

Payloads

Invoice events carry the document's public identity and money facts — the same fields the API serves, and nothing it does not:

{
  "invoiceId": 4180,
  "invoiceNumber": "UK2607AA-2608AAB",
  "contractId": 42,
  "sellerCustomerId": 7,
  "origin": "usage",
  "status": "paid",
  "paidVia": "payment_link",
  "currency": "EUR",
  "grandTotalMinor": 12000,
  "collectibleMinor": 12000,
  "dueDate": "2026-08-15T00:00:00Z",
  "occurredAt": "2026-08-20T14:02:11Z"
}

Buyer name and email are deliberately absent — webhook bodies land in the receiver's logs, and the single-invoice read serves them to a caller who holds the key. Treat the event as the signal and GET /invoices/{invoiceId} as the source of truth.

A subscription event carries the subscription's identity and where it landed:

{
  "subscriptionId": 15,
  "contractId": 74,
  "status": "paused",
  "billingFrequency": "monthly",
  "currency": "EUR",
  "priceMinor": 820000,
  "currentPeriodStart": "2026-08-17T00:00:00Z",
  "currentPeriodEnd": "2026-09-17T00:00:00Z",
  "occurredAt": "2026-08-28T11:04:22Z"
}

The event type says what happened and status says where it landed; there is no previousStatus. Nothing in the body identifies the customer — look them up through the contract if you need to.

Credit events:

{
  "contractId": 42,
  "invoiceId": 4181,
  "invoiceNumber": "UK2607AA-2608AAC",
  "currency": "EUR",
  "appliedMinor": 250000,
  "remainingMinor": 0,
  "occurredAt": "2026-09-01T00:05:00Z"
}

remainingMinor is the contract's available credit after the movement — it is 0 on every CreditDepleted by definition.

Delivery, signing, retries

Deliveries are HTTP POSTs to the webhook endpoints configured for your account, signed with your endpoint's secret:

X-Fynex-Signature: sha256=<hex HMAC-SHA256 of "<X-Fynex-Timestamp>.<raw body>">
X-Fynex-Timestamp: <unix seconds at which the delivery was signed>

Compute the HMAC over the X-Fynex-Timestamp header value, a literal ., and the raw request body — before parsing — and reject a delivery whose timestamp is more than 5 minutes from your own clock. Signing the timestamp together with the body is what bounds replay of a body someone captured: neither can be changed independently. Full verifiers are in the Payments Webhooks guide. Only a 200 counts as delivered: anything else is retried up to 3 attempts in total and then marked failed, so your handler must be idempotent and must answer 200, not 202 or 204. Deduplicate on the envelope's eventId, which is stable across every retry of the same event.

Endpoint registration, secret rotation and the retry schedule are the Payments API's webhook machinery — see its Webhooks guide; nothing about it is billing-specific.

Polling still works

The polling guidance in Workflows remains valid and is the right tool for reconciliation: a month-end close should read GET /invoices for the period rather than reconstruct it from events. Webhooks are for the moment an invoice being paid unlocks something — activating a seller, releasing a listing, restoring access — where minutes of polling latency is real waiting.