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.
Before you start
Section titled “Before you start”- 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.writeto seal events. A key with no allowed scopes listed can seal as before. Reading an event back needs no extra scope.
1. Seal an event
Section titled “1. Seal an event”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" } }'| Field | Required | Rules |
|---|---|---|
type | yes | custom. 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. |
subjectId | no | What the event is about — your recording id, contract id, order id. 200 characters max. |
sha256 | no | The 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. |
data | no | Any 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.
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:
source | integrity | Meaning |
|---|---|---|
hot_cache | verified | Recent event (about 3 days). Axowl re-hashed the stored row and it matches its seal. |
hot_cache | tampered | The stored row no longer matches its seal. Treat the record as compromised and contact support. |
archive | archived | Older 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.
Errors
Section titled “Errors”| Status | error | When |
|---|---|---|
| 400 | INVALID_JSON, INVALID_TYPE, INVALID_SUBJECT_ID, INVALID_SHA256, INVALID_DATA | The body or one field does not follow the rules above. |
| 401 | — | Missing, unknown or revoked X-Api-Key. |
| 402 | code: UPGRADE_REQUIRED | Read only: the event is older than your plan’s audit retention window. |
| 403 | insufficient_scope | The API key lists allowed scopes and ledger.write is not one of them (write only). |
| 404 | NOT_FOUND | Unknown 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. |
| 413 | DATA_TOO_LARGE | data 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. |
| 503 | SEAL_UNAVAILABLE, ARCHIVE_UNAVAILABLE | Sealing or the archive is briefly unavailable. Nothing was written; retry. |
Where it shows up
Section titled “Where it shows up”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.