badgeIA

Using Badge

Audit log

What a workspace audit log records, who can see and export it, how to verify its hash chain and signed checkpoints yourself, and what Badge cannot guarantee.

View Markdown

The workspace audit log is an append-only record of changes to a team workspace: agents, members, invitations, certificates, and retention settings. Each entry is linked to the one before it by a hash, so a change to any past entry can be detected. Owners can export the log, verify the chain, list the signed checkpoints of its head, and create new ones.

Audit log export is included from the Team plan, and Enterprise has it too, as shown on Pricing. Single sign-on and SCIM are the Enterprise-only governance features.

The audit log is available only when it is switched on for your Badge deployment. While it is off, the audit log page and its endpoints are not available.

What is recorded

Each entry names the action, the target, and the actor.

Action, Recorded when table
ActionRecorded when
agent.created, agent.updated, agent.deleteda workspace agent is created, changed, or removed
agent.workspace_changeda workspace agent's workspace-relevant settings change (the endpoint itself is recorded only as present or absent)
agent.otlp_token_minted, agent.otlp_token_rotated, agent.otlp_token_revokedthe trace-ingest token of a workspace agent is created, replaced, or revoked (the token itself is never recorded)
member.joined, member.removed, member.role_changeda person joins or leaves the workspace, or their role changes
invitation.created, invitation.resent, invitation.revokedan invitation to the workspace is sent, sent again, or revoked
certificate.issueda certificate is issued for a workspace agent
audit.retention_updatedthe owner changes the retention period
audit.exportedsomeone exports the log
audit.checkpointeda signed checkpoint is created
audit.redacteda single entry's details are removed
audit.retention_appliedthe retention period removes details from old entries
audit.subject_erasedan account whose actions appear in the log is erased
sso.config_updatedan owner changes the single sign-on settings, or turns single sign-on off
sso.login_succeededa person signs in through single sign-on
sso.login_faileda single sign-on sign-in is refused. The entry holds a fixed reason, never the assertion, the email address or the identity provider's message. At most one entry a minute for each reason and workspace; the entry that follows a quiet minute carries a suppressed count
sso.user_provisioneda person's first single sign-on sign-in creates their account, or adds them to the workspace
sso.domain_verifieda company domain claimed for single sign-on is proved with its DNS record
sso.domain_claim_displacedanother workspace's claim on a domain that was not yet proved with DNS replaced this workspace's claim
sso.identity_linkedan account that already existed is connected to the workspace's single sign-on. consent is true when the person confirmed it with their current password or GitHub sign-in (method), which every account with its own Badge credential must do. replaced and previous_provider mark a connection that moved to a new identity from the same workspace

