Skip to content
Authentication

Authentication

Every call to the BigLedger API carries two things: a credential, and the code of the tenant the call applies to. This page covers both: the two credentials an integration can hold, the routes that need neither, what BigLedger reads when a call arrives, and the exact response code each failure returns.

If you are writing a server-to-server integration, the credential you want is an access key, and Integration → Getting Started walks you through issuing one end to end.

The two headers

HeaderRequiredValue
AccessId + AccessKeyone credential is requiredAn access key pair (server-to-server)
Authorizationone credential is requiredBearer <token> (a signed-in user’s token)
tenantCodeyes, on every tenant endpointThe tenant you are addressing

tenantCode is not part of the credential. It selects which customer database the request runs against, and the same credential can be valid in more than one tenant. Most endpoints under /core2/platform/ are not tenant-scoped and do not need it; the few that are answer CLIENT_TENANT_CODE_IS_EMPTY when it is missing.

Credential 1 — access keys (server-to-server)

An access key is a long-lived credential intended for processes that make many API calls. It is the credential every BigLedger data integration uses.

A key has two halves:

HalfShapeSent as
Access ID10 upper-case alphanumeric charactersAccessId header
Access key25 alphanumeric charactersAccessKey header
GET /core2/tnt/dm/erp/fi/fi-items/etl-ep/query?limit=100 HTTP/1.1
Host: api-etl.akaun.com
AccessId: <your 10-character access id>
AccessKey: <your 25-character access key>
tenantCode: <your tenant code>

Both header names are matched case-insensitively and in hyphenated form, so AccessId, AccessID, accessId, accessID and access-id are all accepted, as are AccessKey, accessKey and access-key. Pick one spelling and keep it.

What BigLedger reads when a key arrives

Behind the scenes, a key is one record in BigLedger’s platform registry, beside the sign-in e-mails and mobile numbers, and not in any tenant’s database. The record holds the access id, your label, the user the key belongs to, a hash of the secret, the dates the key is valid from and to, and a status. Nothing in it names a tenant. That is why one key works in any tenant its user belongs to, and why tenantCode does the choosing.

On every call that carries the pair, BigLedger:

  1. looks the record up by AccessId, skipping keys that have been deleted;
  2. compares the key’s end date with the current time, and refuses an expired key before it reads the secret, so an expiry answer tells you nothing about whether your secret is right;
  3. checks AccessKey against the stored hash;
  4. hands the endpoint the user the key belongs to, and nothing else.

The record is read afresh on each call, so a key you delete, or one whose end date passes, is refused on the next call that sends that pair. Nothing about the call is written back to the record: it keeps no last-used date and no call count, so BigLedger has no answer to “is this key still in use?” Your integration’s own logs do.

Issuing a key

Key issuance is self-service. You sign in as a normal user, and mint a key for that user. The /login-ep route checks no permission beyond a valid sign-in, which is why the rule in What a key can do matters.

POST /core2/platform/dm/identity/access-keys/login-ep HTTP/1.1
Host: api.akaun.com
Authorization: Bearer <your user token>
Content-Type: application/json

{
  "label": "GadgetSphere Online order sync",
  "type": "",
  "validDateEnd": "2027-09-06T00:00:00+08:00"
}
{
  "code": "OK_RESPONSE",
  "data": { "keyId": "<10 characters>", "keySecret": "<25 characters>" },
  "message": ""
}

keyId is the AccessId; keySecret is the AccessKey.

keySecret is shown once and never again. It is stored hashed. If you lose it, delete the key and issue another. No endpoint reveals it.
Always send validDateEnd. If you omit it, the key is created with an expiry of 31 December 9999, which in practice means a credential that never expires. Set a real date and diarise the rotation.

Managing your keys

Issue a keyPOST /core2/platform/dm/identity/access-keys/login-ep
List your keysGET /core2/platform/dm/identity/access-keys/login-ep
Delete a keyDELETE /core2/platform/dm/identity/access-keys/login-ep/{guid}

