Skip to content

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).

  1. POST /core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-ep with a container body whose keys you copied from a real invoice (reading before writing), with new line guids, because the copied ones belong to the original. Generate the header guid yourself and store it before the first attempt, so a retry cannot make a second invoice. Leave posting_status out: a create sent as FINAL stores FINAL and starts no posting, yet can still reach LHDN with no journal behind it.
  2. 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 no date_finalized, and a foreign-currency document finalised on it posts no journal (what differs). Its 200 means the posting job was queued, not that anything posted.
  3. A minute later, GET /core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-ep/{guid} and read posting_journal on the header (and posting_inventory, which works the same way, for stock). POSTED means it was written; still blank means the job failed or never ran (how you know it worked).

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,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.

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-epMeant 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.

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,058 endpoint methods across 177 controllers, covering roughly 320 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.

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
Finalise (post)PUT /core2/tnt/dm/erp/gen-doc/{docType}/etl-ep/update-posting-status/{guid}
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": [ ]
}
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.

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 a guid already in the tenant is refused, so a retry after a timeout cannot make a second invoice.
  • posting_status. Leave it out, or send DRAFT, which is the default. Never send FINAL on a create — why.
  • status. Leave it out and it is ACTIVE. It is a different field from posting_status: status is the record’s lifecycle, while posting_status says whether the document has been posted (DRAFT, then FINAL, or VOID).

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.

Do not send "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 read posting_status first and call once.
  • Stock is not checked. The screens’ route checks the stock balance at FINAL unless 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 FINAL with no journal behind it.
  • date_finalized is not written. The date_txn_logic and validate_stock_balance options 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:

  1. GET …/gen-doc/internal-sales-invoices/etl-ep/{guid}.
  2. On bl_fi_generic_doc_hdr, posting_status should read FINAL. That is your call landing.
  3. posting_journal should read POSTED. The journal job writes that on success and writes nothing on failure. posting_inventory works the same way on a document that moves stock.
  4. 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_comp and guid_store are required, and a doc_entity_hdr_guid you 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>_CREATED event 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.

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 — the lifecycle (ACTIVE and so on), not posting_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 (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 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. Two things stand in for one, and a careful integration uses both. Set client_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 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. One refused document refuses the whole batch
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
400EXCEPTION_INCONSISTENT_DATAThe body failed a consistency check; data lists each failure
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.

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 seeWhyWhat to do
417, SERVER_GENERAL_UNKNOWN_ERROR, message beginning No static resourceThe path is not routedCopy the path from this page or the API Reference; do not compose it
404, OK_NODATA, on create, update, delete or readBigLedger does not know that {docType} slugTake the slug from the table under Documents. server-doc-types gives type names, not slugs
404, CLIENT_VALIDATION_GUID_DOES_NOT_EXIST, on FinaliseOne 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 itGET the document with the same slug. If that finds it, the cause is permission
403, CLIENT_AUTH_USER_NOT_AUTHORIZED, on create or updateThe key’s user lacks that permission for this document type, for the company, branch or location the document namesCheck the key’s permissions for that branch, not only for the tenant
400, EXCEPTION_INCONSISTENT_DATAThe document failed validation. On …/multi, nothing in the batch was writtenFix every errorCode in data and send the whole batch again
400, data naming GENERIC_DOC_HDR_OBJECT_GUID_ALREADY_EXISTSA document with that header guid already existsOn a retry, your first attempt landed. GET it; do not create it again
400 on Finalise, data naming FISCAL_PERIOD_LOCKEDThe transaction date is in a locked periodMove 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_XRATEA foreign-currency document with no exchange rateSet the rate on the draft and finalise again — then read the foreign-currency point under what differs
400, CLIENT_MALFORMED_JSON_REQUESTThe body is not valid JSON for this resourceMirror a record read back with GET (Reading before writing)
200 on Finalise, but posting_journal is still blank minutes laterA 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 otherFix the cause, then use Trace Document’s Resolve. Do not call Finalise again — it queues the chain again and repairs nothing
429, CLIENT_OVERLOAD_DATABASEThe database is out of connection slotsBack 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 guid per order, plus client_source and client_key (for GSO, storefront and 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 FINAL document 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=FINAL and a date_txn range. Compare the count and total with your own system, and look for any row whose posting_journal is still blank. The finance person who owns GSO is the right owner. They know it is done when the counts match and every FINAL document carries POSTED. Anything else goes to Trace Document.

Related documentation

Last updated on