Skip to content

Data API

Reference for the etl-ep endpoints — the server-to-server half of the BigLedger REST API. If you have not issued an access key yet, start with Getting Started.

Hosts

HostUse it for
https://api-etl.akaun.comData traffic. The ingress BigLedger’s own integrations use.
https://api.akaun.comSigning in and managing access keys

Both serve the same paths. Everything on this page is under api-etl.akaun.com.

Path grammar

/core2/{tnt|platform}/{dm|ms|cm}/<module>[/<sub-module>]/<resource>/<access-endpoint>

Read a path off this page or the API Reference rather than composing one: the scope and layer segments are usually present but 65 of the 1,191 etl-ep routes sit directly under /core2/dm/… or /core2/ms/…, one uses the cm layer, and most paths carry a sub-module (erp/fi/fi-items, erp/gen-doc/{docType}, alg/cc/actions) rather than the bare module-plus-resource shape below.

SegmentMeaning
tntTenant-scoped. Needs a tenantCode header. This is where business data lives.
platformPlatform-scoped: identity, applets, tenants. No tenantCode.
dmDomain model — the normal, current surface
msMicro-service — older, narrower endpoints
<module>erp, crm, inv, fi, cms, pgw, fm and others
<resource>The thing you are addressing
<access-endpoint>Who the endpoint is for

Access endpoints:

etl-epServer-to-server integrations. Yours.
backoffice-epA staff user working in the BigLedger shell
login-epA signed-in user acting on their own records
login-entity-epAn external party — a customer or supplier — acting on their own records
public-epDeliberately unauthenticated

Almost every business resource has both a backoffice-ep and an etl-ep variant, calling the same handler with the same permission checks. Build against etl-ep: it is the variant maintained for unattended callers, and it will not move when the user interface changes.

An unrecognised path returns 417, not 404, with "code": "SERVER_GENERAL_UNKNOWN_ERROR" and a message beginning No static resource. If you see that, the path is wrong.

The six operations

Most resources on this surface expose the same shape. Taking the item master as the example:

OperationRequest
CreatePOST /core2/tnt/dm/erp/fi/fi-items/etl-ep
UpdatePUT /core2/tnt/dm/erp/fi/fi-items/etl-ep
DeleteDELETE /core2/tnt/dm/erp/fi/fi-items/etl-ep/{guid}
List allGET /core2/tnt/dm/erp/fi/fi-items/etl-ep
Read oneGET /core2/tnt/dm/erp/fi/fi-items/etl-ep/{guid}
Filter and pageGET /core2/tnt/dm/erp/fi/fi-items/etl-ep/query

A handful of resources add extras, and they are rarer than they look: …/etl-ep/multi for batch create exists on five resources (contacts in both crm and erp, employees, financial items, and generic documents), …/etl-ep/query/count on two (entities), and …/etl-ep/query/snapshot on twelve. File attachment endpoints appear where the resource carries files. Check before you plan around one.

Do not assume all six exist on every resource. They usually do, but there are real gaps and they are not signposted. Entities are the one you will hit first: POST, PUT, GET and …/query all work at entity/{entityType}/etl-ep, but there is no delete on the typed path — delete is only on the type-agnostic DELETE /core2/tnt/dm/erp/entity/etl-ep/{guid}, and there is also PUT /core2/tnt/dm/erp/entity/etl-ep/inactivation/{guid} if you want to retire a record rather than remove it. Contacts are narrower still: on this surface they offer only POST …/contacts/etl-ep/multi. Only about a third of etl-ep resources carry all six operations, so probe the one you need before you design around it — a path that is not routed at all returns 417. Note that this does not distinguish a bad {docType} or {entityType}: the route exists, so an unknown value comes back as 404 from the handler.

What is on the surface

1,044 endpoint methods across 173 controllers, covering roughly 306 resources. The groups you are most likely to need:

Master data