These three need only a valid user token, and they act only on the keys of the user who is signed in. The listing shows that user’s keys that are still active. A delete of somebody else’s key is refused as not authorised.

The listing returns each key’s record: the access id, your label, the validity dates and the status. It never returns the secret. Treat the listing response as sensitive all the same and do not log it.

Deleting a key marks its record deleted rather than erasing it. From then on, a call with that pair gets the same answer as an access id that never existed.

The same three endpoints also exist without the /login-ep segment. They check a platform-level permission first and then do exactly the same thing, still only to your own keys. Use the /login-ep variants.

Who looks after the keys

Three people are involved, and the API knows about only one of them.

  • The tenant administrator creates the integration user and grants it permissions in the tenant, in the Tenant Admin applet. The API does not let an integrator grant itself anything.
  • Whoever holds the integration user’s sign-in issues, rotates and deletes its keys. Nobody else sees those keys through the access-key endpoints, not even the tenant administrator. Write down who that person is, and hand the sign-in over when they change roles.
  • Whoever owns the integration answers the question BigLedger leaves open: is this key still in use? The key keeps no record of its use, so the answer comes from the integration’s own logs.

A review that works: at each rotation, sign in as each integration user, list its keys, and match every label to an integration that is running today. Delete any key you cannot match. You know it worked when the key has gone from the listing and a test call with it answers AUTH_TOKEN_OR_INVALID_ACCESS_ID.

If nobody can sign in as an integration user any more, the tenant administrator’s lever is that user’s permissions: remove them, and the endpoints that check permissions refuse the key. Removing the key record itself is a platform administrator’s task at BigLedger.

What a key can do

A key inherits the permissions of the user it was issued for. It has no scopes of its own.

There is no per-key permission model, no read-only flag and no endpoint allowlist. The key resolves to a login subject, and every endpoint then runs the same permission checks it would run for that person signed in through the browser.

The practical consequence is the one rule worth building around:

Create a dedicated integration user. Do not mint keys against a finance manager’s login. Create a user whose only job is the integration, grant it exactly the permissions that integration needs in the tenant, and issue the key against that user. When the integration is retired or the key leaks, you delete that user’s key, and remove the user’s permissions if you need to act before anyone can sign in as it. Nobody’s daily work is touched.

The rule cuts the other way too. Any user can mint a key for themselves, and that key carries everything the user holds, in any tenant the user belongs to, until its end date. A key minted against a tenant administrator’s login is a tenant administrator that never signs out, running from a server nobody watches.

Credential 2 — a signed-in user’s token

Interactive clients — the BigLedger shell, applets, portal logins — carry a signed token in the Authorization header.

POST /core2/platform/dm/identity/login HTTP/1.1
Host: api.akaun.com
Content-Type: application/json

{ "email": "you@example.com", "password": "…" }

The response carries authToken, subjectGuid, email, mobileNumber, the entity links for that login, and the per-applet tokens the shell needs:

{
  "code": "OK_RESPONSE",
  "data": {
    "authToken": "<token>",
    "subjectGuid": "…",
    "email": "you@example.com",
    "mobileNumber": null,
    "data": [],
    "appletTenantTokenList": []
  },
  "message": ""
}

Send it as Authorization: Bearer <authToken>.

What the password is checked against is a hash in BigLedger’s own table. The login endpoint finds the principal for the e-mail or mobile number, reads the last password hash stored for that subject and compares; a match returns the token above, a mismatch returns Invalid attempt. There is no AWS Cognito behind it — every Cognito call in the identity services is commented out and the Cognito token validator has no caller — so a token is never checked against a Cognito user pool. Sign-in options covers the social providers and MFA.

The token is an RS256-signed JWT and it expires 30 days after issue. Log in again to get a new one: no exchange extends a user token. The two endpoints under POST /core2/platform/dm/identity/token/refresh do something else — they take a valid user token and return applet-tenant tokens for the applet and tenant pairs you name (404 CLIENT_VALIDATION_INVALID_DATA when a pair does not exist for that user). They do not renew the token you sent. Because a user token expires and because it carries the full rights of a human user, it is the wrong credential for an unattended integration — use it to mint an access key, then use the key.

