> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Governance

> Manage actor PII mappings, list audit-log archives and download archive objects, and re-verify Matcher's tamper-evident audit hash chain.

Matcher's governance surface groups three capabilities under `/v1/governance`: **actor mappings** (link opaque actor IDs to PII, with pseudonymize and delete operations), **archives** (list completed audit-log archives and download archive objects from object storage), and **audit logs** (immutable, hash-chained history with a read-only integrity check). With `AUTH_PROVIDER=plugin-auth` in multi-tenant mode, tenant identity comes from the JWT; Matcher rejects startup when `MULTI_TENANT_ENABLED=true` and `PLUGIN_AUTH_ENABLED=false`. `workos` currently resolves verified requests to the configured default tenant, so do not use it for tenant selection.

<Note>Every governance route is scoped to the caller's tenant. Actor-mapping reads are split into two authorization tiers: the list (which omits `displayName` and `email`) versus the single-record de-anonymization read, so identity resolution stays separable from browse access.</Note>

## Actor mappings

***

An actor mapping links an opaque `actorId` (for example `user:550e8400-e29b-41d4-a716-446655440000`) to human-readable PII (`displayName`, `email`). Outside local, development, and test environments, set `ACTOR_PII_ENCRYPTION_KEY` to a base64-encoded 32-byte key before using actor mappings. If it is unset, Matcher continues to run, but PII-bearing mapping operations (upsert, single-record read, and pseudonymization) return an encryptor-required error; the PII-free list and delete paths do not require an encryptor. Mapping PII is never stored in plaintext. List rows omit the mapping PII fields (`displayName`, `email`) **by design**, but they do return the `actorId` itself. On upsert, Matcher trims leading and trailing whitespace and rejects empty or whitespace-only IDs and IDs longer than 255 characters. It does not impose an opaque-ID format or redact the value, so an `actorId` that itself contains PII (such as an email address) appears in list rows as its stored value and is preserved by pseudonymization; use opaque identifiers if list access must stay PII-free. The `PUT` response and the single-record `GET` return cleartext identity. Only the single-record `GET` is gated behind the `deanonymize` permission: the `PUT` response is gated by write access alone and echoes the full stored record, including any stored field the caller did not submit, so treat actor-mapping write access as PII-revealing. Audit logs can retain the raw `actorId`, which can be an email address.

### List actor mappings

Cursor-paginated rows that omit `displayName` and `email`. Filter by an actor-ID prefix.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/actor-mappings?actorId=user:&limit=25" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "items": [
    {
      "actorId": "user:550e8400-e29b-41d4-a716-446655440000",
      "createdAt": "2026-01-15T10:30:00Z",
      "updatedAt": "2026-01-15T10:30:00Z"
    }
  ],
  "limit": 25
}
```

Query parameters: `actorId` (prefix filter), `limit` (default 25, capped at 100), and `cursor`.

### Upsert an actor mapping

Creates or updates the PII for an actor ID. `PUT` is idempotent — the same call creates the record on first use and updates it thereafter. At least one of `displayName` or `email` must be supplied.

```bash theme={null}
curl -X PUT "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "John Doe",
    "email": "john.doe@example.com"
  }'
```

```json theme={null}
{
  "actorId": "user:550e8400-e29b-41d4-a716-446655440000",
  "displayName": "John Doe",
  "email": "john.doe@example.com",
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-01-15T10:30:00Z"
}
```

### Get one actor mapping (de-anonymize)

Returns the cleartext PII for a single actor ID. This **is** the de-anonymization primitive, so it is gated behind the narrower `deanonymize` permission rather than plain read.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \
  -H "Authorization: Bearer $TOKEN"
```

### Pseudonymize

Replaces the mapping's `displayName` and `email` with `[REDACTED]` while preserving the record and its `actorId` link. This scrubs PII from the mapping only: immutable audit records keep the raw `actorId` they were written with (which can itself be an email address), so historical audit logs and archived files are not redacted. Responds `204 No Content`.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}/pseudonymize" \
  -H "Authorization: Bearer $TOKEN"
```

### Delete a mapping

Permanently removes the mapping. Responds `204 No Content`.

```bash theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \
  -H "Authorization: Bearer $TOKEN"
```

<Note>Pseudonymize keeps the record (PII scrubbed); delete removes it entirely. Choose pseudonymize when you must retain the audit linkage, delete when the record itself must not persist.</Note>

## Archives

***

The archival worker is disabled by default (`ARCHIVAL_WORKER_ENABLED=false`). When you enable it and configure archival storage, aging audit-log partitions are compressed and moved to object storage. Archive retrieval routes are registered when archival object storage is available, independently of whether the worker is enabled. The list endpoint returns completed archives; the download endpoint issues time-limited URLs for archive objects.

### List archives

Offset-paginated. Filter by date range.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/archives?from=2024-01-01&to=2024-03-31&limit=20&offset=0" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "items": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "partitionName": "audit_logs_2024_q1",
      "dateRangeStart": "2024-01-01T00:00:00Z",
      "dateRangeEnd": "2024-03-31T23:59:59Z",
      "rowCount": 150000,
      "compressedSizeBytes": 10485760,
      "storageClass": "GLACIER",
      "checksum": "sha256:abc123def456...",
      "status": "COMPLETE",
      "archivedAt": "2024-04-01T02:30:00Z"
    }
  ],
  "limit": 20,
  "hasMore": true
}
```