ResourcePath under /core2/tnt/dm/erp/
Customers, suppliers, employees, merchantsentity/{entityType}/etl-ep
All entities regardless of typeentity/etl-ep
Financial items (the sellable/purchasable item master)fi/fi-items/etl-ep
Inventory itemsinv/inv-items/etl-ep
Companiescompanies/etl-ep
Branchesbranches/etl-ep
Locationslocations/etl-ep
Chart of accounts, ledgers and cashbookschart-of-acc/etl-ep, fi/ledgers/etl-ep, cashbooks/etl-ep
Contacts (batch create only)contacts/etl-ep/multi
Labels and label listslabels/etl-ep, label-lists/etl-ep
Pricing scheme linkspricing-scheme-link/etl-ep
Credit limits and credit terms (write only — POST and PUT; no read, no /query, no delete on this surface)fi/entity-sales-credit-limit/etl-ep and siblings

{entityType} is one of entities, customers, suppliers, employees, merchants — so a customer read is GET /core2/tnt/dm/erp/entity/customers/etl-ep/query.

Business documents

Every document type goes through one controller, keyed by a URL slug:

OperationRequest
List the document types this platform knowsGET /core2/tnt/dm/erp/gen-doc/etl-ep/server-doc-types
CreatePOST /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep
Create manyPOST /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/multi
UpdatePUT /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep
DeleteDELETE /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/{guid}
Read oneGET /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/{guid}
Filter and page, one typeGET /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/query
Filter and page, all typesGET /core2/tnt/dm/erp/gen-doc/etl-ep/query
VoidPUT /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/void/{guid}
Print to PDFGET /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/print-jasper-pdf/{guid}

There are 97 document-type slugs. The common ones:

{docType} slugDocument type
internal-sales-invoicesINTERNAL_SALES_INVOICE
internal-sales-cashbillsINTERNAL_SALES_CASHBILL
internal-sales-ordersINTERNAL_SALES_ORDER
internal-sales-returnsINTERNAL_SALES_RETURN
internal-sales-credit-notesINTERNAL_SALES_CREDIT_NOTE
internal-sales-debit-notesINTERNAL_SALES_DEBIT_NOTE
internal-purchase-ordersINTERNAL_PURCHASE_ORDER
internal-purchase-invoicesINTERNAL_PURCHASE_INVOICE
internal-purchase-goods-received-notesINTERNAL_PURCHASE_GOODS_RECEIVED_NOTE
internal-stock-adjustmentINTERNAL_STOCK_ADJUSTMENT
internal-payment-vouchersINTERNAL_PAYMENT_VOUCHER
internal-receipt-vouchersINTERNAL_RECEIPT_VOUCHER
server-doc-types returns the type names, not the URL slugs. It gives you INTERNAL_SALES_INVOICE; the URL wants internal-sales-invoices. Use it to confirm a type exists, then take the slug from the table above. If the type you need is not in that table, open an issue at BigLedger-Support/public and ask for its slug — do not guess the pluralisation, because it is not consistent (internal-stock-adjustment is singular).

Other modules

crm/ carries membership cards, classes, points transactions and contacts. alg/cc/ carries the contact-centre resources — agents, conversations, channels, tasks. cms/ carries website and post resources. fm/ carries files and labels. pgw/ carries payment providers and payment transactions. All follow the same six-operation shape.

If a resource you need is not listed here, it may still exist — ask for the current route rather than guessing it.

Request and response format

Headers

AccessId: <10 characters>
AccessKey: <25 characters>
tenantCode: <the tenant>
Content-Type: application/json      # on POST and PUT

Full credential detail in Authentication.

Envelopes

Single object or non-paged list:

{ "code": "OK_RESPONSE", "data": { }, "message": "" }

Paged (…/query):

{
  "totalRecords": 5217,
  "offset": 0,
  "limit": 200,
  "code": "OK_RESPONSE",
  "message": "",
  "data": [ ]
}

code is a platform response code, not an HTTP status. Match on it.

Responses are streamed, not buffered. A large page arrives progressively and the connection stays open for the duration — parse incrementally rather than waiting for a complete body.

Responses are gzip-compressed above 1 KB. Send Accept-Encoding: gzip.

Bodies

Write bodies are containers: a header object plus arrays of related rows, keyed by the underlying table names.

{
  "bl_fi_generic_doc_hdr":  { },
  "bl_fi_generic_doc_line": [ ],
  "bl_fi_generic_doc_ext":  [ ],
  "bl_fi_generic_doc_link": [ ]
}