The platform also accepts the token in a jwtToken cookie or a jwtToken query parameter. Both exist for the browser shell. Do not use them from a server: a token in a query string ends up in logs, proxies and browser history. A leftover cookie also matters to a key: see the CLIENT_AUTH_INVALID_TOKEN row under Error codes.

Credential 3 — no credential

Routes with public-ep in the path are the ones built to be open, and most of them are: they read the tenant code and nothing else. Not all of them. At the backend version this page was checked against, 37 of the 300 public-ep routes still ask for a credential. Treat public-ep as a hint and the response as the answer: an authentication code from a public-ep route means that route wants a credential after all.

Everything else requires one of the two credentials above. A few routes take special-purpose credentials of BigLedger’s own (remote-agent tokens and configuration authentication codes); this page does not cover them. There is no OAuth flow and no client library to wrap any of this: you send the headers above from whatever HTTP client you already use.

Which credential works where

What a route accepts is decided by its handler, not by its path, and the access segment in the path is the best guide you have:

RouteAccess keyUser token
etl-ep on /core2/tnt/… and /core2/dm/…yes, tried firstyes, when the key pair fails or is absent
etl-ep on /core2/platform/… (a handful of catalogue routes)yesno — the key pair only
backoffice-ep, login-ep and most other routes on tenant pathsyes, when no token is sentyes, tried first
Most /core2/platform/… routes, and 74 tenant-path routes built the same waynoyes
public-epnot needed on most (see above)not needed on most

Two things follow.

Build against etl-ep wherever one exists. It is the variant maintained for server-to-server callers, it checks your key before anything else, and it is the one the Data API documents. Building against backoffice-ep means building against the surface the user interface moves around.

On a token-first route, any token wins. If your client sends a stale Authorization header or still holds a jwtToken cookie, a backoffice-ep route validates that token and never looks at your key. An etl-ep route reads the key first and is not caught out this way.

Error codes

Responses carry a platform response code in code that is more specific than the HTTP status. Match on code, not on the status.

When it goes wrong: each refusal and its fix

HTTPcodeWhat happenedWhat to do
400AUTH_TOKEN_OR_ACCESS_ID_WAS_NOT_PROVIDEDNo credential of any kind on a tenant endpointSend the AccessId and AccessKey headers on every request
400AUTH_TOKEN_OR_ACCESS_KEY_WAS_NOT_PROVIDEDAccessId sent without AccessKey, or with a blank oneSend both halves; check the secret variable is set where the client runs
400ACCESS_ID_WAS_NOT_PROVIDEDA platform route built for keys received a token, or nothingSend the key pair; these routes do not read a token
401CLIENT_AUTH_NO_TOKENNo token on a token-only route, even if the AccessId/AccessKey headers were sentThis route does not read the AccessId/AccessKey header pair, so adding it changes nothing. Send a signed-in user’s token in the Authorization header, or look for an etl-ep variant of the route
403AUTH_TOKEN_OR_INVALID_ACCESS_IDNo live key with that AccessId: mistyped, or deletedCompare it with your key listing. If it is not there, issue a new key
403AUTH_TOKEN_OR_EXPIRED_ACCESS_IDThe key’s validDateEnd has passedIssue a new key and delete this one. The secret was not checked
403AUTH_TOKEN_OR_ACCESS_ID_TOKEN_NOT_MATCHEDRight AccessId, wrong AccessKeyCheck for a truncated or swapped secret. If it is lost, delete the key and issue another
403CLIENT_AUTH_INVALID_TOKENThe token is malformed, expired (30 days) or not signed by BigLedger; or a stray token was read instead of your key (always on a token-first route, and on etl-ep when the key itself failed)Sign in again for a token. If you meant to send a key, remove the Authorization header and any jwtToken cookie
403CLIENT_AUTH_USER_NOT_AUTHORIZEDYour credential was accepted, but its user lacks the permission this endpoint needs in this tenant, or holds nothing in this tenant at allAsk the tenant administrator for the permission. Do not reissue the key
400CLIENT_TENANT_CODE_IS_EMPTYNo tenantCode header on a tenant endpointSend it with each request to a tenant endpoint
404CLIENT_TENANT_CODE_DOES_NOT_EXISTThe tenantCode is not a tenantCheck the spelling and that you are on the right environment
404no bodyOn the document query, the document type in the path is not one BigLedger knowsCheck the path against the Data API
417SERVER_GENERAL_UNKNOWN_ERROR, message No static resource…The path matches no routeCheck the spelling before you suspect the credential

