Skip to main content

Chain Integrity

Stateway's audit log is tamper-evident. Each entry includes a cryptographic hash (entryHash) computed over the entry's own content plus the hash of the immediately preceding entry (prevEntryHash). This creates an unbroken chain: deleting, inserting, or modifying any entry in the middle produces a detectable mismatch.

How it works​

When an audit log entry is written:

  1. Stateway fetches the entryHash of the most recent entry for the tenant.
  2. It computes a new hash over the combined content of the new entry and the previous hash.
  3. Both prevEntryHash and entryHash are stored alongside the entry.

The first entry of a tenant has no predecessor; its entryHash is computed using the sentinel value "GENESIS" in place of prevEntryHash.

No application code path ever deletes or updates audit log entries.

Verifying the chain​

GET /v1/audit/verify re-computes and validates the hash chain over a time range:

curl "https://api.stateway.io/v1/audit/verify?from=2026-05-01T00:00:00Z&to=2026-05-31T23:59:59Z" \
-H "X-API-Key: sw_live_your_key"

Valid chain​

{
"data": {
"status": "valid",
"partial": false,
"entries_verified": 1420,
"first_entry_id": "log_01j...",
"last_entry_id": "log_99j...",
"from": "2026-05-01T00:00:00Z",
"to": "2026-05-31T23:59:59Z"
}
}

Tampered chain​

{
"data": {
"status": "tampered",
"partial": false,
"entries_verified": 89,
"first_broken_entry_id": "log_45j...",
"break_detected_at": "2026-05-15T14:32:10Z",
"detail": "entry_hash mismatch: expected a3f1... got 9b2c..."
}
}

status: "tampered" means verification stopped at the entry identified by first_broken_entry_id. All entries before that point validated correctly.

Want to recompute and check the chain yourself, without calling this endpoint? See Hash Chain Algorithm for the exact, reproducible algorithm behind entryHash.

Verifying a partial window​

from does not need to match the tenant's actual first audit entry. The endpoint anchors the check on the entryHash of the entry immediately preceding from — not on the "GENESIS" sentinel — so a window that starts in the middle of the tenant's history still validates correctly when the chain is intact. If no entry precedes from (the window reaches back to the real start of the chain), the anchor is "GENESIS", same as described above.

This is why verifying one window at a time (see Performance below) is safe: each window is checked against the real chain, not treated as if it were the whole history.

A chain link broken detail (as opposed to entry_hash mismatch) means an entry's stored prevEntryHash does not match the hash of the entry that precedes it — for example, an entry was deleted from between two others.

Note: this is a different reconstruction than Hash Chain Algorithm describes for an exported audit log. An export only carries the rows in its own range, so a reader reconstructing the chain from an export has no choice but to seed prevHash from the first exported row's own prev_entry_hash. This endpoint has direct database access, so it seeds from the real predecessor instead — a stronger check, since forging the first verified entry would require forging the entire chain back to GENESIS.

What to do if tampering is detected​

A status: "tampered" result indicates that one or more audit log entries were modified, deleted, or inserted outside the normal application path. Preserve the response body as evidence and contact Stateway support.

Parameters​

ParameterTypeRequiredDescription
fromISO 8601YesStart of the time range to verify
toISO 8601YesEnd of the time range to verify

Performance​

Verification reads entries sequentially and re-computes each hash in memory. The endpoint verifies up to 10,000 entries per call.

When the time range holds more than 10,000 entries, only the first 10,000 are verified and the response says so with "partial": true. In that case status: "valid" covers only the entries verified — not the whole range — and next_from holds the timestamp of the first entry left unverified:

{
"data": {
"status": "valid",
"partial": true,
"next_from": "2026-05-12T09:41:07.312Z",
"entries_verified": 10000,
"first_entry_id": "log_01j...",
"last_entry_id": "log_7fj...",
"from": "2026-05-01T00:00:00Z",
"to": "2026-05-31T23:59:59Z"
}
}

To continue, repeat the call with from set to next_from and the same to, until a response comes back with "partial": false. Each call is anchored on the entry immediately preceding its from (see Verifying a partial window), so consecutive calls check the chain without gaps. The whole range is verified only when every call returns status: "valid" and the last one returns "partial": false.

curl "https://api.stateway.io/v1/audit/verify?from=2026-05-12T09:41:07.312Z&to=2026-05-31T23:59:59Z" \
-H "X-API-Key: sw_live_your_key"

You can also verify in smaller windows up front:

# Verify one week at a time
curl "https://api.stateway.io/v1/audit/verify?from=2026-05-01T00:00:00Z&to=2026-05-07T23:59:59Z" \
-H "X-API-Key: sw_live_your_key"

Chain verification is designed for periodic compliance audits and spot checks — not continuous polling.