The same container type comes back from GET, so the reliable way to learn a write body is to read a record and mirror it. Dates are ISO-8601 strings, never epoch numbers.

Key names are not consistently pluralised. Document lines are bl_fi_generic_doc_line (singular); item lines are bl_fi_mst_item_lines (plural). Copy the keys from a real response.

The document chain

The fourth key in that container, bl_fi_generic_doc_link, is the one nothing else on this page explains, and it is the one you will need. It is how a business document says which document it came from — a delivery order pointing back at the sales order it fulfils, an invoice pointing back at the delivery order.

The link is made line to line, not document to document. When BigLedger converts one document into another, it writes one link row for each goods-or-service line it carried across, and each row records the header and the line on both sides.

    flowchart LR
  subgraph SRC["Sales Order — doc 1, the source"]
    direction TB
    SH["bl_fi_generic_doc_hdr<br/>guid = A"]
    SL1["bl_fi_generic_doc_line<br/>guid = A1"]
    SL2["bl_fi_generic_doc_line<br/>guid = A2"]
    SH --- SL1
    SH --- SL2
  end
  subgraph LNK["bl_fi_generic_doc_link"]
    direction TB
    L1["guid_doc_1_hdr = A<br/>guid_doc_1_line = A1<br/>guid_doc_2_hdr = B<br/>guid_doc_2_line = B1<br/>server_doc_type_doc_1_hdr = INTERNAL_SALES_ORDER<br/>server_doc_type_doc_2_hdr = INTERNAL_OUTBOUND_DELIVERY_ORDER<br/>txn_type = KO"]
    L2["guid_doc_1_hdr = A<br/>guid_doc_1_line = A2<br/>guid_doc_2_hdr = B<br/>guid_doc_2_line = B2<br/>same two document types<br/>txn_type = KO"]
  end
  subgraph DST["Delivery Order — doc 2, the target"]
    direction TB
    DH["bl_fi_generic_doc_hdr<br/>guid = B"]
    DL1["bl_fi_generic_doc_line<br/>guid = B1"]
    DL2["bl_fi_generic_doc_line<br/>guid = B2"]
    DH --- DL1
    DH --- DL2
  end
  SL1 --> L1
  SL2 --> L2
  L1 --> DL1
  L2 --> DL2
  

Read the columns as doc 1 is where it came from, doc 2 is where it went. Every link row also denormalises the transaction date and the server document type of both ends, so you can filter a chain without joining back to the documents.

Three things follow, and all three change how you write against this.

The chain table is not on the etl-ep surface. Every other resource on this page has an etl-ep variant; this one does not. The routes are GET /core2/tnt/ms/bl-fi-generic-doc-link/query and GET /core2/tnt/ms/bl-fi-generic-doc-link/{guid} — no access-endpoint segment — with a backoffice-ep twin. The query filters you want are guid_doc_1_hdr and guid_doc_2_hdr (walk forwards and backwards from a document), guid_doc_1_line and guid_doc_2_line (from a single line), and server_doc_type_doc_1_hdr / server_doc_type_doc_2_hdr (every conversion of one type into another). Each also has a plural set form.
There is no foreign key holding the chain together. bl_fi_generic_doc_link has exactly two foreign keys, both to app_login_subject for the created-by and updated-by columns. The four columns that carry the chain — guid_doc_1_hdr, guid_doc_1_line, guid_doc_2_hdr, guid_doc_2_line — are unconstrained. The database will accept a link to a document that does not exist, and it will not stop you deleting the document a link points at. If you write links, check both ends yourself; if you read them, treat a missing target as possible rather than impossible.
Quantity and amount on a link row are written by the caller, not by the conversion. A link row carries quantity_contra, quantity_signum, amount_contra and amount_signum. Document conversion leaves all four null — it records that a line was carried across, not how much of it was consumed. They are filled by the knock-off endpoints (POST /core2/tnt/ms/bl-fi-generic-doc-link/knock-off/backoffice-ep and its draft and quantity-validation siblings). So do not read quantity_contra as “how much of the order has shipped” unless you know the rows you are reading came from a knock-off.

