Webhooks

Get notified the moment a lodgement changes status, instead of polling. Register an HTTPS endpoint and we'll push you a signed event.

Overview

Webhooks let you subscribe to status changes on your lodgements instead of polling GET /status endpoints. When a subscribed request transitions into a notifiable status, we send an HTTP POST with a small JSON event to your endpoint and retry on failure.

Webhook payloads are deliberately minimal: a nudge to fetch full detail, not a replacement for it. Treat a webhook as "something changed, go check GET /status", not as the source of truth itself.

Registering an Endpoint

Register endpoints from the API Keys page in your dashboard (each API key has its own Webhooks panel, under the Live or Test tab), or programmatically:

Method Path Purpose
GET /api/v2/webhooks List endpoints for this key (secret omitted)
POST /api/v2/webhooks Create an endpoint
PATCH /api/v2/webhooks/{id} Update URL, event types, or enable/disable
DELETE /api/v2/webhooks/{id} Remove an endpoint
POST /api/v2/webhooks/{id}/rotate-secret Invalidate the current signing secret and issue a new one

Swap /api/v2/ for /api/test/v2/ to manage test-environment endpoints. Endpoints are scoped to the API key (and therefore environment) they were created under, so a test key's webhooks only ever fire for test-key lodgements. Secret keys only; same rate limit and card requirement as other lodgement endpoints.

Create an endpoint:

curl https://businessapi.com.au/api/v2/webhooks \
-H "Authorization: Bearer bapi_sk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/webhooks/bapi", "eventTypes": ["company_registration.status_changed"]}'

The response includes a secret field (whsec_...). Store it immediately. It's shown once, on create and again on rotation, and is never returned by GET.

  • url must be HTTPS and resolve to a public IP: private, loopback, link-local, and carrier-grade NAT ranges are rejected, both when you register the endpoint and again on every delivery
  • eventTypes is a non-empty array; use ["*"] to subscribe to everything
  • Up to 10 endpoints per API key
  • Disabling an endpoint (enabled: false via PATCH) stops deliveries immediately and resets its failure count when re-enabled

Events

A delivery fires when a request's status changes into one of the statuses below. Each transition is delivered independently. A request that moves from manual review to finished fires two separate events.

Event Interaction Notifiable statuses
company_registration.status_changed company-registration finished, rejected, validation failed, manual review, error
abn_registration.status_changed abnreg.lodge finished, rejected, manual review, error
business_name_registration.status_changed business-name-registration finished, rejected, manual review, error
business_name_renewal.status_changed business-name-renewal finished, rejected, manual review, error
ppsr.alert.grantor_registration_changed N/A - PPSR mailbox notification, not status-driven Fires whenever PPSR notifies us of a new registration against an organisation you're watching via an active PPSR alert subscription

ppsr.alert.grantor_registration_changed has a different payload shape to the others above - it has no requestId or interaction, and instead carries organisationNumberType, organisationNumber, and registrationSummary alongside the common event, timestamp, and deliveryId fields.

Payload

Every delivery is a POST with a JSON body and Content-Type: application/json:

{
"event": "company_registration.status_changed",
"requestId": 30172,
"interaction": "company-registration",
"status": "rejected",
"timestamp": "2026-07-31T01:40:11+00:00",
"entityName": "EXAMPLE PTY LTD",
"acn": "123456789",
"abn": "12345678901",
"deliveryId": "wd_9f2a1c7e..."
}
Field Type Notes
event string One of the event names above
requestId integer The request id (pair it with GET /status for full detail)
interaction string The interaction type, e.g. company-registration
status string The status this transition landed on
timestamp string ISO 8601, UTC
entityName / acn / abn string, optional Omitted entirely (not sent as null) when not applicable, e.g. no acn on business name events
deliveryId string Stable across retries of the same delivery, so use it to dedupe

Verifying Signatures

Every delivery includes an X-Bapi-Signature header so you can confirm it actually came from us:

X-Bapi-Signature: t=1785552011,v1=5257a869e7bfa8...

Recompute the HMAC-SHA256 over {timestamp}.{raw_request_body} using your endpoint's secret, and compare it to v1 with a constant-time comparison:

$header = $_SERVER['HTTP_X_BAPI_SIGNATURE'];
parse_str(str_replace(',', '&', $header), $parts);
$timestamp = $parts['t'];
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $yourSecret);
 
if (!hash_equals($expected, $parts['v1'])) {
// reject - signature mismatch
}
 
// Optional: reject if $timestamp is too old, to guard against replay

Use the raw, unparsed request body. Re-serialising JSON before hashing can change byte-for-byte formatting and break the comparison. If you rotate your secret, both the old and new secret should be tried until you've confirmed the new one is live everywhere you verify.

Responding

  • Return any 2xx status code to acknowledge (the response body isn't inspected)
  • Respond within 15 seconds; slower responses are treated as a failed attempt
  • Redirects are not followed: a 3xx counts as a failed attempt, so point the URL at its final destination
  • Do the actual processing asynchronously on your side and return quickly. Don't make us wait on your downstream work

Retries & Failure Handling

A non-2xx response, a timeout, or a connection error is a failed attempt and is retried with backoff, up to 6 attempts total:

Attempt Delay since previous attempt
1immediate
230 seconds
35 minutes
415 minutes
51 hour
66 hours

If attempt 6 also fails, the delivery is marked permanently failed. Consecutive failures accumulate per endpoint (any 2xx resets the count to zero); at 20 consecutive failures the endpoint is automatically disabled and we email the account owner. Re-enable it from the dashboard or via PATCH once your endpoint is healthy again (this also resets the failure count).

Webhooks are a convenience layer, not a guarantee of instant delivery: retries can stretch a permanently-failed delivery out to roughly 24 hours after the first attempt. If your integration depends on knowing the current status right now, call GET /status directly rather than waiting on a webhook.