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 a real 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 a test event, and on an event where 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".
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 byte for byte what your endpoint received.
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. Retrying 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, and data.previous_status is null.
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.
Reply with a 2xx code and take no action, even though the order may be real.
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 about 11 hours ago
Recommended reading
See what each status means for each order type