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

# Logs, audit trail and retention

> Two records are kept for every call to the public API. They exist for different reasons and — this is the important part — they are kept for different…

Two records are kept for every call to the public API. They exist for different
reasons and — this is the important part — they are **kept for different lengths of
time**.

|                    | Request log                  | Audit trail                           |
| ------------------ | ---------------------------- | ------------------------------------- |
| Table              | `api_request_log`            | `api_audit_log`                       |
| Written for        | **every** request            | every **successful mutation**         |
| Contains a payload | Yes — scrubbed and truncated | **No**                                |
| Retention          | **30 days**, then deleted    | **Kept permanently**                  |
| Can be changed     | It is deleted on schedule    | **No.** `UPDATE`/`DELETE` are blocked |

The asymmetry is deliberate. Deleting request logs on a schedule is a privacy
obligation — they contain payload data. Deleting audit rows would destroy the
evidence of who changed a customer's data, which is the one thing that has to
outlive a debugging window.

***

## Request log

One row per request, holding:

* `request_id` — the same value returned in the `X-Request-Id` header
* tenant, API key, method, route
* status code and latency
* IP and user agent
* the request and response payloads, **scrubbed and truncated**

**This is why `X-Request-Id` is worth keeping.** Quote it in a support ticket and
we can find your exact request.

Visible to company admins under **API Usage → Request log**, filterable by route
and status. Row-level security restricts it to your own company; the filtering is
enforced by the database, not by the page.

### What is removed before storage

Nothing reaches the table raw. Two independent passes run first, because either
alone leaves a hole:

**By field name** — any field whose name contains `password`, `secret`, `token`,
`api_key`, `authorization`, `credential`, `cvv`, `card_number` and similar. Names
are compared ignoring case and separators, so `api_key`, `apiKey`, `api-key` and
`x-api-key` are all caught by one rule.

**By value shape** — anything that *looks* like a credential wherever it appears,
including in a field with a perfectly innocent name:

* API keys (`f9_live_…`, `f9_test_…`, legacy `wcrm_…`)
* `Bearer …` tokens and JWTs (`eyJ…`)
* Supabase keys
* card-number-shaped digit runs

A credential pasted into a `notes` field is exactly what happens in practice, and
the name pass alone would miss it.

Redacted values are replaced with `[redacted]`. Payloads over \~4 KB are truncated
and flagged with `was_truncated`.

**Scrubbing runs before truncation**, so a secret cannot survive by sitting past
the cut-off point.

***

## Audit trail

One row per **successful** mutation — `POST`, `PUT`, `PATCH`, `DELETE` that
returned 2xx. It records who did what, and deliberately holds **no payload**:

* `request_id`, tenant, API key, and the **user the key acts for**
* method, route, action (`CREATE` / `UPDATE` / `DELETE`), entity
* status code, IP, timestamp

Two things it does *not* do, both on purpose:

* **Failed mutations are not audited.** A rejected write changed nothing;
  recording it would claim a change that never happened. It is still in the
  request log for debugging.
* **Reads are not audited.** The trail answers "who changed this", and `GET`
  traffic would drown that.

### Immutability

`UPDATE` and `DELETE` are blocked by a database trigger. Not by convention or by
permissions — by the database itself:

```
ERROR: append-only table: UPDATE not permitted on api_audit_log
```

Unlike the billing tables that use the same pattern, this one has **no purge
escape hatch**. Nothing should ever rewrite the record of who changed a customer's
data.

***

## Retention in practice

A scheduled job runs nightly at 03:40 and deletes request logs older than 30 days.
It deletes in batches so a large backlog cannot hold a long transaction open
against a table that is being written to constantly.

Correctness does not depend on that job running: it only keeps the table small.

**The job never touches `api_audit_log`.**

***

## If you need something removed sooner

Request logs age out in 30 days on their own. For a specific erasure request,
contact support with the `request_id` — the audit trail will retain the fact that
a change occurred and who made it, which is a separate, legitimate record.
