Skip to main content
The audit log records every change made through the API to a project’s agents, credentials, phone numbers, API keys and settings. Each entry says which API key made the change, what it did to which resource, when, and what changed. TurnCall has no notion of a person, so an entry is keyed by the API key. A change a person makes in the TurnCall builder is audited there too, and the builder’s entry links to this one. Entries are append-only. Nothing in the API updates or deletes one, and they are kept until the retention the operator sets.

What is recorded

A request that changes several things writes an entry for each, and every one of them carries the same request_id. Publishing version 2 of an agent whose version 1 is live writes three: version 2 published, version 1 archived, and the number that pointed at version 1 updated. Every successful request on these routes writes at least one entry, even one that changed nothing: deleting a credential that is not there writes a deleted entry with no changes. A request that fails writes none. The project and its first API key are created with the platform credential (X-Platform-Key), not an API key. Their entries have actor: "platform" and api_key_id: null. Every other entry has actor: "api_key" and the key’s id.

What changed

changes maps each field that changed to its value before and after, as the API reads it:
Each field is named by its JSON Pointer (/config/llm/model, /limits/live_calls, /config/tools/0/webhook_secret): one / per level, a list item by its index. A ~ or / inside a field’s own name is written ~0 or ~1, so a metadata key such as team.name or a/b is /metadata/team.name or /metadata/a~1b and never reads as a nested field. A field that was not set reads null. When something is created, every field it was created with is listed with before: null; when it is deleted, every field it had, with after: null. No secret is ever recorded. A secret that changed shows as "***" on both sides, so you can see that it changed but not what it was or became. This covers:
  • a credential’s value: a credential entry records only that one is set ("value": "***");
  • an API key’s material: the raw key, its hash and its prefix. A key is recorded by its name, role, environment and revoked_at;
  • whatever the agent read masks: llm.api_key (and each failover’s), the AWS secret key and session token, server_url.secret, each tool’s webhook_secret, and the values of MCP servers’ headers and env;
  • a phone number’s server_url_secret (which the number read does return) and its own Twilio auth token.
The audit log is not redacted: it records configuration, not conversations. Don’t put personal data in agent names, prompts or metadata you do not want kept for as long as the audit log is.

Read it

This needs an admin key; a developer or viewer key gets 403. A key reads only its own project’s entries. Entries come newest first. Filters, all optional:
The audit log pages by cursor rather than by page number, because new entries keep arriving at the head of the list and would shift every page. Pass next_cursor back as cursor until it is null.

Link to an entry

Every response to an audited request names one entry in the X-TurnCall-Audit-Id header: the one for the resource the request was about, such as the version a publish published.
That id is also the request_id of every entry the request wrote, so one read returns all of them, the versions a publish archived and the numbers it re-pointed included, however many there are:
Store the id next to your own record of the change (the TurnCall builder does, for the person who made it). The header is on successful responses only; a request that failed wrote no entry.

Append-only

  • No route writes an entry. The only route is GET /v1/audit-log.
  • The entry is written in the change’s transaction. A change that is rolled back leaves no entry, and a change that is saved always has one: the entries are written as the change commits, and a commit on these routes that has no entry is refused with a 500 and its change rolled back, rather than saved unrecorded.
  • The database refuses changes too. A trigger on the audit_log table refuses every UPDATE and TRUNCATE, and every DELETE except the retention janitor’s. It protects the trail from the application and anything connecting as it; the table’s owner can still drop the trigger, so restrict who owns the database if that matters to you.

Retention

Entries are kept for good unless the operator sets AUDIT_LOG_RETENTION_DAYS on the server (1 to 36500; set but empty is the same as unset). With it set, each server deletes entries older than that many days once an hour, for every project. Servers sweeping at the same time skip each other’s rows rather than wait on them. It is a setting of the deployment, not of a project, so an admin key cannot shorten the trail that records its own changes. Call retention never touches the audit log, and the audit log’s retention never touches calls. A deleted project keeps its entries. Deleting a project writes a project deleted entry, and the project’s entries stay when the project is purged later (PROJECT_PURGE_RETENTION_DAYS), until the audit log’s own retention deletes them. Once the project is deleted its API keys stop working, so the entries can only be read from the database: SELECT * FROM audit_log WHERE project_id = '...' ORDER BY created_at DESC.

Not recorded

Changes to knowledge bases and their documents, tools, takeaways, webhook subscriptions, campaigns, eval scenarios and runs, ambience sounds and the suppression list are not recorded. Neither are calls, messages or anything else a call does.