Each entry also records:

  • Actor type: user (a signed-in person), api_key (a request made with an API key on a person's behalf), or system (a scheduled job, account erasure, or a refused single sign-on attempt, which has no signed-in person yet). A refused single sign-on attempt is labelled sso:saml or sso:google.
  • Actor label: the actor's email address at the time of the action. Erasure replaces it.
  • Target: the type and id of the thing changed, with a label where one is useful.
  • Request id and IP address, where the request carried them.
  • Before and after fingerprints for changes to agents and settings. An agent.updated entry is written only when something changed, and its metadata.changed_fields names what changed. An agent's endpoint, credentials, manifest URL and payload schema are named there but their values are never logged.

Secrets are never written to the log. Tokens, passwords, API key values, and signing keys are refused at write time. Invitation tokens and accept links are never stored.

Not recorded. API key creation and revocation, and webhook creation and deletion, are not in the log. These are settings on your personal account, not on the workspace.

Who can see and export it

Action, Owner, Admin, Member, Viewer table
ActionOwnerAdminMember, Viewer
View the logyesyesno
Export the logyesnono
Verify the chainyesnono
Create a signed checkpointyesnono
List signed checkpointsyesnono
Remove details from one entryyesnono
Set the retention periodyesnono

How to read an export

Export from the audit log page as CSV or JSON Lines. An unfiltered export contains every entry in order of seq, starting at 1. An export follows the filters set on the page: action, actor_user_id, target_type, target_id, and a date range (from inclusive, to exclusive). It contains only the matching entries, so a filtered export has gaps in seq and cannot be chain-verified on its own. For an audit, export unfiltered as JSON Lines with checkpoints (include=checkpoints, or Download full log with checkpoints under Checkpoints on the page).

  • seq is the entry's position in the chain. Gaps mean missing entries.
  • prev_hash is the hash of the previous entry. The first entry has none.
  • hash is the entry's fingerprint. It covers these fifteen fields, in this order: seq, the workspace id, actor_type, actor_ref_hash, actor_display_hash, action, target_type, target_id, target_label_hash, before_hash, after_hash, metadata_hash, request_id, created_at, and prev_hash.
  • actor_ref_hash is the SHA-256 of the actor's user id. It stays after an account is erased.
  • actor_display_hash is the SHA-256 of the actor's label and IP address together. target_label_hash is the SHA-256 of the target label.
  • metadata is the stored detail. A removed entry shows {"redacted":true,"redacted_at":...} here.
  • metadata_hash is fixed when the entry is written and does not change when its details are removed.

The actor label, target label, IP address, the raw user id, and the metadata body are not part of hash directly. The hashes above cover them, which is why an erasure can replace them and the chain still verifies.

In CSV, a blank cell means no value. In a spreadsheet, a value that begins with =, +, -, or @ is prefixed with an apostrophe in the actor_label, target_label, and ip_address columns. The hashed columns are exported exactly as stored.

The export is the log as it stood when you pressed export. Entries written while the download runs are not included, and the export itself is recorded as an audit.exported entry.

How to verify the chain

Owners can press Verify chain on the audit log page, or call GET /teams/{team_id}/audit-log/verify. The check recalculates every hash from the stored fields and reports:

  • valid: true only when every check passed.
  • reason: when valid is false, the first check that failed (see the table below).
  • checked: how many entries were examined.
  • first_invalid_seq: the entry or checkpoint the failure points at, if any.
  • head_seq and head_hash: the most recent entry, as recorded by the chain head.
  • last_checkpoint: the most recent signed checkpoint, with signature_status.
  • checkpoints_checked and checkpoints_expected: how many signed checkpoints were found, and how many the chain head has counted. With reason checkpoints_missing, the difference is how many are gone, and first_invalid_seq is empty.
reason, What it means table
reasonWhat it means
chain_brokenan entry is missing, out of order, or its hash does not match its fields
metadata_mismatchthe stored details no longer match metadata_hash
labels_mismatcha stored label or IP address no longer matches its hash
actor_mismatchactor_ref_hash does not match the stored user id
unjustified_redactiondetails were removed with no matching record of why
retention_forgeda retention record names an entry that is not older than the record's own cutoff, so it cannot be a real retention run
target_invalida retention record does not name a valid entry: its target is not a plain entry number, or is past the end of the log. Badge ignores it when deciding what to remove
timestamp_in_futurean entry is dated more than an hour ahead of Badge's own clock
timestamp_out_of_orderan entry is dated more than an hour before an entry that comes earlier in the log
unjustified_erasurean account was cleared with no matching audit.subject_erased entry
head_mismatchthe chain head disagrees with the last entry, so entries were removed from the end
checkpoint_mismatcha signed checkpoint does not match the entry at its position
checkpoint_signature_invalida signed checkpoint's signature does not verify
checkpoint_key_unknowna signed checkpoint was made with a key Badge no longer holds (see below)
checkpoints_missingfewer signed checkpoints are stored than the chain head counts

Verify an export yourself

To check an export's hash chain yourself, offline, use an unfiltered export and the following steps.

  1. Take the workspace id from the file name: the id at the end of audit-log-<workspace name>-<date>-<team id>.jsonl (a file fetched straight from the API is audit-log-<team id>.jsonl). No row contains it.
  2. For the first entry, prev_hash must be empty. For every later entry, prev_hash must equal the previous entry's hash.
  3. For each entry, build a JSON object with exactly the fifteen fields listed above, using the stored values. Write it as JSON with sorted keys, no spaces, and non-ASCII characters left as they are. Encode it as UTF-8, and take its SHA-256. Empty values are JSON null. The timestamp is written as 2026-10-16T09:30:00.123456Z: UTC, six fractional digits, and a trailing Z. The result must equal the entry's hash.
  4. Where metadata is not a redaction marker, the SHA-256 of its sorted, compact JSON must equal metadata_hash.

If the first entry fails step 3, the usual causes are the wrong workspace id or a timestamp without its six fractional digits.

Signed checkpoints

A checkpoint is a signature, made with Badge's verification key, over the chain head at one point in time. It records the entry number (seq), the hash at that position (head_hash), and the time it was signed.

  • The owner can create a checkpoint on demand. Badge also creates one each day while the audit log is on and the background jobs are running. If nothing has been written since the last checkpoint, no new one is made.
  • GET /teams/{team_id}/audit-log/checkpoints lists every checkpoint, oldest first. GET .../export?format=jsonl&include=checkpoints adds the checkpoints at or before the export's last entry to the end of the file, marked "record_type": "checkpoint".
  • The signed bytes are the ASCII text badge-audit-checkpoint-v1, one zero byte, then the JSON object {"head_hash":...,"seq":...,"signed_at":...,"team_id":...} with sorted keys and no spaces. The signature is Ed25519, in base64url. The public key is published at /.well-known/badge-verification-key.json.

To check a checkpoint, confirm that the entry at its seq has hash equal to its head_hash, and that its signature verifies against the signed bytes above.

Before signing, Badge checks that the chain head matches the entry at its seq: the entry must carry that hash and still hash to it. If it does not, Badge refuses to sign and the request fails with 409 Conflict. Run verify to find the first invalid entry.

Key rotation. Badge holds one verification key. A checkpoint signed under a different key id is reported as key_rotated and makes the chain not intact, with reason checkpoint_key_unknown. Badge cannot yet verify old keys on its own. Save the public key with each checkpoint you rely on, and verify it against that saved key.

If Badge ever rotates that key before it supports old keys, the Verify banner on the audit log page reads "The checkpoint at event #N was signed with a key Badge no longer recognises" in amber, not "Tamper detected", and links here. That is a key change, not an edit to your events. Verify the checkpoint offline against the public key you saved with it. The same reason is returned by GET /teams/{team_id}/audit-log/verify.

Keep checkpoints outside Badge. A checkpoint you have saved elsewhere lets you prove later that the log had that head at that point. Save one before you rely on an export for an audit.

Retention and removal of details

An owner can set a retention period between 1 and 3650 days, or keep the log indefinitely. Once a day, entries older than the period have their details removed. The entry itself stays: its seq, actor type, action, target, and hashes are kept, so the chain still verifies. A removal is recorded as an audit.retention_applied entry.

An owner can also remove the details of a single entry. This is recorded as an audit.redacted entry that names the entry it changed. An entry's details can be removed only once.

Entries are never deleted while the workspace exists.

Account erasure

When a person's account is erased, their user id is removed from every entry they performed, and their email address and IP address are replaced in the log. Their anonymous fingerprint stays, so the log still shows that one actor, now anonymous, did these things. Each affected workspace receives an audit.subject_erased entry.

What Badge cannot guarantee

  • Tampering by someone with direct database access. The database refuses changes to the hashed fields and refuses deletion of entries. A database administrator can still turn that protection off. Signed checkpoints are the evidence against that, and they are only as strong as where you keep them. Save a checkpoint outside Badge if you rely on the log for an audit.
  • Removal of the most recent entries. Verification detects a gap at the end of the log only when the chain head and the entries disagree. Entries removed after the last checkpoint, with the chain head changed to match, are not detected, because no checkpoint covers them. Compare head_seq with the seq of the latest checkpoint you hold.
  • Forged removal records. Someone with write access to the database can add a well-formed record claiming that details were removed. The two kinds are bounded differently, and both need that write access.
    • A forged retention record covers many entries at once, so verification limits it. It can cover only entries older than the cutoff it carries, that cutoff must sit at least one day (the shortest retention period) before the record, and the record's own time cannot be ahead of Badge's clock or well behind the entries before it (an hour of leeway). Anything else is reported as retention_forged, target_invalid, timestamp_in_future or timestamp_out_of_order. In practice it can blank only entries that are about a day old or older.
    • A forged redaction record has no age limit. It blanks exactly the one entry it names, whatever its age, and it appears in the log as an audit.redacted entry, one per blanked entry.
    • In both cases the hashed fields of the blanked entries are unchanged, so the log still shows that something was removed and when.
  • Forged entries of any kind. The same person can add any well-formed entry, not only removal records, such as a role change that never happened. Verification cannot tell it from a real entry. The chain proves the order of entries and that they have not changed. It does not prove who wrote them. A checkpoint you saved earlier proves only that the entry was not there when the checkpoint was signed.
  • A rewritten log with a fresh signature. Badge signs the chain head the database holds. It refuses a head that does not match the entry at its seq, but it does not re-check the whole chain first. A database administrator who rewrites every entry consistently, deletes the old checkpoints, and then asks for a new one gets a valid signature, and verification passes. Only checkpoints you saved outside Badge before the rewrite expose it.
  • Restores from backup. Restoring a database to an earlier point rolls the log back with it. The chain still verifies, but entries written after the backup are gone. Record the head and checkpoint values before any restore.
  • Activity before the log was switched on. Entries are written only while the audit log is on.
  • Activity outside the list above. If an action is not in the table, it is not in the log.
  • The contents of removed details. Once details are removed, they cannot be recovered from the log. Their fingerprints remain.