Webhooks
BigLedger can POST to a URL of yours when something changes in a tenant, so your system does not
have to poll for it.
This page documents what that mechanism does and — at least as important — what it does not do. Read the guarantees section before you design around it.
What it is
You register a subscription: a URL, a topic, and optionally one HTTP header for the
receiver to authenticate with. When a matching change happens in that tenant, BigLedger POSTs a
JSON body to your URL. Every attempt is written to a delivery log.
Topics are strings such as CUSTOMER_CREATED, INTERNAL_SALES_INVOICE_CREATED or
INVENTORY_UPDATED. A subscription points at one topic. For several topics, register several
subscriptions.
What it does not do
Four absences, all of which change how you should build.
500, or times out, or is
mid-deploy, the event is gone — there is no backoff, no dead-letter queue and no replay endpoint.
Nothing tells you it was lost. Webhooks are a hint that something changed, not a delivery
guarantee. Pair every subscription with a periodic reconciliation pull on updated_date_from
(see Getting Started).
That pull is what makes your integration correct; the webhook only makes it fast.Setting one up
Step 1 — make sure the topic exists
A subscription points at a topic record, not at a topic string, so the record has to exist first.
curl -s "https://api-etl.akaun.com/core2/tnt/dm/webhook-topic/query?limit=200" \
-H "AccessId: $BLG_ACCESS_ID" -H "AccessKey: $BLG_ACCESS_KEY" -H "tenantCode: $BLG_TENANT_CODE"If the list is empty, the tenant has never been seeded. Seeding is one call and it is idempotent:
curl -s -X POST "https://api-etl.akaun.com/core2/tnt/dm/webhook-topic/populate-default" \
-H "AccessId: $BLG_ACCESS_ID" -H "AccessKey: $BLG_ACCESS_KEY" -H "tenantCode: $BLG_TENANT_CODE"That inserts the platform’s built-in list of 54 topics. Take the guid of the one you want
from the query above.
The built-in list is a seed, not the full set — and it does not match what actually fires.
More than seventy topic codes that BigLedger genuinely emits are absent from the built-in list, so
populate-default never creates them and nothing in the product lists them. Conversely, at least
one seeded topic can never fire: the built-in list spells the branch-deleted topic BRANCH_DELTED
while the platform emits BRANCH_DELETED, so a subscription to the offered topic receives
nothing, forever.
If the event you need is not in the seeded list, do not assume it does not exist — open an issue
at BigLedger-Support/public and ask for the exact topic code, then create
the topic record yourself with POST /core2/tnt/dm/webhook-topic. The code must match the
platform’s string exactly; there is no fuzzy matching and no error if it never matches.
Step 2 — create the subscription
curl -s -X POST "https://api-etl.akaun.com/core2/tnt/dm/webhook-subscription" \
-H "AccessId: $BLG_ACCESS_ID" -H "AccessKey: $BLG_ACCESS_KEY" \
-H "tenantCode: $BLG_TENANT_CODE" -H "Content-Type: application/json" \
-d '{
"bl_webhook_subscription_hdr": {
"topic_hdr_guid": "<guid of the topic record>",
"url": "https://hooks.gadgetsphere.example/blg/sales-invoice-created",
"auth_header_name": "X-GadgetSphere-Token",
"auth_header_value": "<a long random string>",
"status": "ACTIVE"
}
}'| Field | Notes |
|---|---|
topic_hdr_guid | The topic record’s GUID, not the topic code |
url | Maximum 255 characters. Not validated — a typo produces a subscription that silently never delivers. |
auth_header_name | A valid HTTP header name. No spaces. |
auth_header_value | The shared secret. Maximum 255 characters. |
status | ACTIVE. Only DELETED stops delivery; there is no pause. |
auth_header_name column. It never sets auth_header_value, and BigLedger only sends the
header when both halves are present. A subscription created in the user interface therefore
arrives at your endpoint with no authentication header at all. The screen also makes the URL
read-only once saved, so changing a destination means deleting and recreating. The API sets all
four fields properly.Managing subscriptions:
| Create | POST /core2/tnt/dm/webhook-subscription |
| Update | PUT /core2/tnt/dm/webhook-subscription |
| Delete | DELETE /core2/tnt/dm/webhook-subscription/{guid} |
| List | GET /core2/tnt/dm/webhook-subscription |
| Read one | GET /core2/tnt/dm/webhook-subscription/{guid} |
| Filter | GET /core2/tnt/dm/webhook-subscription/query |
These need the tenant permissions API_TNT_DM_WEBHOOK_SUBSCRIPTION_CREATE, _UPDATE, _DELETE
and _READ, or tenant owner or administrator rank. Step 1 additionally needs
API_TNT_DM_WEBHOOK_TOPIC_READ to list topics and API_TNT_DM_WEBHOOK_TOPIC_CREATE to seed or
create them. Ask for all of them when you ask for the integration user — a key that can read
records cannot manage subscriptions unless it was granted these too.
auth_header_value is stored and returned in clear text, and it is a filterable query
parameter. Anyone who can read subscriptions in the tenant can read the secret. Use a value that
is worth nothing except to your endpoint, and rotate it on the same schedule as your access key.What success looks like
Thirty seconds, and worth spending before you write any receiving code.
- Point the subscription at a request-capture service (
https://webhook.site/…or your ownnc -l 8080) instead of your real endpoint. - Make the change the topic names — for
CUSTOMER_CREATED, create one customer in the tenant. - Within a second or two you should see one
POST,Content-Type: application/json, carrying the header name and value you set, and a body whose top-level key is a table name such asbl_fi_mst_entity_hdr.
If nothing arrives, work backwards in this order: the topic record exists and its topic_code
matches the platform’s string exactly; the subscription’s status is ACTIVE; the url has no
typo (it is not validated, so a wrong one fails silently and forever); the topic is one the
platform actually emits, not merely one populate-default seeded. If the POST arrives but has
no authentication header, the subscription was created in the Web Hook screen rather than through
the API — delete it and create it again with both header halves.
What arrives at your endpoint
POST /blg/sales-invoice-created HTTP/1.1
Host: hooks.gadgetsphere.example
Content-Type: application/json
X-GadgetSphere-Token: <your auth_header_value>
{ "bl_fi_generic_doc_hdr": { … }, "bl_fi_generic_doc_line": [ … ] }Always POST. Always application/json. The method is not configurable.
The body is the record itself, in the same container shape the Data API returns — and nothing else.
true, and a couple send an English
sentence with Content-Type: application/json. Parse defensively.Your endpoint should return quickly — acknowledge, queue, and process asynchronously. BigLedger allows 60 seconds to establish the connection and then waits indefinitely for your response, and delivery for the whole platform runs on a small shared pool. An endpoint that accepts a connection and then hangs is worse than one that fails fast.
Topics
POST /core2/tnt/dm/webhook-topic/populate-default seeds 54 topics. The ones most integrations
use:
| Area | Topics |
|---|---|
| Entities | CUSTOMER_CREATED · CUSTOMER_UPDATED · CUSTOMER_DELETED · SUPPLIER_CREATED · SUPPLIER_UPDATED · SUPPLIER_DELETED · EMPLOYEE_CREATED · EMPLOYEE_UPDATED · EMPLOYEE_DELETED |
| Items | FINANCIAL_ITEM_CREATED · FINANCIAL_ITEM_UPDATED · FINANCIAL_ITEM_DELETED · INVENTORY_UPDATED |
| Sales documents | INTERNAL_SALES_ORDER_CREATED · INTERNAL_SALES_ORDER_UPDATED · INTERNAL_SALES_ORDER_DELETED · INTERNAL_SALES_INVOICE_CREATED · INTERNAL_SALES_RETURN_CREATED · INTERNAL_SALES_RETURN_UPDATED · INTERNAL_SALES_RETURN_DELETED |
| Organisation | COMPANY_CREATED · COMPANY_UPDATED · BRANCH_CREATED · BRANCH_UPDATED · LOCATION_CREATED · LOCATION_UPDATED · LOCATION_DELETED |
| Membership | MEMBERSHIP_CARD_CREATED · MEMBERSHIP_CARD_UPDATED · MEMBERSHIP_CARD_DELETED · MEMBERSHIP_CLASS_CREATED · MEMBERSHIP_POINTS_TXN_CREATED |
| Other | VOUCHER_CREATED · VOUCHER_UPDATED · VOUCHER_DELETED · INQUIRY_CREATED · INQUIRY_UPDATED · INQUIRY_DELETED · PAYMENT_CONFIG_CREATED · PAYMENT_CONFIG_UPDATED · PAYMENT_CONFIG_DELETED |
Some seeded topics never fire, because the code path they name does not emit them — the supplier
sales-order and supplier sales-return topics are the clearest example. And document topics follow
the pattern <SERVER_DOC_TYPE>_CREATED / _UPDATED / _DELETED, so document types beyond the
seeded handful do emit events; their topic records just have to be created by hand.
Before you build on a topic, test it. Subscribe, make the change in the tenant, and confirm your endpoint is called. A topic that is registered but never fires looks identical to a quiet system.
How to build on this
The pattern that works:
- Subscribe to the topics you care about, one URL each.
- Treat every delivery as “something may have changed” — take the GUID from the body, and
re-read the record through
…/etl-ep/{guid}before acting. - Reconcile on a schedule with an
updated_date_frompull. This catches everything the webhook dropped, and it is the part you must not skip. - Log every delivery on your side, with the raw body. The platform’s own log is not readable.
- Return
200fast. Queue the work.
The webhook makes your integration responsive. The reconciliation pull makes it correct. You need both.