Webhook Event Fix: invoice.finalized Now Fires Correctly on Finalize
Webhook Event Fix: invoice.finalized Now Fires Correctly on Finalize
Release: v1.0.96
What Changed
Prior to this release, calling POST /v1/invoices/[id]/finalize — the endpoint that transitions a draft invoice to open status — was emitting the wrong webhook event. Instead of firing invoice.finalized, it was emitting invoice.created.
Why This Matters
This was a spec deviation with two concrete consequences:
-
Double-emit of
invoice.created— Theinvoice.createdevent is already fired when a draft invoice is first created viaPOST /v1/invoices. Firing it again on finalize caused webhook consumers to receive a duplicate creation signal, which could trigger unintended side-effects such as double-provisioning, duplicate notifications, or incorrect billing state transitions. -
Suppressed
invoice.finalizedevent — Downstream systems and webhook consumers that listen forinvoice.finalizedto know when an invoice becomes payable never received this event. Any logic gated on an invoice moving from draft → open (e.g. sending a payment link, updating an accounts receivable ledger, or triggering a dunning workflow) would silently fail.
Correct Behaviour (from v1.0.96)
| Action | Endpoint | Webhook Event |
|---|---|---|
| Create draft invoice | POST /v1/invoices | invoice.created |
| Finalize draft invoice | POST /v1/invoices/[id]/finalize | invoice.finalized |
Action Required
If you have webhook consumers that were listening to invoice.created as a signal that an invoice became payable (i.e. open), you should update them to listen for invoice.finalized instead.
The invoice.created event should only be treated as a signal that a new draft invoice exists — not that it is ready for payment.
Example: Updating a Webhook Handler
// Before (incorrect — relying on invoice.created for payable invoices)
if (event.type === 'invoice.created') {
await sendPaymentLink(event.data.object);
}
// After (correct — use invoice.finalized for payable invoices)
if (event.type === 'invoice.finalized') {
await sendPaymentLink(event.data.object);
}
// invoice.created should now only handle draft invoice creation
if (event.type === 'invoice.created') {
await recordDraftInvoice(event.data.object);
}
Affected Webhook Event Types
invoice.created— Fires when a new draft invoice is created viaPOST /v1/invoices. No change to this behaviour.invoice.finalized— Now correctly fires when a draft invoice is transitioned to open viaPOST /v1/invoices/[id]/finalize.