Skip to content

Audit Trail API

The audit trail records what changed somewhere else in BigLedger: which record, what it looked like before and after, who did it, and when. It is the data behind the Audit Trail screen in the Tenant Admin Applet, and behind the Audit Trail item in every applet’s own sidebar.

The fast path for the usual question, what happened to this document?:

  1. Hold TNT_API_DM_APPLET_AUDIT_TRAIL_READ (or the OWNER or ADMIN code), and sign in as a user: both surfaces take a signed-in user’s token.
  2. Call GET https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep/query with foreign_key set to the document’s guid, and send tenantCode. Every filter is an exact match or a range; there is no partial match.
  3. Bound the search with created_date_from, not date_txn_from. BigLedger’s own writers never set date_txn, so a range on it excludes every row the platform wrote itself.
  4. On each UPDATED row, compare old_data with new_data under message_json.payload. action is not a filter, so narrow by action on your side (Querying).
  5. Send orderBy=created_date to get the rows in the order they happened. Every call from create to delete is shown in order, with its request and answer, under Worked examples, in order.
The in-applet screen does not use these endpoints. It reads the same table through the generic ERP listing, POST https://api-etl.akaun.com/core2/tnt/dm/erp/listing/audit-trails/backoffice-ep, which takes filterConditions and accepts the audit trail OWNER, ADMIN or CREATE permission, but not READ. A user holding only TNT_API_DM_APPLET_AUDIT_TRAIL_READ can call /query below, but gets nothing on the in-applet screen.

The same record is served by two independent surfaces. Which one you want depends on whose data you are reading.

Base paths:

SurfaceBase pathReads
Tenanthttps://api-etl.akaun.com/core2/tnt/dm/erp/audit-trailsOne tenant’s own database. Send tenantCode.
Platformhttps://api.akaun.com/core2/platform/dm/applet-audit-trailThe master database. Platform-wide, not tenant-scoped.

See API Reference for authentication, and the Data API reference for the response envelope, streaming and paging.

Both surfaces take a signed-in user’s token. The tenant surface is served on /backoffice-ep, and the platform surface has no access-endpoint segment. For server-to-server work with an access key, see the integration path.

Permissions

The caller needs one of these for each operation. The two surfaces have separate families with the same shape.

OperationTenant permissions (any one)Platform permissions (any one)
CreateTNT_API_DM_APPLET_AUDIT_TRAIL_OWNER, ..._ADMIN, ..._CREATEMST_API_DM_APPLET_AUDIT_TRAIL_OWNER, ..._ADMIN, ..._CREATE
Read..._OWNER, ..._ADMIN, TNT_API_DM_APPLET_AUDIT_TRAIL_READ..._OWNER, ..._ADMIN, MST_API_DM_APPLET_AUDIT_TRAIL_READ
Update..._OWNER, ..._ADMIN, TNT_API_DM_APPLET_AUDIT_TRAIL_UPDATE..._OWNER, ..._ADMIN, MST_API_DM_APPLET_AUDIT_TRAIL_UPDATE
Delete..._OWNER, ..._ADMIN, TNT_API_DM_APPLET_AUDIT_TRAIL_DELETE..._OWNER, ..._ADMIN, MST_API_DM_APPLET_AUDIT_TRAIL_DELETE

A platform administrator passes every check on the platform surface without holding any of its codes. A failed check answers CLIENT_AUTH_USER_NOT_AUTHORIZED, which is an authorisation problem rather than an authentication one.


Endpoints

OperationTenant pathPlatform path
CreatePOST /backoffice-epPOST /
UpdatePUT /backoffice-epPUT /
DeleteDELETE /backoffice-ep/{guid}DELETE /{guid}
List allGET /backoffice-epGET /
Get oneGET /backoffice-ep/{guid}GET /{guid}
QueryGET /backoffice-ep/queryGET /query

Reads behave slightly differently on the two surfaces, which is worth knowing if you are building a listing.

  • Tenant. Permission is checked once per request. A caller who passes the check sees every field of every record returned.
  • Platform. Records outside the caller’s permission are returned with "status": "PERMISSION_DENIED" and their remaining fields empty, rather than being omitted. This is a response-only marker, so do not send it back on a write.

The AppletAuditTrailContainer

The same shape on both surfaces, and the same shape a read returns.

