Skip to main content
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. 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:
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.