Query parameters: `from`, `to` (`YYYY-MM-DD` or RFC 3339), `limit` (1–200, default 20), and `offset`. Only `COMPLETE` archives are listed — in-progress and failed archives are never surfaced.

### Download an archive

Returns a presigned URL plus the checksum for integrity verification.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/archives/{id}/download" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "downloadUrl": "https://s3.amazonaws.com/bucket/archive.gz?X-Amz-Signature=...",
  "expiresAt": "2026-02-05T13:00:00Z",
  "checksum": "sha256:abc123def456..."
}
```

<Warning>The download handler confirms tenant ownership but does not check `COMPLETE`; it presigns the stored `archiveKey`. That key and its checksum are assigned when an archive reaches `UPLOADED`, before `COMPLETE`. Treat `GET /v1/governance/archives/{id}/download` as a direct object-key path, not proof that archival completed. If you need only completed archives, select IDs from the list endpoint.</Warning>

<Warning>Archive availability depends on the configured S3-compatible storage backend and lifecycle policy. Confirm any restore requirement with the owner of that storage deployment before relying on a download URL.</Warning>

## Audit logs

***

Instrumented governance workflows write immutable, per-tenant audit records. Each record is linked into a tamper-evident SHA-256 hash chain (`recordHash` = `SHA-256(prevHash || canonical content)`), so inconsistent changes are detectable. Use the verify endpoint below for the server-side integrity verdict.

### List audit logs

Cursor-paginated, with rich filters.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs?actor=user@example.com&action=CREATE&entity_type=context&date_from=2025-01-01&date_to=2025-01-31&limit=20" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "items": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "tenantId": "550e8400-e29b-41d4-a716-446655440001",
      "entityType": "reconciliation_context",
      "entityId": "550e8400-e29b-41d4-a716-446655440002",
      "action": "CREATE",
      "actorId": "user@example.com",
      "changes": { },
      "truncated": false,
      "originalSize": 0,
      "createdAt": "2025-01-15T10:30:00Z",
      "tenantSeq": 1,
      "recordHash": "dd3f8a09dda3a8fdcd1e5c54ef76a9168bbabbfd92ad1dd736400d03a3f8a585",
      "prevHash": "0000000000000000000000000000000000000000000000000000000000000000",
      "hashVersion": 1
    }
  ],
  "limit": 20,
  "hasMore": false
}
```

Query parameters: `actor`, `action`, `entity_type`, `date_from`, `date_to` (`YYYY-MM-DD` or RFC 3339), `limit` (1–200, default 20), and `cursor`.

`actor` filters on the record's `actorId` value: the raw actor identifier captured when the record was written (an email address in this example). Audit records store that identifier as-is; it is not required to be an actor-mapping `actorId`.

When a diff exceeds the outbox payload cap, `changes` carries a truncation-marker envelope instead of the full diff, and `truncated` becomes `true` with `originalSize` reporting the pre-truncation byte size.

### Verify the audit chain

Re-verifies that every inspected record links to the previous one and matches its stored hash. Verification walks a contiguous span from the chain start, bounded by `maxRecords`, so `intact` speaks only for that inspected span. At the HTTP endpoint, a supplied `maxRecords` must be 1–10,000 (values outside that range return `422`); when omitted, Matcher uses the 10,000-record default. The check is strictly read-only: it detects tampering, never mutating a record.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/verify?maxRecords=10000" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "intact": true,
  "verifiedCount": 1024,
  "truncated": false
}
```

`intact` is `true` when the whole inspected span is unbroken; if a break is found, `firstBrokenSeq` reports the `tenantSeq` of the first failing record and `verifiedCount` reports how many held before it. `truncated` is `true` when the chain holds more records than the `maxRecords` inspection bound allowed.

### Get one audit log

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/{id}" \
  -H "Authorization: Bearer $TOKEN"
```

<Note>You can also list an entity's history directly with `GET /v1/governance/entities/{entityType}/{entityId}/audit-logs` (cursor-paginated), which is convenient when you already know the entity you are auditing.</Note>

## Response codes

***

| Status | Meaning                                                                                   |
| ------ | ----------------------------------------------------------------------------------------- |
| `200`  | Mapping, archive, or audit data returned                                                  |
| `204`  | Actor mapping pseudonymized or deleted                                                    |
| `400`  | Application-level invalid input (missing displayName/email or an invalid date)            |
| `403`  | Missing the required permission tier (e.g. `deanonymize` for single-record PII)           |
| `404`  | Actor mapping, archive, or audit log not found                                            |
| `422`  | Request/schema validation failed (e.g. invalid email format or a constrained query value) |
