Skip to content
AXOWLDevelopers
Quickstart

Seal anything into the audit ledger

Your app does something that matters — a meeting recording is finalized, a contract is signed, an order ships — and you want proof later that it happened, when, and what exactly it was. Saying “sealed” in your own database proves nothing: you own that database. The proof has to live somewhere you can’t edit.

POST /api/public/apps/{applicationKey}/ledger/events writes one event into your organization’s audit ledger — the same sealed, write-once trail the console shows under Audit → custom / business events. Each event is chained and signed by Axowl’s sealer the moment it is written. Later, anyone holding the event id can ask Axowl (not your database) what was recorded, and compare.

  • An org API key (ah_live_…) from Console → Organization Settings → API Keys. Call this endpoint from your server — never ship the key to a browser or mobile app.
  • Your application key (client_…) from Console → Application. The API key’s organization must own that application. A key limited to one App Group or one application works only for that App Group or application.
  • If the key lists allowed scopes, that list must include ledger.write to seal events. A key with no allowed scopes listed can seal as before. Reading an event back needs no extra scope.
Terminal window
curl -X POST "https://api.axowl.com/api/public/apps/client_xxxxxxxxxxxxxxxxxxxx/ledger/events" \
-H "X-Api-Key: ah_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"type": "custom.recording_sealed",
"subjectId": "meeting_2026_11_09",
"sha256": "9f2c4e…64 hex characters…",
"data": { "durationSec": 3600, "format": "mp4" }
}'
FieldRequiredRules
typeyescustom. followed by lowercase letters, digits or _. Dots separate up to 4 parts after custom., each 1–40 characters (custom.sap.invoice.posted). 100 characters max.
subjectIdnoWhat the event is about — your recording id, contract id, order id. 200 characters max.
sha256noThe SHA-256 of the thing you are sealing (file bytes, canonical JSON), as 64 hex characters. Stored in lowercase. This is what you will compare later.
datanoAny JSON object, 16 KB max. Sealed together with the rest.

Pick event names that describe a kind of event (custom.recording_sealed, custom.contract_signed) and keep ids and values in subjectId / data. The whole request body is capped at 32 KB.

Response — 201 Created

{
"id": "0193a0f2-5c1e-7b3a-9d2e-1f4c8a7b6e21",
"type": "custom.recording_sealed",
"subjectId": "meeting_2026_11_09",
"sha256": "9f2c4e…",
"data": { "durationSec": 3600, "format": "mp4" },
"createdAt": "2026-11-09T05:12:44.318Z",
"sealed": true,
"seal": {
"chainHash": "…",
"signature": "…",
"algorithm": "HS256",
"payloadHash": "…"
},
"integrity": "sealed",
"source": "ledger"
}

Store the id. It is the handle for verification. Each call creates a new ledger entry — if you retry after a network error, you may get two entries for the same subject; both are kept (the ledger is append-only).

2. Verify it later — against Axowl, not your database

Section titled “2. Verify it later — against Axowl, not your database”

GET /api/public/apps/{applicationKey}/ledger/events/{id} returns what was recorded for that id, with the same X-Api-Key rule as the write.

Terminal window
curl "https://api.axowl.com/api/public/apps/client_xxxxxxxxxxxxxxxxxxxx/ledger/events/0193a0f2-5c1e-7b3a-9d2e-1f4c8a7b6e21" \
-H "X-Api-Key: ah_live_xxxxxxxxxxxxxxxx"

The response has the same shape as above, with two fields that tell you how the answer was checked:

sourceintegrityMeaning
hot_cacheverifiedRecent event (about 3 days). Axowl re-hashed the stored row and it matches its seal.
hot_cachetamperedThe stored row no longer matches its seal. Treat the record as compromised and contact support.
archivearchivedOlder event, read from the write-once archive (WORM). seal.algorithm and seal.payloadHash are null here.

A verify page then does one comparison: hash the file or document you have now, and check it equals the sha256 Axowl returned. If they differ, what you hold is not what was sealed.

const res = await fetch(`${API}/api/public/apps/${APP_KEY}/ledger/events/${id}`, {
headers: { 'X-Api-Key': process.env.AXOWL_API_KEY! },
});
const recorded = await res.json();
const ok = recorded.sealed && recorded.integrity !== 'tampered' && recorded.sha256 === sha256Hex(currentBytes);

Show your users what Axowl returned — createdAt, sha256 and integrity — rather than a bare “sealed” badge.

StatuserrorWhen
400INVALID_JSON, INVALID_TYPE, INVALID_SUBJECT_ID, INVALID_SHA256, INVALID_DATAThe body or one field does not follow the rules above.
401—Missing, unknown or revoked X-Api-Key.
402code: UPGRADE_REQUIREDRead only: the event is older than your plan’s audit retention window.
403insufficient_scopeThe API key lists allowed scopes and ledger.write is not one of them (write only).
404NOT_FOUNDUnknown application, an application your key doesn’t own, or an id that isn’t one of this application’s ledger events. Another organization’s ids always answer 404.
413DATA_TOO_LARGEdata over 16 KB. A body over 32 KB also answers 413, without a JSON body.
429—More than 60 requests a minute from one IP.
503SEAL_UNAVAILABLE, ARCHIVE_UNAVAILABLESealing or the archive is briefly unavailable. Nothing was written; retry.

Console → Audit: the event appears with your type, actor external (written with an API key), and the subjectId / sha256 / data in its detail. It follows your plan’s audit retention like every other audit event.

Already using the gRPC SDK? EventStreamService.Publish (Eventing) writes to the same ledger through the same sealer — this endpoint is the plain-HTTP door to it.