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... }
HeaderValue
Content-Typeapplication/json
X-BillPro-SignatureThe timestamp and signature. See Verify webhook signatures.
X-BillPro-DeliveryThe 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"
  }
}
FieldDescription
idThe event ID. It matches the X-BillPro-Delivery header.
eventThe event name, such as order.active.
occurred_atWhen the order reached the new status, in ISO 8601 UTC with milliseconds. Use it to put events in order.
data.previous_statusThe 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.statusThe status the order moved to.
data.orderThe order at the moment of the event. See below.
links.orderAPI URL of the order. Fetch it for the current state.
links.paymentsAPI URL of the payments taken on the order.
links.scheduled_paymentsAPI 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, channel and description.
  • 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 the created_at, updated_at and status_updated_at times.

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_retry and duration. duration is a number of payments, or Order Good Until Cancelled.
  • Instalment and subscription orders add charging_interval, repeated_amount and repeated_start.
  • Subscription and metered orders add good_until_cancel. Metered orders add interval.
  • An instalment's summary adds balance, duration and recurring_amount.
  • A subscription's summary has initial_fee, initial_discount, recurring_fee, recurring_discount and recurring_amount instead of fee_total and discount_total.
  • initial_amount is left out of the order when the order doesn't collect a first payment. summary.initial_amount is 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 parse

Check the signature against the raw body first. Parse the JSON only after the signature matches.


Events

EventSent when the order becomes
order.draftDraft: created, nothing collected yet.
order.pendingPending: waiting for the first payment.
order.registeredRegistered: the card is saved and the first payment is waiting.
order.activeActive: collecting payments on its schedule.
order.pausedPaused: paused by the merchant. Payments restart when it's resumed.
order.failedFailed: a payment was declined but can be retried.
order.suspendedSuspended: stopped after a failed payment, on an order that had already collected a payment.
order.rejectedRejected: declined because of a card problem. Charging the same card again won't help.
order.reviewReview: needs a manual check, for example after an overpayment.
order.completeComplete: nothing more is due.
order.cancelledCancelled: cancelled. Nothing more is collected.
order.testNever 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.


Recommended reading

See what each status means for each order type

Did this page help you?