{
  "bl_applet_audit_trail": {
    "guid": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
    "applet_guid": "6f1c9e10-3a44-4f2e-9a71-0c3d6b2f77aa",
    "applet_code": "salesInvoiceApplet",
    "action": "UPDATED",
    "foreign_table": "bl_fi_generic_doc_hdr",
    "foreign_key": "9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33",
    "date_txn": "2026-09-15T10:04:22+08:00",
    "message_json": { "payload": { } },
    "status": "ACTIVE",
    "revision": "0f3e7a52-1c6b-4a9d-8e21-7b4c9f0a1d26"
  }
}

PUT takes the whole record back, not a patch. A field you leave out is written as null.

bl_applet_audit_trail

FieldTypeOn createDescription
guidUUIDOptionalDefaults to a freshly generated UUID, which the response carries back. Required on update, and the same value addresses the record in GET /{guid} and DELETE /{guid}.
applet_guidUUIDOptionalThe applet the change happened in. If supplied it must name an applet that exists and is not DELETED.
applet_codestringOptionalThe applet’s registry code, for example salesInvoiceApplet. Shown as Applet Code on the Audit Trail screen.
actionstringOptionalWhat happened. BigLedger’s own writers use CREATED, UPDATED, DELETED or DISCARD. Shown as Action Name. Indexed.
foreign_tablestringOptionalThe table the audited record lives in, for example bl_fi_generic_doc_hdr. Shown as Table Name. Indexed.
foreign_keystringOptionalThe audited record’s guid, held as text in the database rather than as a UUID, so the query matches it character for character. Indexed, and the filter to reach for when the question is what happened to this document?
message_jsonobjectOptionalThe before-and-after payload. Nothing validates its shape, so the structure under What writes rows is a convention BigLedger’s own writers keep, not one the API enforces on you.
descrstringOptionalFree text, and there is no descr filter, so nothing you put here is searchable. On rows BigLedger writes itself it repeats action rather than describing the change — do not expect a sentence.
date_txndatetimeOptionalWhen the audited transaction happened, as distinct from when the record was written. Indexed.
txn_type, event_code, obj_typestringOptionalYour own classification of the change, in three independent slots. BigLedger’s own writers set none of them, so all three are null on every row the platform writes; each is queryable, matched exactly.
id_user, id_device, id_user_agentstringOptionalYour identifiers for who and what made the change — not BigLedger’s. The platform records its own actor in created_by_subject_guid and leaves these three null, so a filter on them returns only rows your integration created. Each indexed.
link_guidUUIDOptionalA free link to a related record. Nothing in BigLedger follows it, and nothing checks that it points anywhere — it exists so you can tie a row to something the audit trail does not otherwise know about. Queryable.
module_guidUUIDOptionalStored and returned, but it is not a working filter: a module_guid sent to /query is accepted and then ignored, with no error. Narrow by applet_guid or applet_code instead.
namespace, vrsnstringOptionalCarried unchanged, for callers running more than one application against a single tenant. Neither is set by BigLedger and neither is a query parameter, so a value here can be read back on the record but never searched for.
statusstringOptionalDefaults to ACTIVE. Every read excludes DELETED unless /query is asked for that status by name.
revisionUUIDOptionalDefaults to a freshly generated UUID. On update it is the optimistic lock: send the value your last read returned, and if the stored row has moved on since, the write is refused with RACE_CONDITION and nothing changes. A successful update returns a new revision — use that one next time.
created_date, updated_datedatetimeDefaultedBoth default to the moment of the write when you leave them out, rather than being forced. Because PUT takes the whole record, an update that omits created_date does not preserve it — it is reset to now. updated_date is overwritten with the current time on every update whatever you send.
created_by_subject_guidUUIDServer-assignedTaken from your credential on create. On update, a value in your body is kept, and your credential fills it only when you leave it null. Must name a real login subject either way.
updated_by_subject_guidUUIDServer-assignedAlways set from the credential that made the write, so it reliably identifies the caller.
created_by_name, updated_by_namestringServer-assignedResolved by looking those subjects up, never read from your body. Create sets both; update refreshes updated_by_name only. Shown as Action By.
property_json, acl_config, acl_policyobjectOptionalStored and returned exactly as you send them, and not interpreted. Despite the two names, they do not govern access to the record: that is decided by the permission codes above.

What writes rows

