Webhook events and payloads
The request BillPro sends, the event document and an example payload
Each webhook is an HTTPS POST request with a JSON body. The body describes one order event. This page shows the request and the fields you can rely on.
The request
POST /billpro/webhooks HTTP/1.1
Host: erp.example.com
Content-Type: application/json
X-BillPro-Signature: t=1768478400,v1=<64 lowercase hex characters>
X-BillPro-Delivery: 3f2b1c88-9d4e-4a7b-9f01-2c6d5e8a1b34
{ ...event document... }| Header | Value |
|---|---|
Content-Type | application/json |
X-BillPro-Signature | The timestamp and signature. See Verify webhook signatures. |
X-BillPro-Delivery | The delivery ID. The same on every retry and redelivery. Use it to skip repeats. |
Treat the delivery ID as an opaque string. Don't rely on its format.
The event document
This is an example order.active document. The order in it is BillPro's sample order, so its details are made up.
{
"id": "3f2b1c88-9d4e-4a7b-9f01-2c6d5e8a1b34",
"event": "order.active",
"occurred_at": "2026-01-15T12:00:00.000Z",
"data": {
"previous_status": "pending",
"status": "active",
"order": {
"id": 1001,
"merchant_invoice_id": "SAMPLE-0001",
"order_type": {
"value": "single_payment",
"label": "Single payment"
},
"status": {
"value": "active",
"label": "Active"
},
"currency": "GBP",
"channel": "ECOM",
"description": "Sample order for a webhook test ping",
"collect_initial_payment": true,
"card_details_saved": "No",
"initial_amount": "50.00",
"customer_id": 2001,
"customer": {
"display_customer_id": "SAMPLE-CUST-0001",
"merchant_customer_id": "SAMPLE-CUST-0001",
"first_name": "Sample",
"last_name": "Customer",
"full_name": "Sample Customer",
"email": "[email protected]",
"phone": "+44 20 7946 0000",
"date_of_birth": null,
"street": "1 Sample Street",
"city": "London",
"state": null,
"postal_code": "EC1A 1BB",
"country": "GB",
"country_name": "United Kingdom"
},
"order_products": [
{
"id": 3001,
"product_id": 4001,
"name": "Sample product",
"selection_text": null,
"options": null,
"qty": 1,
"amount": "50.00",
"products_total": "50.00",
"discount_amount": "0.00",
"fee_amount": "0.00",
"tax_amount": "0.00",
"subtotal_amount": "50.00",
"product_discount_id": null,
"product_fee_id": null,
"product_tax_id": null
}
],
"summary": {
"products_total": "50.00",
"fee_total": "0.00",
"discount_total": "0.00",
"tax_total": "0.00",
"tax_type": null,
"sub_total": "50.00",
"initial_amount": "50.00",
"payment_amount": "50.00"
},
"tag_list": [],
"created_at": "2026-01-15T12:00:00.000Z",
"updated_at": "2026-01-15T12:00:00.000Z",
"status_updated_at": "2026-01-15T12:00:00.000Z"
}
},
"links": {
"order": "https://app.bill.pro/api/v2/profiles/7/orders/1001",
"payments": "https://app.bill.pro/api/v2/profiles/7/orders/1001/payments",
"scheduled_payments": "https://app.bill.pro/api/v2/profiles/7/orders/1001/scheduled_payments"
}
}| Field | Description |
|---|---|
id | The event ID. It matches the X-BillPro-Delivery header. |
event | The event name, such as order.active. |
occurred_at | When the order reached the new status, in ISO 8601 UTC with milliseconds. Use it to put events in order. |
data.previous_status | The status the order moved from. null on order.draft for a new order, on a test event, and when BillPro couldn't tell the previous status. |
data.status | The status the order moved to. |
data.order | The order at the moment of the event. See below. |
links.order | API URL of the order. Fetch it for the current state. |
links.payments | API URL of the payments taken on the order. |
links.scheduled_payments | API URL of the order's payment schedule. |
data.order holds the order as it was when the event was raised:
- The order details, such as
id,merchant_invoice_id,order_type,status,currency,channelanddescription. customer: the customer details saved on the order.order_products: every line on the order, with its amounts.summary: the totals for the order.tag_list, and thecreated_at,updated_atandstatus_updated_attimes.
order_type and status are objects with a value to match on and a label to show. Amounts are strings, such as "50.00". Don't rely on two decimal places: some show as "100.0".
The example is a single-payment order. Other order types carry more fields:
- Instalment, subscription and metered orders add
auto_retryandduration.durationis a number of payments, orOrder Good Until Cancelled. - Instalment and subscription orders add
charging_interval,repeated_amountandrepeated_start. - Subscription and metered orders add
good_until_cancel. Metered orders addinterval. - An instalment's
summaryaddsbalance,durationandrecurring_amount. - A subscription's
summaryhasinitial_fee,initial_discount,recurring_fee,recurring_discountandrecurring_amountinstead offee_totalanddiscount_total. initial_amountis left out of the order when the order doesn't collect a first payment.summary.initial_amountis always there.
Key order isn't fixed. Parse the JSON. Don't compare it byte for byte with the example on this page.
To see the full document BillPro sent for any delivery, open the endpoint, click the Delivery ID in Recent deliveries, and look at Signed payload. It's the document your endpoint received, laid out for reading. You can't use it to check a signature.
Verify before you parseCheck the signature against the raw body first. Parse the JSON only after the signature matches.
Events
| Event | Sent when the order becomes |
|---|---|
order.draft | Draft: created, nothing collected yet. |
order.pending | Pending: waiting for the first payment. |
order.registered | Registered: the card is saved and the first payment is waiting. |
order.active | Active: collecting payments on its schedule. |
order.paused | Paused: paused by the merchant. Payments restart when it's resumed. |
order.failed | Failed: a payment was declined but can be retried. |
order.suspended | Suspended: stopped after a failed payment, on an order that had already collected a payment. |
order.rejected | Rejected: declined because of a card problem. Charging the same card again won't help. |
order.review | Review: needs a manual check, for example after an overpayment. |
order.complete | Complete: nothing more is due. |
order.cancelled | Cancelled: cancelled. Nothing more is collected. |
order.test | Never sent for an order. Sent only when you click Send test event. |
The data.status value uses the same names as the order status in the API, such as active.
For how these statuses apply to each order type, see Webhook events and order statuses.
Test events
A test event has the event name order.test. It has the same fields as any other event. data.previous_status is null, and data.status is the status of the order it carries.
The delivery isn't linked to an order, but data.order still holds one:
- Until the endpoint has accepted one delivery, it's a sample order with made-up details, like the one shown above.
- After that, it's the most recent real order on the profile.
With the sample order, the links point to an order that doesn't exist on your profile.
Reply with a 2xx code and take no action, even though the order may be real.
This is a real order.test document with the sample order. data.order is shortened here. It's the same sample order as in the example above.
{
"id": "a2c668c0-4e32-409d-a327-55f8c2e6a38c",
"event": "order.test",
"occurred_at": "2026-09-10T13:50:25.369Z",
"data": {
"previous_status": null,
"status": "active",
"order": {
"id": 1001,
"merchant_invoice_id": "SAMPLE-0001",
"order_type": {
"value": "single_payment",
"label": "Single payment"
},
"status": {
"value": "active",
"label": "Active"
},
"description": "Sample order for a webhook test ping"
}
},
"links": {
"order": "https://app.bill.pro/api/v2/profiles/7/orders/1001",
"payments": "https://app.bill.pro/api/v2/profiles/7/orders/1001/payments",
"scheduled_payments": "https://app.bill.pro/api/v2/profiles/7/orders/1001/scheduled_payments"
}
}Events added later
BillPro may add new order events. An endpoint with Enable all order events turned on receives them automatically.
Make your handler reply 2xx to events it doesn't recognise, and ignore them. Otherwise BillPro retries them.
Updated 14 days ago
Recommended reading
See what each status means for each order type