Data API
Is there an API, and what can it do? Yes. This is the reference for its server-to-server half,
the etl-ep endpoints of the BigLedger REST API. If you have not issued an access key yet, start with
Getting Started.
The example that runs through this page is an integration pushing the web orders of GadgetSphere
Online (GSO, the e-commerce arm of a fictional Malaysian electronics retailer) into BigLedger as
sales invoices: create each one, finalise it, and confirm it posted. The part most integrations get
wrong is the middle step — see Draft, then Finalise.
Every path and method on this page is taken from the platform’s own route table, so you can use them exactly as written. The API Reference lists the routes this page leaves out.
The fast path: one web order in, as a posted sales invoice, in three calls to
https://api-etl.akaun.com with the AccessId, AccessKey and tenantCode headers. Before the first
one, map the company’s default GL codes: a missing receivable mapping stops the journal after Finalise
has already answered 200, and a missing sales or tax mapping leaves that line out of it
(what must exist first).
POST /core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-epwith a container body whose keys you copied from a real invoice (reading before writing), with new lineguids, because the copied ones belong to the original. Generate the headerguidyourself and store it before the first attempt, so a retry cannot make a second invoice. Leaveposting_statusout: a create sent asFINALstoresFINALand starts no posting, yet can still reach LHDN with no journal behind it.PUT /core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-ep/update-posting-status/{guid}with{ "posting_status": "FINAL" }, once: this route does not refuse a second call, checks no stock, writes nodate_finalized, and a foreign-currency document finalised on it posts no journal (what differs). Its200means the posting job was queued, not that anything posted.- A minute later,
GET /core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-ep/{guid}and readposting_journalon the header (andposting_inventory, which works the same way, for stock).POSTEDmeans it was written; still blank means the job failed or never ran (how you know it worked).
Hosts
| Host | Use it for |
|---|---|
https://api-etl.akaun.com | Data traffic. The ingress BigLedger’s own integrations use. |
https://api.akaun.com | Signing 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,205 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.
| Segment | Meaning |
|---|---|
tnt | Tenant-scoped. Needs a tenantCode header. This is where business data lives. |
platform | Platform-scoped: identity, applets, tenants. No tenantCode. |
dm | Domain model — the normal, current surface |
ms | Micro-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-ep | Server-to-server integrations. Yours. |
backoffice-ep | A staff user working in the BigLedger shell |
login-ep | A signed-in user acting on their own records |
login-entity-ep | An external party — a customer or supplier — acting on their own records |
public-ep | Meant for unauthenticated callers. Not a guarantee: some public-ep routes still ask for a credential, so test the one you need |
278 of the 345 etl-ep resource prefixes also have a backoffice-ep twin. Build against
etl-ep: it is the variant that takes an access key. Do not assume that a twin, or the route a
screen calls, behaves identically. The document Finalise route is the clearest case where the
server-to-server route and the route the screens call do different work —
what differs.
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:
| Operation | Request |
|---|---|
| Create | POST /core2/tnt/dm/erp/fi/fi-items/etl-ep |
| Update | PUT /core2/tnt/dm/erp/fi/fi-items/etl-ep |
| Delete | DELETE /core2/tnt/dm/erp/fi/fi-items/etl-ep/{guid} |
| List all | GET /core2/tnt/dm/erp/fi/fi-items/etl-ep |
| Read one | GET /core2/tnt/dm/erp/fi/fi-items/etl-ep/{guid} |
| Filter and page | GET /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,058 endpoint methods across 177 controllers, covering roughly 320 resources. The groups you are most likely to need:
Master data
| Resource | Path under /core2/tnt/dm/erp/ |
|---|---|
| Customers, suppliers, employees, merchants | entity/{entityType}/etl-ep |
| All entities regardless of type | entity/etl-ep |
| Financial items (the sellable/purchasable item master) | fi/fi-items/etl-ep |
| Inventory items | inv/inv-items/etl-ep |
| Companies | companies/etl-ep |
| Branches | branches/etl-ep |
| Locations | locations/etl-ep |
| Chart of accounts, ledgers and cashbooks | chart-of-acc/etl-ep, fi/ledgers/etl-ep, cashbooks/etl-ep |
| Contacts (batch create only) | contacts/etl-ep/multi |
| Labels and label lists | labels/etl-ep, label-lists/etl-ep |
| Pricing scheme links | pricing-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.
Documents
Every document type goes through one controller, keyed by a URL slug:
| Operation | Request |
|---|---|
| List the document types this platform knows | GET /core2/tnt/dm/erp/gen-doc/etl-ep/server-doc-types |
| Create | POST /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep |
| Create many | POST /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/multi |
| Update | PUT /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep |
| Finalise (post) | PUT /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/update-posting-status/{guid} |
| Delete | DELETE /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/{guid} |
| Read one | GET /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/{guid} |
| Filter and page, one type | GET /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/query |
| Filter and page, all types | GET /core2/tnt/dm/erp/gen-doc/etl-ep/query |
| Void | PUT /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/void/{guid} |
| Print to PDF | GET /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/print-jasper-pdf/{guid} |
There are 97 document-type slugs. The common ones:
{docType} slug | Document type |
|---|---|
internal-sales-invoices | INTERNAL_SALES_INVOICE |
internal-sales-cashbills | INTERNAL_SALES_CASHBILL |
internal-sales-orders | INTERNAL_SALES_ORDER |
internal-sales-returns | INTERNAL_SALES_RETURN |
internal-sales-credit-notes | INTERNAL_SALES_CREDIT_NOTE |
internal-sales-debit-notes | INTERNAL_SALES_DEBIT_NOTE |
internal-purchase-orders | INTERNAL_PURCHASE_ORDER |
internal-purchase-invoices | INTERNAL_PURCHASE_INVOICE |
internal-purchase-goods-received-notes | INTERNAL_PURCHASE_GOODS_RECEIVED_NOTE |
internal-stock-adjustment | INTERNAL_STOCK_ADJUSTMENT |
internal-payment-vouchers | INTERNAL_PAYMENT_VOUCHER |
internal-receipt-vouchers | INTERNAL_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 PUTFull 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": [ ]
}bl_fi_generic_doc_line
(singular); item lines are bl_fi_mst_item_lines (plural). Copy the keys from a real response.On a create, the server fills in what you leave out, and three of those fields are worth deciding yourself:
- The header
guid. Leave it out and BigLedger generates one. Better, generate your own UUID and store it before the first attempt: a create that reuses aguidalready in the tenant is refused, so a retry after a timeout cannot make a second invoice. posting_status. Leave it out, or sendDRAFT, which is the default. Never sendFINALon a create — why.status. Leave it out and it isACTIVE. It is a different field fromposting_status:statusis the record’s lifecycle, whileposting_statussays whether the document has been posted (DRAFT, thenFINAL, orVOID).
Reading before writing
The same container type comes back from GET, so the reliable way to learn a write body is to
read a record of the same type and mirror it. For GSO, that means fetching one sales invoice
someone keyed on a screen — GET …/gen-doc/internal-sales-invoices/etl-ep/{guid} — and using its
keys as your template. Dates are ISO-8601 strings, never epoch numbers.
When you turn the copy into a create, give it a new header guid and new line guids (the copied
ones belong to the original), and take out its posting_status.
Draft, then Finalise
Creating a document saves it. It does not post it. Nothing reaches the journal, stock, the cashbook
or e-invoicing until a second, separate call sets the document FINAL. The screens work the same
way, Save and then Finalise, and this split is the one most integrations miss.
Create as a draft
POST …/gen-doc/internal-sales-invoices/etl-ep answers 200 with OK_RESPONSE, and data is the
stored document, guid included. With no posting_status in the body it is stored as DRAFT.
"posting_status": "FINAL" on a create. The create path stores the posting status
you send and starts no posting. The document reads FINAL everywhere, and no journal is written for
it unless someone repairs it by hand. Since mid-October 2026 it is still handed to e-invoicing, so
it can reach LHDN with no journal behind it. Create as a draft, then finalise.Finalise
PUT /core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-ep/update-posting-status/{guid}
Content-Type: application/json
{ "posting_status": "FINAL" }The server loads the stored document and sets the status you sent. It then runs the checks a
document must pass at FINAL, saves it, and queues one job. Those checks include an open fiscal
period, an exchange rate on a foreign-currency document, serial, batch and bin quantities, and a
blacklisted customer on cash bills, invoices, orders and delivery orders. The response comes back
before the job runs.
That job is the primary posting job. It looks up which follow-on jobs your tenant subscribes to — journal, stock, cashbook, e-invoice, membership points and others — and queues each one. Which of them exist is decided per tenant, and a company can narrow the list. That is why the same call can post more for one business than for another. How work actually runs traces the whole chain.
An ordinary PUT …/etl-ep keeps the posting status already stored, whatever your body says. On
this surface the posting status changes only through Finalise and the void and discard routes.
What the etl-ep Finalise does differently
The screens finalise through routes that take a signed-in user’s token, commonly the sibling
PUT …/gen-doc/{docType}/update-posting-status/{guid} with no etl-ep segment. That route and this
one do not do the same work. On the etl-ep route:
- A second call is not refused. The screens’ route refuses a document that is already
FINAL, with “Generic Document has already been posted to FINAL”. This route runs the checks again, saves again and queues the posting job again. The journal and stock jobs each decline to post a document twice. We have not read every other follow-on job, so readposting_statusfirst and call once. - Stock is not checked. The screens’ route checks the stock balance at
FINALunless told not to. This one does not ask for that check. - A foreign-currency document posts no journal. The screens’ route first makes a base-currency
copy of the document and posts that. This route makes no copy, and the journal job declines a
foreign-currency document, so a USD invoice finalised here reads
FINALwith no journal behind it. date_finalizedis not written. Thedate_txn_logicandvalidate_stock_balanceoptions the screens’ route reads from the body have no effect here.
Whether all four are intended is a question BigLedger’s engineers have been asked and have not yet answered. Build for what the route does today, and come back to this section before you rely on any of the four.
How you know it worked
A 200 with OK_RESPONSE from Finalise means the document was saved FINAL and the posting job
was queued. It does not mean anything posted. The follow-on jobs run after the response, and a
failure inside one of them never comes back to you. So check the document, not the response:
GET …/gen-doc/internal-sales-invoices/etl-ep/{guid}.- On
bl_fi_generic_doc_hdr,posting_statusshould readFINAL. That is your call landing. posting_journalshould readPOSTED. The journal job writes that on success and writes nothing on failure.posting_inventoryworks the same way on a document that moves stock.- Give it a minute. If it is still blank after that, the job failed or never ran. From here the two look the same.
For one document, a person with the Financial Report applet can go further: Trace Document checks whether the journal, cashbook line and shadow document exist. Once the cause is fixed, its Resolve button posts the missing piece. For a day’s worth of documents, the reconciliation in What the API leaves to you is the check.
What must exist first, and what it sets off
Before the create:
- A key whose user may create and update this document type. The check is made against the company, branch and location the document itself names. A key that works for one branch can therefore be refused for another.
- The company, the location and the customer.
guid_compandguid_storeare required, and adoc_entity_hdr_guidyou send must exist. - The company’s default GL codes, for every transaction code the document will post to — see
General ledger defaults. This one is
not among the Finalise checks above. A missing code stops the journal job after Finalise has
already answered
200.
After Finalise:
- The ledger, stock and the rest, through the follow-on jobs above, on the tenant’s own subscriptions.
- Webhooks. Most document types queue a
<TYPE>_CREATEDevent on the create. A sales invoice queues it at Finalise instead. Every save, Finalise included, sends<TYPE>_UPDATED. See Webhooks. - E-invoicing, where the tenant subscribes to it: a finalised sales document is what the e-invoice job picks up. The E-Invoice API is where you follow what became of it.
The blast radius of one stopped job. The HTTP call has already succeeded, so a stopped journal
is invisible to your integration. It is invisible to the people at the screens too: the invoice
shows FINAL everywhere, and the ledger is short by its amount until somebody reposts it.
The document chain
The fourth key in the write 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.
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.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_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.
| Parameter | Default | Notes |
|---|---|---|
limit | 100 | No enforced maximum. Responses stream, so a big page is a long response. |
offset | 0 | |
orderBy | record guid | A column on the record, e.g. updated_date |
order | ASC | ASC or DESC; anything else is treated as ASC |
calcTotalRecords | false | Costs an extra COUNT. Send it on the first page only. |
status | — | Filter by record status — the lifecycle (ACTIVE and so on), not posting_status |
excluded_statuses | — | Exclude statuses |
search_word | — | Free-text search, where the resource supports it |
querying_timezone | Asia/Kuala_Lumpur | The zone date filters are interpreted in |
Most of the resources you will sync also accept, in ISO-8601:
| Parameter | Meaning |
|---|---|
updated_date_from / updated_date_to | When the row last changed — use this for incremental pulls |
created_date_from / created_date_to | When the row was created |
date_txn_from / date_txn_to | The 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 (the
filter for DRAFT / FINAL / VOID),
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 record | Query parameter | Use it for |
|---|---|---|
client_key | hdr_client_key | Your primary key for the record |
client_source | hdr_client_source | Which 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-0043117client_key on everything you write, and query on hdr_client_key before you
create. Also send a header guid you generated and stored before the first attempt: a create that
reuses an existing guid is refused, so a retry after a timeout cannot produce a second document.Limits
Measured from the platform’s own configuration.
| Request timeout | 2 hours for asynchronous request handling |
| Multipart upload | 2 GB per file and per request |
| Response compression | gzip above 1 KB |
| Page size | Defaults to 100; no maximum enforced |
| Batch create | No maximum enforced on …/etl-ep/multi. One refused document refuses the whole batch |
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
| HTTP | code | Meaning |
|---|---|---|
| 200 | OK_RESPONSE | Success |
| 200/404 | OK_NODATA | The request was valid; nothing matched |
| 400 | AUTH_TOKEN_OR_ACCESS_ID_WAS_NOT_PROVIDED | No credential |
| 400 | AUTH_TOKEN_OR_ACCESS_KEY_WAS_NOT_PROVIDED | AccessId without AccessKey |
| 400 | CLIENT_TENANT_CODE_IS_EMPTY | No tenantCode header |
| 401 | CLIENT_AUTH_NO_TOKEN | No credential on a platform endpoint |
| 403 | AUTH_TOKEN_OR_INVALID_ACCESS_ID | No key with that AccessId |
| 403 | AUTH_TOKEN_OR_EXPIRED_ACCESS_ID | The key has passed its validDateEnd |
| 403 | AUTH_TOKEN_OR_ACCESS_ID_TOKEN_NOT_MATCHED | Wrong AccessKey for that AccessId |
| 403 | CLIENT_AUTH_INVALID_TOKEN | Malformed or expired token |
| 403 | CLIENT_AUTH_USER_NOT_AUTHORIZED | Authenticated; the user lacks the permission |
| 404 | CLIENT_TENANT_CODE_DOES_NOT_EXIST | Unknown tenant |
| 404/403 | CLIENT_VALIDATION_GUID_DOES_NOT_EXIST | No record with that GUID |
| 400 | EXCEPTION_INCONSISTENT_DATA | The body failed a consistency check; data lists each failure |
| 417 | SERVER_GENERAL_UNKNOWN_ERROR | Unhandled 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.
A validation refusal carries its detail in data, one entry per failure:
{
"code": "EXCEPTION_INCONSISTENT_DATA",
"message": "…",
"data": [
{ "errorCode": "FISCAL_PERIOD_LOCKED",
"shortMessage": "The selected date falls within a locked fiscal period.",
"longMessage": "…", "targetedObject": { } }
]
}When a document call fails — what you see, why, and what to do
| What you see | Why | What to do |
|---|---|---|
417, SERVER_GENERAL_UNKNOWN_ERROR, message beginning No static resource | The path is not routed | Copy the path from this page or the API Reference; do not compose it |
404, OK_NODATA, on create, update, delete or read | BigLedger does not know that {docType} slug | Take the slug from the table under Documents. server-doc-types gives type names, not slugs |
404, CLIENT_VALIDATION_GUID_DOES_NOT_EXIST, on Finalise | One of three things, answered the same way: the slug is not the document’s type, the guid does not exist, or the key’s user may not update it | GET the document with the same slug. If that finds it, the cause is permission |
403, CLIENT_AUTH_USER_NOT_AUTHORIZED, on create or update | The key’s user lacks that permission for this document type, for the company, branch or location the document names | Check the key’s permissions for that branch, not only for the tenant |
400, EXCEPTION_INCONSISTENT_DATA | The document failed validation. On …/multi, nothing in the batch was written | Fix every errorCode in data and send the whole batch again |
400, data naming GENERIC_DOC_HDR_OBJECT_GUID_ALREADY_EXISTS | A document with that header guid already exists | On a retry, your first attempt landed. GET it; do not create it again |
400 on Finalise, data naming FISCAL_PERIOD_LOCKED | The transaction date is in a locked period | Move the draft’s date into an open period, or ask whoever closes the books whether the period should reopen |
400 on Finalise, data naming FOREX_DOC_REQUIRES_NON_ZERO_XRATE | A foreign-currency document with no exchange rate | Set the rate on the draft and finalise again — then read the foreign-currency point under what differs |
400, CLIENT_MALFORMED_JSON_REQUEST | The body is not valid JSON for this resource | Mirror a record read back with GET (Reading before writing) |
200 on Finalise, but posting_journal is still blank minutes later | A follow-on job stopped after the response. The usual cause is a default GL code the company lacks, which the job records as MISSING_DEFAULT_GL_CODE. On this route, a foreign-currency document is the other | Fix the cause, then use Trace Document’s Resolve. Do not call Finalise again — it queues the chain again and repairs nothing |
429, CLIENT_OVERLOAD_DATABASE | The database is out of connection slots | Back off and retry later — see Limits |
What the API leaves to you
The document endpoints are deliberately general. The checks a screen makes before it calls them live in the screen, not in the API. BigLedger keeps one general endpoint per operation rather than one per screen rule, so this is a design choice and not a gap. It does move those checks onto your side, and each one needs an owner.
- Not creating the same order twice. Whoever owns the integration settles the key scheme once
and writes it down: a stored header
guidper order, plusclient_sourceandclient_key(for GSO,storefrontand the web order number). Changing the scheme later breaks the duplicate check for everything already sent. - The checks the screen would have made. Two examples: the stock balance at Finalise on this route, and how much of an order a delivery has consumed (the link quantities above). Your integration makes these before it calls, or someone decides they are not needed. An online arm that sells ahead of stock on purpose is a legitimate answer, but it should be somebody’s decision.
- When a document becomes final. The API finalises the moment you ask. Whoever owns GSO’s books
decides whether web orders post on arrival or wait as drafts for review. A draft is cheap to
correct; a
FINALdocument has already posted. - Noticing a posting that stopped. Nothing calls you back, and nothing in the product reads the
job-failure table. So the check is yours, daily. Query yesterday’s documents with
hdr_client_source=storefront,hdr_posting_status=FINALand adate_txnrange. Compare the count and total with your own system, and look for any row whoseposting_journalis still blank. The finance person who owns GSO is the right owner. They know it is done when the counts match and everyFINALdocument carriesPOSTED. Anything else goes to Trace Document.