Platform

Webhooks

Receive signed, real-time events for invoices, payments, stock, purchases and every other change in a company.

Overview

Webhooks let your systems react to changes in Corebill without polling. When a subscribed event happens, Corebill sends an HTTP POST with a JSON body to your endpoint, signed with the destination secret.

Events fire the same way whether the change came from the dashboard, the REST API, an automation, Stripe or the AI Agent. Destinations are configured per company in Settings > Developers > Webhooks: URL, events, custom headers, signing secret and the delivery log.

Events of optional modules (Vendors, Inventory, Purchases, Cash, Point of sale, Work orders, Projects, Expenses, Subscriptions) are only available while the module is enabled in the company.

Available Events

Customers

EventWhen it firesAutomations
customer.createdA new customer was added from the app, the API, an import or the AI agent.Yes
customer.updatedAny field of a customer changed.Yes
customer.deletedA customer was deleted.No

Quotes

EventWhen it firesAutomations
quote.createdA quote or a new version of a quote was created.Yes
quote.updatedThe lines, amounts or details of a quote changed.Yes
quote.sentA quote was sent to the customer.Yes
quote.approvedThe customer or your team approved a quote.Yes
quote.rejectedThe customer or your team rejected a quote.Yes
quote.expiredA quote passed its valid until date without an answer.Yes
quote.convertedAn invoice was created from a quote.Yes
quote.deletedA quote was deleted.No

Invoices

EventWhen it firesAutomations
invoice.createdA draft invoice was created.Yes
invoice.updatedThe lines, dates or details of an invoice changed.Yes
invoice.issuedAn invoice left draft and got its final number.Yes
invoice.sentAn invoice was sent to the customer.Yes
invoice.payment_recordedA payment was recorded and the invoice still has a balance due.Yes
invoice.paidAn invoice was paid in full.Yes
invoice.payment_updatedA recorded payment of an invoice was edited.No
invoice.payment_deletedA recorded payment of an invoice was deleted.No
invoice.overdueAn invoice reached its due date with a balance due.Yes
invoice.cancelledAn issued invoice was voided.Yes
invoice.marked_uncollectibleThe balance due of an invoice was written off.Yes
invoice.refundedA refund was recorded on an invoice.Yes
invoice.deletedA draft invoice was deleted.No

Payments

EventWhen it firesAutomations
payment.receivedMoney came in for an invoice: manual, Stripe, point of sale or API.Yes
payment.failedA card payment through Stripe was declined.Yes

Services and products

EventWhen it firesAutomations
item.createdA new item was added to the catalog.Yes
item.updatedThe price or details of a catalog item changed.Yes
item.deletedA catalog item was deleted.No

Subscriptions

Requires the Subscriptions module.

EventWhen it firesAutomations
subscription.canceledA Stripe subscription ended.Yes
subscription.cancel_scheduledA Stripe subscription will end at the close of its period.Yes
subscription.plan_changedA Stripe subscription moved to another plan or price.Yes

Projects and tasks

Requires the Projects module.

EventWhen it firesAutomations
project.createdA new project was created.Yes
project.updatedThe status or details of a project changed.Yes
project.deletedA project was deleted.No
task.createdA task was added to a project.Yes
task.updatedThe status, assignee or details of a task changed.Yes
task.deletedA task was deleted.No

Expenses

Requires the Expenses module.

EventWhen it firesAutomations
expense.createdAn expense was recorded, including Stripe fees and recurring expenses.Yes
expense.updatedThe amount or details of an expense changed.Yes
expense.deletedAn expense was deleted.No

Vendors

Requires the Vendors module.

EventWhen it firesAutomations
vendor.createdA new vendor was added.Yes
vendor.updatedThe details, terms or bank data of a vendor changed.Yes
vendor.deletedA vendor was deleted.No

Inventory

Requires the Inventory module.

EventWhen it firesAutomations
stock.adjustedStock went in or out with an adjustment (count, damage, gift...) or its reversal.Yes
stock.transferredProducts moved from one warehouse to another.Yes
stock.lowA product reached its reorder point after a sale or stock out.Yes

Purchases

Requires the Purchases module.

EventWhen it firesAutomations
purchase_order.createdA purchase order was created as a draft.Yes
purchase_order.approvedA purchase order was approved or confirmed.Yes
purchase_order.receivedGoods of a purchase order were received, in part or in full.Yes
bill.registeredA vendor bill was registered and added to accounts payable.Yes
bill.payment_recordedA payment to a vendor was recorded.Yes
bill.paidA vendor bill was paid in full.Yes
bill.cancelledA vendor bill was cancelled.Yes

Cash

Requires the Cash module.

EventWhen it firesAutomations
cash_session.openedA cash register session was opened with its opening amount.Yes
cash_session.closedA cash register session was closed with its count and difference.Yes
cash_movement.createdCash went in or out of a register manually.Yes

Point of sale

Requires the Point of sale module.

EventWhen it firesAutomations
pos_sale.completedA point of sale ticket was charged.Yes
pos_sale.voidedA point of sale ticket was voided and refunded.Yes

Work orders

