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
| Header | Required | Value |
|---|---|---|
AccessId + AccessKey | one credential is required | An access key pair (server-to-server) |
Authorization | one credential is required | Bearer <token> (a signed-in user’s token) |
tenantCode | yes, on every tenant endpoint | The 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:
| Half | Shape | Sent as |
|---|---|---|
| Access ID | 10 upper-case alphanumeric characters | AccessId header |
| Access key | 25 alphanumeric characters | AccessKey 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:
- looks the record up by
AccessId, skipping keys that have been deleted; - 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;
- checks
AccessKeyagainst the stored hash; - 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.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 key | POST /core2/platform/dm/identity/access-keys/login-ep |
| List your keys | GET /core2/platform/dm/identity/access-keys/login-ep |
| Delete a key | DELETE /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:
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:
| Route | Access key | User token |
|---|---|---|
etl-ep on /core2/tnt/… and /core2/dm/… | yes, tried first | yes, when the key pair fails or is absent |
etl-ep on /core2/platform/… (a handful of catalogue routes) | yes | no — the key pair only |
backoffice-ep, login-ep and most other routes on tenant paths | yes, when no token is sent | yes, tried first |
Most /core2/platform/… routes, and 74 tenant-path routes built the same way | no | yes |
public-ep | not 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
| HTTP | code | What happened | What to do |
|---|---|---|---|
| 400 | AUTH_TOKEN_OR_ACCESS_ID_WAS_NOT_PROVIDED | No credential of any kind on a tenant endpoint | Send the AccessId and AccessKey headers on every request |
| 400 | AUTH_TOKEN_OR_ACCESS_KEY_WAS_NOT_PROVIDED | AccessId sent without AccessKey, or with a blank one | Send both halves; check the secret variable is set where the client runs |
| 400 | ACCESS_ID_WAS_NOT_PROVIDED | A platform route built for keys received a token, or nothing | Send the key pair; these routes do not read a token |
| 401 | CLIENT_AUTH_NO_TOKEN | No token on a token-only route, even if the AccessId/AccessKey headers were sent | This 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 |
| 403 | AUTH_TOKEN_OR_INVALID_ACCESS_ID | No live key with that AccessId: mistyped, or deleted | Compare it with your key listing. If it is not there, issue a new key |
| 403 | AUTH_TOKEN_OR_EXPIRED_ACCESS_ID | The key’s validDateEnd has passed | Issue a new key and delete this one. The secret was not checked |
| 403 | AUTH_TOKEN_OR_ACCESS_ID_TOKEN_NOT_MATCHED | Right AccessId, wrong AccessKey | Check for a truncated or swapped secret. If it is lost, delete the key and issue another |
| 403 | CLIENT_AUTH_INVALID_TOKEN | The 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 |
| 403 | CLIENT_AUTH_USER_NOT_AUTHORIZED | Your credential was accepted, but its user lacks the permission this endpoint needs in this tenant, or holds nothing in this tenant at all | Ask the tenant administrator for the permission. Do not reissue the key |
| 400 | CLIENT_TENANT_CODE_IS_EMPTY | No tenantCode header on a tenant endpoint | Send it with each request to a tenant endpoint |
| 404 | CLIENT_TENANT_CODE_DOES_NOT_EXIST | The tenantCode is not a tenant | Check the spelling and that you are on the right environment |
| 404 | no body | On the document query, the document type in the path is not one BigLedger knows | Check the path against the Data API |
| 417 | SERVER_GENERAL_UNKNOWN_ERROR, message No static resource… | The path matches no route | Check the spelling before you suspect the credential |
Two of these are worth reading carefully:
CLIENT_AUTH_USER_NOT_AUTHORIZEDis 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 isCLIENT_AUTH_INVALID_TOKENorCLIENT_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 inmessage; 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
validDateEndand 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.