Most rows are not written by an API caller. BigLedger writes them itself when an audited operation runs, and those rows have a predictable shape.

  • action and descr both carry the operation, one of CREATED, UPDATED, DELETED, DISCARD
  • foreign_table and foreign_key identify the audited record
  • applet_guid and applet_code come from the caller’s token, and are set only when that token carries an applet
  • message_json is {"payload": { … }}, and the payload holds whichever of old_data, new_data and delete_data applied, plus an auth_token_fields object describing the caller

So a CREATED row carries new_data alone, an UPDATED row carries old_data and new_data so the two can be compared field by field, and a DELETED row carries delete_data, the removed record kept after the record itself is gone.


Querying

Both /query endpoints take the same parameters. The shared paging parameters first: limit (default 100), offset (default 0), orderBy (default the record’s guid), order (ASC or DESC) and calcTotalRecords, which must be true for totalRecords in the envelope to be anything but 0.

Every filter is an equality or a range. There is no partial match and no wildcard anywhere on this endpoint, so supply whole values rather than fragments. These eleven match exactly and need no further explanation: applet_guid, applet_code, foreign_table, foreign_key, link_guid, txn_type, event_code, obj_type, id_user, id_device, id_user_agent, created_by_subject_guid and updated_by_subject_guid.

The rest behave in ways their names do not tell you.

ParameterWhat to know
created_by_name, updated_by_nameExact and case-sensitive, against the actor’s stored profile name — not a login and not an e-mail address. This is what the Action By box on the Audit Trail screen sends, which is why that box returns nothing when the capitalisation is off by one letter.
statusOmit it and everything except DELETED comes back. Send one and you get that status alone — including status=DELETED, which is the only way to see rows a DELETE has soft-removed, because GET /{guid} and the listing never return them.
date_txn_from / date_txn_toAn inclusive range on date_txn — but BigLedger’s own writers never set date_txn, so this range excludes every row the platform wrote itself. Range on created_date when you are searching platform-written history.
created_date_from / created_date_to, updated_date_from / updated_date_toInclusive ranges on the write timestamps, which are always populated. created_date_from is the dependable way to bound a search by time.
action, descr, module_guidNot filters, which is the trap on this endpoint. action and descr have no query parameter at all, and a module_guid you send is accepted and silently dropped. To narrow by action, request the period you need and filter the results on your side.

Timestamps are ISO-8601 with an offset, as everywhere else in the API.

Errors on write

POST and PUT validate before anything is stored, on both surfaces, and a failure returns HTTP 400 with the failing checks in the envelope. The same five checks run for both methods, and the fifth, on the two actor fields, can return any of four codes, so there are eight in all.

codeCause
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_GUID_IS_NULLguid was null and could not be defaulted
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_STATUS_IS_NULL_OR_EMPTYstatus was null
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_REVISION_IS_NULLrevision was null
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_APPLET_GUID_DOES_NOT_EXISTSapplet_guid was supplied but names no applet, or names a DELETED one
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_CREATED_BY_SUBJECT_GUID_DOES_NOT_EXISTOn create, the subject on your credential is not a login subject. On update, the created_by_subject_guid in your body names no login subject.
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_UPDATED_BY_SUBJECT_GUID_DOES_NOT_EXISTThe subject on your credential is not a login subject.
API_PLATFORM_DM_APPLET_AUDIT_TRAIL_OBJECT_CREATED_BY_SUBJECT_GUID_IS_NULL, ..._UPDATED_BY_SUBJECT_GUID_IS_NULLDefined, but not reachable through these endpoints: both controllers fill the actor fields from your credential before validation runs, so you cannot trigger them by leaving the fields out.

DELETE returns CLIENT_VALIDATION_GUIDDOESNOTEXIST with HTTP 404 when no record carries that guid. The full list of code values is on the Error Codes page.


Worked examples, in order

The calls below follow one record through its life on the tenant surface: read the history BigLedger already wrote for a document, then create a row of your own, read it back, update it, see the update refused when your copy is stale, delete it, and find it again after the delete. Every example uses the same sales invoice, 9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33, number 1004382. The Audit Trail guide shows the same history on screen.

Every call sends the same three headers, shown in full in the first example only.

1. Read a document’s history, oldest first

GET /core2/tnt/dm/erp/audit-trails/backoffice-ep/query

Ask by foreign_key, bound the search with created_date_from, and sort by created_date. Leave orderBy out and the rows come back sorted by their own guid, which is random, so the history arrives out of order.