Requires the Work orders module.

EventWhen it firesAutomations
work_order.createdA new work order was opened.Yes
work_order.status_changedA work order moved to pending, in progress, ready or delivered.Yes
work_order.deliveredA work order was delivered to the customer.Yes
work_order.payment_recordedA payment or down payment of a work order was recorded.Yes

Payload

JSON
1{
2 "id": "evt_8f2c1a9b7d3e4f56",
3 "event": "invoice.paid",
4 "created_at": "2026-10-11T15:30:00.000Z",
5 "webhook_id": "wh_a1b2c3d4e5f6",
6 "data": {
7 "id": "inv_b2c3d4e5f6g7h8i9",
8 "invoice_number": "INV-2026-000042",
9 "customer_id": "cus_a1b2c3d4e5f6g7h8",
10 "issue_date": "2026-10-11",
11 "due_date": "2026-11-10",
12 "currency": "USD",
13 "subtotal": 1500,
14 "discount_amount": 0,
15 "tax_amount": 270,
16 "total": 1770,
17 "amount_paid": 1770,
18 "amount_due": 0,
19 "status": "paid",
20 "lines": [
21 {
22 "id": "dit_a1b2c3d4e5f6",
23 "item_id": "itm_e5f6g7h8i9j0k1l2",
24 "name": "Web Development - Senior",
25 "quantity": 10,
26 "unit_price": 150,
27 "total": 1500
28 }
29 ],
30 "created_at": "2026-10-11T15:30:00.000Z",
31 "updated_at": "2026-10-11T15:30:00.000Z"
32 },
33 "metadata": {}
34}

id is the event id (the same on every retry), event the event type and data the resource. Core resources (customers, quotes, invoices, items, expenses) use the same shape as the REST API. metadata carries the custom fields of customers, quotes and invoices. Test events include "test": true.

Headers

HeaderDescription
Corebill-Signaturet=,v1=. The signature is HMAC-SHA256 (hex) of . with your secret. After a secret rotation a second v1 signed with the previous secret is included for 24 hours
X-Webhook-SignatureLegacy: HMAC-SHA256 (hex) of the raw body. Kept for compatibility; prefer Corebill-Signature
X-Webhook-EventEvent type (e.g. invoice.paid)
X-Webhook-IdThe destination ID (wh_...)
X-Event-IdThe event ID. Use it to deduplicate
X-Webhook-DeliveryThe delivery ID (new on every attempt)
X-Webhook-AttemptAttempt number (1 to 5)
X-Webhook-TimestampSame timestamp used in the signature
User-AgentCorebill-Webhooks/1.0
Custom headersThe headers you configure on the destination (for example an API token of your system)

Verifying the Signature

Always verify the signature before trusting a webhook. Use the raw request body (do not parse and re-stringify it), compare in constant time and reject timestamps older than 5 minutes to block replays.

1import crypto from 'node:crypto'
2import express from 'express'
3
4const app = express()
5const SECRET = process.env.COREBILL_WEBHOOK_SECRET // whsec_...
6
7// Corebill signs "<t>.<raw body>": use express.raw, not express.json
8app.post('/webhooks/corebill', express.raw({ type: 'application/json' }), (req, res) => {
9 const header = req.get('Corebill-Signature') || ''
10 const parts = header.split(',')
11 const t = parts.find((p) => p.startsWith('t='))?.slice(2)
12 const signatures = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3))
13
14 const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${req.body}`).digest('hex')
15 const valid = signatures.some(
16 (s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected))
17 )
18 // Reject invalid signatures or ones older than 5 minutes
19 if (!valid || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400)
20
21 const event = JSON.parse(req.body)
22 // Respond fast and process later; deduplicate by event.id
23 console.log(event.event, event.data.id)
24 res.sendStatus(200)
25})
26
27app.listen(3000)

Responding

Return a 2xx status code within 10 seconds. Anything else, a redirect or a timeout counts as a failure. Keep handlers fast: store the event, answer 200 and process it in the background.

Retries

AttemptWhen
1Immediately
21 minute after the first failure
35 minutes later
430 minutes later
52 hours later

After 5 failed attempts the delivery stays as failed. You can resend any delivery from the destination log; a resend replaces a pending automatic retry.

Idempotency

The same event can arrive more than once (retries, manual resends). Use the envelope id (or the X-Event-Id header) as an idempotency key: if you already processed evt_..., ignore it.

Secret Rotation

Rotate the signing secret from the destination detail. For the next 24 hours every event carries two v1 signatures (new and previous secret), so you can deploy the new secret without losing events. Accept the event if any v1 matches.

Testing

  1. Open Settings > Developers > Webhooks and pick a destination
  2. Click Send test event and choose any event of the catalog
  3. Corebill sends the example payload with "test": true and shows the status code and response time. Test events are not retried

The delivery log keeps every attempt with the payload, the headers sent (custom header values are hidden), the response and the response headers.

Building the endpoint on your machine? Run corebill listen --forward-to localhost:3000/webhooks/corebill and corebill trigger invoice.paid: events reach your local server signed like real webhooks, no public URL needed. See the Events page.