Two of these are worth reading carefully:

  • CLIENT_AUTH_USER_NOT_AUTHORIZED is not an authentication failure. Your credential was accepted. The user behind it does not hold the permission the endpoint requires. Fix it by granting the permission in the tenant, not by re-issuing the key.
  • The AUTH_TOKEN_OR_ prefix appears because the endpoint tried a token first, found none, and fell through to the access-key pair. The part after the prefix is the real reason. On the few platform routes built for keys alone, the same reasons arrive without the prefix.

BigLedger checks in a fixed order — the credential, then the tenant code, then the permission the endpoint needs — and stops at the first one that fails. A request with a bad credential and a bad tenantCode reports the credential problem first; fix it, try again, and the next answer may be about the next check.

Testing a credential

Test with a call that needs a permission, on a resource your integration will really use. A one-row read of sales invoices is a good first one:

curl -s "https://api-etl.akaun.com/core2/tnt/dm/erp/gen-doc/internal-sales-invoices/etl-ep/query?limit=1" \
  -H "AccessId: $BLG_ACCESS_ID" \
  -H "AccessKey: $BLG_ACCESS_KEY" \
  -H "tenantCode: $BLG_TENANT_CODE"

A 200 with "code": "OK_RESPONSE" — even with an empty data list — means the key is valid, the tenant code exists, and the user behind the key may read sales invoices in that tenant. Run it once more with one character of AccessKey changed: it must answer AUTH_TOKEN_OR_ACCESS_ID_TOKEN_NOT_MATCHED, which shows your client really is sending the key you think it is.

The document-type list at …/gen-doc/etl-ep/server-doc-types checks no permission, so a 200 from it proves only that the key is valid and the tenant code exists. Before your first real run, make the same one-row read on every resource your integration touches; a CLIENT_AUTH_USER_NOT_AUTHORIZED on any of them is a permission to ask the tenant administrator for now.

How you know which check failed

Most of both kinds arrive as 403, so read the code, not the status.

  • The credential failed when the code starts AUTH_TOKEN_OR_, or is CLIENT_AUTH_INVALID_TOKEN or CLIENT_AUTH_NO_TOKEN. BigLedger stopped before it knew who you were. The fix is on your side: the key, the headers, or the route.
  • The permission refused when the code is CLIENT_AUTH_USER_NOT_AUTHORIZED. BigLedger knew exactly who you were, and that user may not do this in this tenant. Some routes put the missing permission’s code in message; pass it to the tenant administrator. Nothing you change in the request fixes it.

Handling credentials

  • Keep the access key in a secret store or an environment variable. Never in source control, never in a URL.
  • One key per integration, labelled so you can tell which is which in the listing.
  • Set validDateEnd and rotate: issue the new key, deploy it, confirm traffic has moved, then delete the old one. Both work at once, so there is no cutover gap.
  • Delete keys for integrations you have retired. An unexpired key is a live credential whether anything is using it or not.

Related documentation

  • Before you can use a key, a tenant administrator must have created your integration user and granted it what the integration needs, and someone must hold that user’s sign-in.
  • Once the key works, you can make every Data API call with the headers above. Its error codes cover what goes wrong once you are through the door.
  • Webhooks run the other way: BigLedger calls you, rather than you polling it. A webhook carries no access key. It sends the header name and value you set on its subscription, and your server checks that header. Your access key never leaves your side.
  • The developer hub at Developers lists the rest of the integration surface.
Last updated on