# Webhooks

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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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 the raw body>
X-Fynex-Timestamp: <unix seconds at which the delivery was signed>
```

Verify the signature against the **raw** request body before parsing, and
reject a delivery whose timestamp is far from your own clock — that is what
bounds replay of a body someone captured. Deliveries are retried with backoff
until your endpoint answers 2xx; your handler must therefore be idempotent.
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.
