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

# Audit

> GET /v1/ops/audit returns AuditEvent rows with full actor attribution. Every financial mutation and sensitive action writes one.

# Audit

Every financial mutation and every sensitive action in the platform writes an
`AuditEvent`. Operators read the trail through `GET /v1/ops/audit`, filtered by customer
and attributed across the full actor chain — including autonomous agents and internal
rules.

## The endpoint

```
GET /v1/ops/audit?customer_id=<id>&limit=<n>
```

The endpoint is operator-gated (`require_role("operator")`). It returns audit events
newest-first, optional `customer_id` filter, capped at `limit` (maximum 200):

```python apps/api/src/neobank/api.py theme={null}
@router.get("/ops/audit", response_model=list[AuditEventView], tags=["Operator"])
async def ops_audit(
    _operator: Annotated[Principal, Depends(require_role("operator"))],
    session: DbSession,
    customer_id: str | None = None,
    limit: int = Query(default=100, ge=1, le=200),
) -> list[AuditEventView]:
```

See [Operator console](operate/operator-console) for the role and MFA requirements that gate this
route.

## The AuditEvent row

Each row is a single attributed action. The columns come from the `AuditEvent` model:

```python apps/api/src/neobank/models.py theme={null}
class AuditEvent(Base):
    __tablename__ = "audit_events"
    __table_args__ = (Index("ix_audit_customer_created", "customer_id", "created_at"),)

    id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_id)
    customer_id: Mapped[str | None] = mapped_column(String(36), index=True)
    actor_id: Mapped[str] = mapped_column(String(255), index=True)
    actor_client_id: Mapped[str | None] = mapped_column(String(255), index=True)
    action: Mapped[str] = mapped_column(String(120))
    resource_type: Mapped[str] = mapped_column(String(80))
    resource_id: Mapped[str | None] = mapped_column(String(255))
    request_id: Mapped[str] = mapped_column(String(128))
    metadata_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
```

| Field                           | Meaning                                                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `customer_id`                   | The customer whose resource was acted on (nullable for cross-customer actions)                               |
| `actor_id`                      | The authenticated subject that performed the action                                                          |
| `actor_client_id`               | The authorized client (agent or internal rule) the actor acted through; `null` for a direct customer session |
| `action`                        | What the actor did (for example, `mandate.execute`, `card.freeze`)                                           |
| `resource_type` / `resource_id` | The kind and ID of the resource acted on                                                                     |
| `request_id`                    | Correlates with `X-Request-ID` on the API response and in logs                                               |
| `metadata`                      | Action-specific detail (amounts, states, failure codes)                                                      |
| `created_at`                    | When the event was written                                                                                   |

The composite index `ix_audit_customer_created` (`customer_id`, `created_at`) backs the
filtered, newest-first listing an operator runs during triage.

## Attribution across the actor chain

The two `actor_*` columns are what make the trail usable for mandate investigation. A
single mandate execution by an autonomous agent produces an event whose `actor_id` is the
agent's subject and whose `actor_client_id` is the authorized client that the customer
approved on the mandate. Direct customer actions leave `actor_client_id` null.

This lets an operator answer "which authorized client executed this mandated payment?"
without inference. Cross-reference the `request_id` against the operation in
`/v1/ops/operations` to tie the audit row to the resulting transfer and its outcome.

<Note title="Internal rules are attributed too">
  Actions taken by an `internal_rule` caller carry that rule's identifier in
  `actor_client_id`, not a human subject. The trail distinguishes a human-initiated action
  from a rule-initiated one in the same column.
</Note>

## Related

* [Operator console](operate/operator-console) — the surface operators use alongside this trail.
* [Agent mandates](integrate/agent-mandates) — how authorized clients and rules come to
  hold execution authority, and what an `actor_client_id` value represents.