curl -G 'https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep/query' \
  --header 'Authorization: Bearer <token>' \
  --header 'tenantCode: gadgetsphere' \
  --data-urlencode 'foreign_table=bl_fi_generic_doc_hdr' \
  --data-urlencode 'foreign_key=9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33' \
  --data-urlencode 'created_date_from=2026-10-26T00:00:00+08:00' \
  --data-urlencode 'orderBy=created_date' \
  --data-urlencode 'order=ASC' \
  --data-urlencode 'calcTotalRecords=true'

The invoice was created, saved, edited, finalised and voided, which wrote seven rows. The response below keeps four of them and trims each snapshot to the fields that moved:

{
  "totalRecords": 7,
  "offset": 0,
  "limit": 100,
  "code": "OK_RESPONSE",
  "message": "",
  "data": [
    {
      "bl_applet_audit_trail": {
        "guid": "0b6e2d41-5c7a-4f18-9e33-7a1c4d8f2e90",
        "applet_code": "salesInvoiceApplet",
        "action": "CREATED",
        "descr": "CREATED",
        "foreign_table": "bl_fi_generic_doc_hdr",
        "foreign_key": "9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33",
        "date_txn": null,
        "message_json": {
          "payload": {
            "new_data": {
              "bl_fi_generic_doc_hdr": { "server_doc_1": null, "status": "TEMP", "posting_status": null },
              "bl_fi_generic_doc_line": []
            },
            "auth_token_fields": { }
          }
        },
        "created_by_name": "GadgetSphere Sales Clerk",
        "created_date": "2026-10-26T14:29:00+08:00"
      }
    },
    {
      "bl_applet_audit_trail": {
        "guid": "4d9f1a62-8b3e-4c27-a5d0-6e2b9c7f1a35",
        "action": "UPDATED",
        "message_json": {
          "payload": {
            "old_data": { "bl_fi_generic_doc_hdr": { "status": "TEMP", "posting_status": null } },
            "new_data": { "bl_fi_generic_doc_hdr": { "status": "ACTIVE", "posting_status": "DRAFT", "amount_txn": 0.00 } },
            "auth_token_fields": { }
          }
        },
        "created_date": "2026-10-26T14:31:29+08:00"
      }
    },
    {
      "bl_applet_audit_trail": {
        "guid": "7a2c5e18-3f6d-4b91-8c04-2d9e6a1b5f73",
        "action": "UPDATED",
        "message_json": {
          "payload": {
            "old_data": { "bl_fi_generic_doc_hdr": { "posting_status": "DRAFT", "date_finalized": null } },
            "new_data": { "bl_fi_generic_doc_hdr": { "posting_status": "FINAL", "date_finalized": "2026-10-26T14:40:21+08:00" } },
            "auth_token_fields": { }
          }
        },
        "created_date": "2026-10-26T14:40:40+08:00"
      }
    },
    {
      "bl_applet_audit_trail": {
        "guid": "c3e8b4f7-1a59-4d26-9f80-5b7d2e4a6c19",
        "action": "UPDATED",
        "message_json": {
          "payload": {
            "old_data": { "bl_fi_generic_doc_hdr": { "posting_status": "FINAL" } },
            "new_data": { "bl_fi_generic_doc_hdr": { "posting_status": "VOID" } },
            "auth_token_fields": { }
          }
        },
        "created_date": "2026-10-26T14:41:29+08:00"
      }
    }
  ]
}

What the full history teaches, beyond the four rows shown:

  • The CREATED row comes before anyone presses Save. The applet writes it when the create form opens, with status TEMP and no document number. An abandoned form leaves exactly this one row behind, so a CREATED row with no later rows is usually a form somebody opened and closed.
  • Finalise and void are UPDATED rows. Sales documents never write an action of their own for them; you find them by posting_status moving in old_data and new_data. On a test tenant the sales invoice applet had written only CREATED and UPDATED rows across 1,700 changes.
  • One action can write two rows. The finalise wrote two UPDATED rows in the same second, both starting from the same snapshot: one with only posting_status and date_finalized, the other also carrying the posting results and the extra document numbers.
  • Not every change writes a row. Each row’s old_data should match the previous row’s new_data. Here the void’s old_data carries a revision and updated_date that neither finalise row left, so the document changed once more in between without a row of its own, most likely the posting that runs after a finalise. Compare neighbouring snapshots before you treat the trail as complete.
  • old_data and new_data hold the whole document, not only its header: bl_fi_generic_doc_hdr plus the lines, events, links and extension rows. The screens show the header only, so this endpoint is the way to see which line changed.

