Audit and compliance
Wexa records consequential events in a hash-chained trail and will verify that chain on request. This page covers retrieving it, reading a record, verifying it, and then the part that matters most to anyone preparing for an audit: the limits of what that trail can actually evidence.
Retrieving the trail
curl $FABRIC_URL/v1/audit -H "Authorization: Bearer $TOKEN"
There is one endpoint and it takes no parameters. GET /v1/audit returns every event visible to
the caller, as a single JSON array, in the order it was written. There is no date range, no
filter by actor or action, no pagination and no export. A limit or actor query string is
accepted by the URL and ignored by the handler — a request with one returns exactly the same
events as a request without.
So "produce the evidence" means: fetch the whole array and filter it yourself. For an auditor this is workable, and you should plan for it:
curl -s $FABRIC_URL/v1/audit -H "Authorization: Bearer $TOKEN" \
> audit-$(date +%Y%m%dT%H%M%S).json
Keep the file. It is the only copy that will exist tomorrow.
Who can read it
Visibility is by scope, not by role. An event is returned when its organization matches the caller's, and when its project matches the caller's project if the caller's token names one.
What a record contains
One real event, unedited:
{
"id": "aud_000016",
"ts": "2026-09-15T15:56:18.332011859+05:30",
"actor": "bob",
"role": "PROJECT_MEMBER",
"action": "create-ontology.approval.redeemed",
"target": "ar_6abb9f3c0b5ae73d6cdd6d41",
"source": "rest",
"kind": "Approvals",
"lifecycle_id": "qlc_000005",
"org_id": "org_d18",
"project_id": "proj_d18",
"payload": { "args_keys": ["mode", "domain", "nodes"] },
"payload_hash": "022689d55b5c02b6148cc266d9f9d6fc9ea105d3f16ad5eb6dbb66a19e4d2692",
"prev_hash": "b6e28fdd59d5f3589d8aa7fe17a2b08957d55d2ec396b120d080c5fe3ee3f706",
"hash": "9807e8645c13740fc158a50e392dcd91f762ecd57bf882fc94c3da158a4eedfe"
}
Events carry one of five kinds — Auth, Tool calls, Policy, Approvals, System — which is
the closest thing to a category filter you get.
Note what the payload is and is not. For an executed tool call the payload records
args_keys: the names of the arguments, not their values. For a refusal it records the stage and
the reason. So the trail evidences that a call was made and which fields it carried, not what
was in them. An auditor asking "what data did this call touch" cannot be answered from the trail
alone; they have to be pointed at the context graph and the
lifecycle record instead.
The lifecycle_id is the thread. Every event a single call produced carries the same one, so a
held call's request, redemption and execution can be joined, together with its decision when the
decision was made over REST. A decision made in the console's Approvals Inbox is recorded in the
platform audit trail described below.
Verifying integrity
curl $FABRIC_URL/v1/audit/verify -H "Authorization: Bearer $TOKEN"
# {"broken_at":"","chain_intact":true}
Each event's prev_hash is the previous event's hash, so an edited or removed record breaks the
chain from that point onward. chain_intact is the verdict; broken_at names the first event that
failed, and is empty when nothing did.
Verification performs three checks per event: the payload re-hashes to payload_hash; prev_hash
equals the previous event's hash; and the event re-hashes to hash.
What the hash actually attests
The event hash is computed over nine fields:
id · ts · actor · action · target · kind · lifecycle_id · payload_hash · prev_hash
Five recorded fields are not in it:
| Field | Recorded | Attested |
|---|---|---|
role | Yes | No |
source | Yes | No |
org_id | Yes | No |
project_id | Yes | No |
trace_id | Yes | No |
This has a consequence you must be able to state. A passing verification proves that who did what,
when, in which order has not been altered. It does not prove which project or which
organization an event belongs to, nor which role the actor held, nor whether the call arrived over
REST or the Wexa MCP server. Any compliance claim of the form "this evidence shows the event
belonged to project X" is not supported by the chain — the project identifier could be changed
without chain_intact ever turning false.
Durability — the part to read twice
The trail is an append-only slice in the gateway's memory. It is not written to a database, a file or object storage. A comment in the source describes production flushing the chain to write-once storage; no code does this.
Executed, in this order, against one gateway:
# before
GET /v1/audit -> 144 events
GET /v1/audit/verify -> {"broken_at":"","chain_intact":true}
# gateway process stopped and started again, same binary, same port
# after
GET /v1/audit -> {"events":[]}
GET /v1/audit/verify -> {"broken_at":"","chain_intact":true}
Two things to take from that.
One hundred and forty-four records became zero, and nothing reported an error. A restart, a crash, a deployment, a pod rescheduling — each of these destroys the trail silently. The gateway is in every other respect fine afterwards.
An empty chain verifies as intact. chain_intact: true on a trail that has just lost everything
is indistinguishable from chain_intact: true on a complete one. Verification answers "has what is
here been altered", never "is what should be here still here". Do not use it as an
"evidence is present" check.
Approval events in the platform audit trail
Approval events are also written to the platform audit trail, which is stored and survives a gateway restart. For every held call the trail records the approval being requested, decided, consumed when its resume token is used, and expired. A decision made over REST carries a flag that says whether it was self-approved. Requests and decisions for every other kind of approval are recorded there too.
Read it in the console under Governance → Audit & Lineage.
What you can and cannot give an auditor
Stated as plainly as it deserves.
You can give them:
- A complete, ordered, tamper-evident record of consequential events since the gateway last restarted — every tool call, every refusal with its stage and reason, and every credential issue and revoke.
- Every approval's request and decision, with both parties, and for a held call its use and expiry, from the platform audit trail. These do not depend on when the gateway last restarted.
- A verification result over that record.
- The lifecycle identifier joining the events of a single call.
You cannot give them, today:
- Gateway events from before the last restart, other than the approval events above. There is no retention period for them because there is no retention.
- A cryptographic assurance that an event belongs to a particular project or organization, or that the actor held a particular role — those fields are outside the hash.
- A filtered or date-ranged extract from the platform. You extract everything and filter it yourself.
- Argument values. Only argument names are recorded.
- An assurance the record is complete, from the platform. Nothing detects a gap.
Making this workable
Until the trail is durable, the retention is whatever you build around it:
- Poll
GET /v1/auditon a schedule and archive each response, with a timestamp in the filename, into storage you control and that your retention policy actually covers. A minute's interval costs one request. - Call
GET /v1/audit/verifyat the moment of each capture and archive the verdict beside the events. A verification run weeks later against an archived copy proves nothing about the gateway; run at capture time it proves the gateway's own chain was intact when you took it. - Record the gateway's start time with each archive. An archive whose first event identifier is
aud_000001began at a restart, and the gap before it is the part you cannot evidence.
The other governance surface
Policy decisions are recorded separately and retrieved separately, at
GET /v1/policy-decisions. Every evaluation appears there, including the ones that matched no rule:
{ "id": "pd_000001", "subject": "bob", "tool": "create-ontology",
"resource": "proj_d18", "verdict": "allow", "rule": "default",
"reasoning": "no rule matched; default allow", "lifecycle_id": "qlc_000002" }
That is worth reading before an audit, because it makes the platform's posture explicit in its own words: a call that no rule addressed is allowed, and recorded as having been allowed by default. The decision log is filtered by organization only — not by project — and it is not durable either.
For the concept treatment of the chain and the lifecycle it belongs to, see lifecycle and audit and policy and approvals.