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.