> Source: https://wexa.ai/docs/administration/audit-and-compliance

# 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

```bash
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:

```bash
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:

```json
{
  "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](/docs/concepts/context-graph) and the
[lifecycle record](/docs/concepts/lifecycle-and-audit) 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](#approval-events-in-the-platform-audit-trail).

## Verifying integrity

```bash
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:

1. **Poll `GET /v1/audit` on 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.
2. **Call `GET /v1/audit/verify` at 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.
3. **Record the gateway's start time with each archive.** An archive whose first event identifier is
   `aud_000001` began 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:

```json
{ "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](/docs/concepts/lifecycle-and-audit) and
[policy and approvals](/docs/concepts/policy-and-approvals).