2. Create a row of your own

POST /core2/tnt/dm/erp/audit-trails/backoffice-ep

An integration that changes a document outside BigLedger’s screens can record that it did. Here a till sync records that it applied a price override to the same invoice, using the three classification slots BigLedger itself leaves empty, so the rows stay easy to tell apart.

curl -X POST 'https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep' \
  --header 'Authorization: Bearer <token>' \
  --header 'tenantCode: gadgetsphere' \
  --header 'Content-Type: application/json' \
  --data '{
    "bl_applet_audit_trail": {
      "action": "UPDATED",
      "foreign_table": "bl_fi_generic_doc_hdr",
      "foreign_key": "9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33",
      "txn_type": "POS_SYNC",
      "event_code": "PRICE_OVERRIDE",
      "id_device": "GS-KV-01-TILL-03",
      "date_txn": "2026-10-26T15:02:10+08:00",
      "descr": "Till price override applied by sync",
      "message_json": {
        "payload": {
          "old_data": { "unit_price_txn": 12400.00 },
          "new_data": { "unit_price_txn": 11900.00 }
        }
      }
    }
  }'

The answer is the row as stored. The server filled guid, status, revision, both dates and both actor fields from your sign-in. Shortened:

{
  "code": "OK_RESPONSE",
  "data": {
    "bl_applet_audit_trail": {
      "guid": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
      "action": "UPDATED",
      "foreign_key": "9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33",
      "txn_type": "POS_SYNC",
      "status": "ACTIVE",
      "revision": "0f3e7a52-1c6b-4a9d-8e21-7b4c9f0a1d26",
      "created_date": "2026-10-26T15:02:11+08:00",
      "updated_date": "2026-10-26T15:02:11+08:00",
      "created_by_subject_guid": "5e2a9c71-4b8d-4f13-a6e0-9d3b7c1f8a24",
      "created_by_name": "GadgetSphere Till Sync",
      "updated_by_name": "GadgetSphere Till Sync"
    }
  },
  "message": ""
}

Keep guid and revision: the next three calls need them. The row now appears in step 1’s history, and on the screens under the applet_code you sent, or under no applet if you sent none.

3. Read it back

GET /core2/tnt/dm/erp/audit-trails/backoffice-ep/{guid}

curl 'https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed' \
  --header 'Authorization: Bearer <token>' \
  --header 'tenantCode: gadgetsphere'

The answer is the same envelope as the create, with the whole row. Shortened:

{
  "code": "OK_RESPONSE",
  "data": {
    "bl_applet_audit_trail": {
      "guid": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
      "action": "UPDATED",
      "foreign_key": "9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33",
      "descr": "Till price override applied by sync",
      "status": "ACTIVE",
      "revision": "0f3e7a52-1c6b-4a9d-8e21-7b4c9f0a1d26",
      "created_by_name": "GadgetSphere Till Sync"
    }
  },
  "message": ""
}

A guid that does not exist, or names a deleted row, answers HTTP 404 with CLIENT_VALIDATION_GUIDDOESNOTEXIST (step 6).

4. Update it

PUT /core2/tnt/dm/erp/audit-trails/backoffice-ep

Send the whole row back with the revision from your last read, not only the field you are changing. A field you leave out is written as null, and a missing created_date is reset to now. Here the sync corrects its description:

curl -X PUT 'https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep' \
  --header 'Authorization: Bearer <token>' \
  --header 'tenantCode: gadgetsphere' \
  --header 'Content-Type: application/json' \
  --data '{
    "bl_applet_audit_trail": {
      "guid": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
      "action": "UPDATED",
      "foreign_table": "bl_fi_generic_doc_hdr",
      "foreign_key": "9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33",
      "txn_type": "POS_SYNC",
      "event_code": "PRICE_OVERRIDE",
      "id_device": "GS-KV-01-TILL-03",
      "date_txn": "2026-10-26T15:02:10+08:00",
      "descr": "Till price override applied by sync, approved by branch manager",
      "message_json": {
        "payload": {
          "old_data": { "unit_price_txn": 12400.00 },
          "new_data": { "unit_price_txn": 11900.00 }
        }
      },
      "status": "ACTIVE",
      "revision": "0f3e7a52-1c6b-4a9d-8e21-7b4c9f0a1d26",
      "created_date": "2026-10-26T15:02:11+08:00",
      "created_by_subject_guid": "5e2a9c71-4b8d-4f13-a6e0-9d3b7c1f8a24"
    }
  }'