Query parameters

…/query endpoints share a common set, plus per-resource filters.

ParameterDefaultNotes
limit100No enforced maximum. Responses stream, so a big page is a long response.
offset0
orderByrecord guidA column on the record, e.g. updated_date
orderASCASC or DESC; anything else is treated as ASC
calcTotalRecordsfalseCosts an extra COUNT. Send it on the first page only.
status—Filter by record status
excluded_statuses—Exclude statuses
search_word—Free-text search, where the resource supports it
querying_timezoneAsia/Kuala_LumpurThe zone date filters are interpreted in

Most of the resources you will sync also accept, in ISO-8601:

ParameterMeaning
updated_date_from / updated_date_toWhen the row last changed — use this for incremental pulls
created_date_from / created_date_toWhen the row was created
date_txn_from / date_txn_toThe business transaction date, on documents

Not every resource supports them. If a date filter does not change the result count, that resource does not implement it.

Document queries add server_doc_type, guid_comp, guid_branch, hdr_posting_status, doc_entity_hdr_guid, hdr_doc_ccy, and the correlation filters below.

Correlating with your own identifiers

Records carry three fields for your side of the mapping. They are yours to populate; BigLedger never writes them.

Field on the recordQuery parameterUse it for
client_keyhdr_client_keyYour primary key for the record
client_sourcehdr_client_sourceWhich of your systems it came from
client_value—Anything else you need to carry
GET …/gen-doc/internal-sales-invoices/etl-ep/query?hdr_client_source=storefront&hdr_client_key=WEB-2026-0043117
There is no idempotency-key header. Query on hdr_client_key before you create, and set client_key on everything you write. That is the only mechanism for not posting the same order twice.

Limits

Measured from the platform’s own configuration.

Request timeout2 hours for asynchronous request handling
Multipart upload2 GB per file and per request
Response compressiongzip above 1 KB
Page sizeDefaults to 100; no maximum enforced
Batch createNo maximum enforced on …/etl-ep/multi
Pace yourself. The platform returns 429 when PostgreSQL reports that it has run out of connection slots — an overload signal — and nothing backs off on your behalf. A runaway job competes with the tenant’s own users for the same server. When a call fails, back off before retrying, lengthen the wait each time, and cap the number of attempts. Pace your jobs, run bulk work outside business hours, and keep concurrency low. Integrations that work well page in the low hundreds to low thousands of rows and pause between pages.

Because there are no enforced maxima, the practical limits are the timeout and your own patience. A 2 GB multipart upload and a two-hour request are both technically allowed and both bad ideas.

Error codes

HTTPcodeMeaning
200OK_RESPONSESuccess
200/404OK_NODATAThe request was valid; nothing matched
400AUTH_TOKEN_OR_ACCESS_ID_WAS_NOT_PROVIDEDNo credential
400AUTH_TOKEN_OR_ACCESS_KEY_WAS_NOT_PROVIDEDAccessId without AccessKey
400CLIENT_TENANT_CODE_IS_EMPTYNo tenantCode header
401CLIENT_AUTH_NO_TOKENNo credential on a platform endpoint
403AUTH_TOKEN_OR_INVALID_ACCESS_IDNo key with that AccessId
403AUTH_TOKEN_OR_EXPIRED_ACCESS_IDThe key has passed its validDateEnd
403AUTH_TOKEN_OR_ACCESS_ID_TOKEN_NOT_MATCHEDWrong AccessKey for that AccessId
403CLIENT_AUTH_INVALID_TOKENMalformed or expired token
403CLIENT_AUTH_USER_NOT_AUTHORIZEDAuthenticated; the user lacks the permission
404CLIENT_TENANT_CODE_DOES_NOT_EXISTUnknown tenant
404/403CLIENT_VALIDATION_GUID_DOES_NOT_EXISTNo record with that GUID
400validation codesThe body failed a consistency check; data carries the failures
417SERVER_GENERAL_UNKNOWN_ERRORUnhandled error — or an unrecognised path

Check code before HTTP status. A validation failure and a permission failure can share a status but never share a code.

Related documentation

Last updated on