Platform

Events

Poll the events of a company, list the event types and test webhooks locally with the CLI.

Overview

Every change that fires a webhook is also stored as an event (evt_...). The Events API lets you read them: catch up after downtime, reconcile your system, or build a local integration without a public URL. The data of an event is exactly the data your webhook endpoint receives for that type.

Events are part of the core API (no module needed), but types that belong to an optional module are only available while that module is on.

The Event Object

JSON
1{
2 "id": "evt_8f2c1a9b7d3e4f56a1b2c3d4e5f6a7b8",
3 "object": "event",
4 "type": "invoice.paid",
5 "created_at": "2026-10-11T15:30:00.000Z",
6 "resource_type": "invoice",
7 "resource_id": "inv_b2c3d4e5f6g7h8i9",
8 "data": {
9 "id": "inv_b2c3d4e5f6g7h8i9",
10 "invoice_number": "INV-2026-000042",
11 "status": "paid",
12 "amount_due": 0
13 },
14 "test": false
15}
Event fieldWebhook envelope field
idid (the same id, use it to deduplicate)
typeevent
created_atcreated_at
datadata
testtest (true for test events; data.test is also true)

Endpoints

MethodPathPermissionDescription
GET/v1/eventsreadList events, newest first (type, created[gte], created[lte], resource_id)
GET/v1/events/{id}readRetrieve an event
GET/v1/events/typesreadEvent types available for the company, with an example payload
POST/v1/events/testwriteCreate a test event (not delivered to webhooks)

List Events

1# Every invoice event since October 1st
2curl -G "https://api.corebill.io/v1/events" \
3 -H "Authorization: Bearer sk_live_your_api_key" \
4 --data-urlencode "type=invoice.*" \
5 --data-urlencode "created[gte]=2026-10-01" \
6 -d limit=100
7
8# The history of one invoice
9curl "https://api.corebill.io/v1/events?resource_id=inv_b2c3d4e5f6g7h8i9" \
10 -H "Authorization: Bearer sk_live_your_api_key"
  • type is an exact type (invoice.paid), a prefix wildcard (invoice.*) or *. An unknown type returns 400 invalid_type
  • created[gte] and created[lte] accept ISO 8601 (2026-10-11 or 2026-10-11T15:30:00Z) or unix seconds. URL-encode the brackets if your HTTP client requires it
  • Results use cursor pagination (limit, starting_after, ending_before) and are always newest first

To poll for new events, request created[gte]=&limit=100, skip the ids you already handled and process the rest oldest first. This is exactly what corebill listen does.

Event Types

GET /v1/events/types returns the types available for the company (types of disabled modules are left out). It is not paginated. Each entry has a label, a description, the module that owns it (null for core events), whether it can trigger automations, and an example of its data.

JSON
1{
2 "object": "list",
3 "url": "/v1/events/types",
4 "data": [
5 {
6 "object": "event_type",
7 "type": "work_order.created",
8 "group": "work_order",
9 "group_label": "Work orders",
10 "module": "work_orders",
11 "label": "Work order created",
12 "description": "A new work order was opened.",
13 "trigger": true,
14 "example": { "id": "wo_a1b2c3d4e5f6", "order_number": "WO-000057" }
15 }
16 ],
17 "has_more": false,
18 "next_cursor": null
19}

Create a Test Event

POST /v1/events/test stores an event of any available type with its example payload, test: true and resource_type: "cli_test". It shows up in GET /v1/events (so corebill listen picks it up) but it is never delivered to your webhook endpoints and does not run automations.

1curl -X POST "https://api.corebill.io/v1/events/test" \
2 -H "Authorization: Bearer sk_live_your_api_key" \
3 -H "Content-Type: application/json" \
4 -d '{ "type": "invoice.paid" }'

Errors: 400 parameter_missing without type, 400 invalid_type for an unknown type and 400 event_type_unavailable when the type belongs to a module that is turned off.

Test Webhooks Locally

You do not need a public URL to build your webhook endpoint. corebill listen follows the events of the active company and forwards each one to your local server with the same body and Corebill-Signature header as a real webhook, signed with a local secret.

  1. Install the CLI and log in: npm install -g @corebill/cli and corebill login
  2. Start the listener pointing at your local endpoint: corebill listen --forward-to localhost:3000/webhooks/corebill
  3. Copy the whsec_... secret it prints into your .env as COREBILL_WEBHOOK_SECRET. It stays the same for this company on this computer
  4. In another terminal, trigger an event: corebill trigger invoice.paid (or do something real in the dashboard)
Terminal 1
1$ corebill listen --forward-to localhost:3000/webhooks/corebill
2Ready! Listening for events on Acme Inc. Your webhook signing secret is whsec_3f9c0a7e1b2d4c6f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d (^C to quit)
3Forwarding events to http://localhost:3000/webhooks/corebill
4
52026-10-11 14:03:22 --> invoice.paid [evt_8f2c1a9b7d3e4f56a1b2c3d4e5f6a7b8] [test]
62026-10-11 14:03:22 <-- [200] POST http://localhost:3000/webhooks/corebill [evt_8f2c1a9b7d3e4f56a1b2c3d4e5f6a7b8]
72026-10-11 14:05:10 --> payment.received [evt_1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e] pay_7d8e9f0a1b2c
82026-10-11 14:05:10 <-- [200] POST http://localhost:3000/webhooks/corebill [evt_1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e]
Terminal 2
1$ corebill trigger invoice.paid
2Triggered invoice.paid (evt_8f2c1a9b7d3e4f56a1b2c3d4e5f6a7b8). Run `corebill listen` to receive it.

Your endpoint verifies the forwarded request exactly like a production webhook: HMAC-SHA256 of . with the whsec_... secret, compared with the v1 value of Corebill-Signature. In production, use the secret of your webhook destination instead (Settings > Developers > Webhooks).

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)
  • --events invoice.*,payment.received forwards only those types (wildcards allowed)
  • --print-json prints the full event; --print-secret prints the secret and exits
  • If your server is down, the error is shown and the listener keeps running
  • Forwarded requests carry test: true for test events; check it if your handler must ignore them