The answer is the row as saved, with a new revision and updated_date set to the time of the write. Use the new revision for any later update. Shortened:

{
  "code": "OK_RESPONSE",
  "data": {
    "bl_applet_audit_trail": {
      "guid": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
      "descr": "Till price override applied by sync, approved by branch manager",
      "revision": "6c41d8e2-7f3a-4b59-a0d6-2e8f5b9c3a17",
      "updated_date": "2026-10-26T15:20:44+08:00"
    }
  },
  "message": ""
}

5. Send a stale revision, and see it refused

Send step 4’s body again unchanged, still carrying the revision the row had before step 4. The stored row has moved on, so nothing is written and the call answers HTTP 400:

{
  "code": "EXCEPTION_INCONSISTENT_DATA",
  "message": null,
  "data": [
    {
      "errorCode": "RACE_CONDITION",
      "shortMessage": "Race condition occurred, you are updating an old version of the data",
      "longMessage": "Race condition occurred, you are updating an old version of the data",
      "targetedObject": { }
    }
  ]
}

Read the row again (step 3), apply your change to what it returns, and send that. Retrying the same body will fail the same way.

6. Delete it, then find it again

DELETE /core2/tnt/dm/erp/audit-trails/backoffice-ep/{guid}

curl -X DELETE 'https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep/1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed' \
  --header 'Authorization: Bearer <token>' \
  --header 'tenantCode: gadgetsphere'
{
  "code": "OK_RESPONSE",
  "data": true,
  "message": ""
}

The delete is soft: the row stays in the table with status set to DELETED. From then on, step 3 for the same guid answers HTTP 404, a second DELETE answers 404, and step 1 no longer lists it:

{
  "code": "CLIENT_VALIDATION_GUIDDOESNOTEXIST",
  "data": null,
  "message": ""
}

The only way back to it is /query asked for that status by name:

curl -G 'https://api-etl.akaun.com/core2/tnt/dm/erp/audit-trails/backoffice-ep/query' \
  --header 'Authorization: Bearer <token>' \
  --header 'tenantCode: gadgetsphere' \
  --data-urlencode 'foreign_key=9c2a5f31-77b2-4d0a-8f4e-2b6a1d5c8e33' \
  --data-urlencode 'status=DELETED'

The response holds the deleted row. totalRecords is 0 because the call did not send calcTotalRecords=true. Shortened:

{
  "totalRecords": 0,
  "offset": 0,
  "limit": 100,
  "code": "OK_RESPONSE",
  "message": "",
  "data": [
    {
      "bl_applet_audit_trail": {
        "guid": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
        "descr": "Till price override applied by sync, approved by branch manager",
        "status": "DELETED"
      }
    }
  ]
}

The deleted row comes back with "status": "DELETED" and everything else as it was. So a delete hides a row from every ordinary read but does not destroy it. Anyone holding the DELETE permission can still hide evidence from the screens, which is why the Scope section says to grant it deliberately.


Scope

  • Credentials. These endpoints are served for signed-in users. The server-to-server surface an access key reaches is described in the integration path and the Data API reference.
  • Write access. POST, PUT and DELETE are available to callers holding the matching permission in the table above. Grant those permissions deliberately, and keep read-only integrations on the READ permission alone.
  • Coverage. Records are written for the operations that support auditing. If a search returns nothing, widen the date range and re-check applet_code and foreign_table before drawing a conclusion.
  • Other audit trails. Claims, entities, journal lines, cashbook transaction lines, memberships and several other records keep their own audit trails. This page covers the applet audit trail only.

Related

  • Tenant Admin Applet covers the Audit Trail screen this API serves: its listing columns, advanced search and detail view.
  • Audit Trail guide walks both screens through as an investigation, for a reader who is not calling the API.
  • API Reference lists the API families, and which of them have a page.
  • Authentication covers what may go in the Authorization header, and where each credential works.
  • Error Codes lists every code value the API can return.
Last updated on