Skip to main content
This guide provides instructions for System Administrators and Developers on how to use QWED’s audit and export features to support SOC 2 preparation, GDPR requests, and audit trail management. These features produce a record of what was checked; they do not by themselves make a system compliant with any standard or regulation.

Audience

  • System Administrators: Use the API endpoints described below to manage compliance tasks.
  • Developers: Integrate these features into your applications using the QWED Python SDK.

1. Security activity summary

GET /admin/compliance/report/soc2/{org_id} returns a security activity summary for your organization over the last 90 days. QWED computes every value from its own records: verification and security event counts, active users and API keys, and a signature and hash-chain check of the audit trail. Use it as input when you prepare for an audit or review security activity.
The summary is not a SOC 2 report or an auditor’s attestation. A SOC 2 report is issued by an independent auditor over a period. Running this endpoint doesn’t make your system SOC 2 compliant. The URL keeps soc2 for compatibility.
Prerequisites:
  • An API key whose owner has the owner or admin role, or a signed-in session (Authorization: Bearer <jwt_token>) for an owner or admin. See Session sign-in for admin endpoints.
  • QWED_AUDIT_SECRET_KEY set on the server. Without it, the audit trail check can’t run and audit_trail.status is NOT_VERIFIED.
API endpoint:
org_id must be your own organization ID. Another organization’s ID returns 403 Forbidden.
Example response:

Response fields

Audit trail status

For every status other than VERIFIED, proof_ref and evidence are null. total_entries, verified_entries, failed_entries, and legacy_entries count the entries checked, and first_failed_entry_id names the lowest failing entry, or null. errors lists at most 20 errors. total_errors gives the full count, and errors_truncated is true when the list was cut.
The check covers the audit entries that were recorded. It doesn’t show that every request wrote an entry, as the scope field states.

Audit trail verification

Every /verify/* endpoint, the Verification Context routes, POST /agents/{agent_id}/verify, and POST /audit/seed append their audit rows through AuditLogger. POST /verify/batch appends one row per item, including failed items. Each new row is HMAC-signed and hash-chained to the previous row for the same organization. If QWED can’t append the row, the request returns 500 with audit persistence failed instead of a result with no audit record. For a batch, the affected item is marked failed with that error and no result. GET /audit/logs and GET /audit/logs/{log_id} return each row’s stored verdict and its chain fields (entry_hash, plus previous_hash and hmac_signature on the detail route). Each row also records:
  • admission: the admission decision its route made (ADMIT or BLOCKED, or ADMIT or DENY for POST /verification-context/from-diagnostic). null for routes that decide no admission and for older rows.
  • api_key_id: the API key that authenticated the request. null for signed-in sessions, the audit seed, the agent route, and older rows.
  • verifier_version: the QWED server version that wrote the row.
  • latency_ms: whole milliseconds from the start of the HTTP request to when QWED began writing the audit row, before it waits for the append lock. null for rows written outside a request and for older rows.
The entry hash covers all four fields, so changing or clearing them on a stored row fails verification. Rows written before these fields existed still verify. To export the whole trail as evidence, page through GET /audit/logs with the before cursor until next_before is null. See Audit endpoints.
Upgrade every QWED process together, and don’t roll back to a release older than v8.0.0 after v8.0.0 has written an audit row. Older releases hash rows without these fields, so they can’t verify the new rows. When such a row is an organization’s chain head, an older release refuses every further append for that organization and its verify routes return 500 (fail closed).
Verify the cryptographic integrity of your audit logs. Verification detects edited entries, reordered entries, and removals that break a link between the entries that remain. It does not prove the trail is complete: deleting the newest entries leaves a valid chain, so detecting that needs a separately stored checkpoint of the latest hash. Per-entry verification covers three checks:
  • hash_valid: the entry’s SHA-256 hash matches a recomputation of its canonical payload. For backwards compatibility, entries hashed before raw_llm_output was covered are also accepted against the legacy canonical form.
  • signature_valid: the entry’s HMAC-SHA256 signature is constant-time-equal to the expected signature.
  • chain_valid: the entry’s previous_hash correctly references the prior entry’s hash within the same organization. Genesis entries (the first entry for an organization) must have a null previous_hash; non-genesis entries must reference a non-empty prior hash.
Audit chains are isolated per organization. Hash linkage is verified only against prior entries belonging to the same organization_id, so cross-tenant activity cannot affect another organization’s chain validity.

Legacy entries

Rows written before the API used the hash chain have no hash or signature. QWED treats an unchained row with no chained row before it as a legacy entry:
  • verify_log_entry returns "legacy": true and "valid": false for it. A legacy entry is never reported as valid.
  • The first chained row after the legacy rows opens a new chain segment with a null previous_hash.
  • An unchained row after a chained row is not legacy. QWED stops appending for that organization and verification fails.
If an entry’s stored result payload cannot be decoded as JSON, verify_log_entry fails closed with a SecurityError rather than reporting the entry as valid. Verify a Single Log Entry:
Verify the whole trail or a period:
start and end are optional ISO 8601 datetimes. Omit both to check the whole chain. The response’s scope names what QWED checked:
  • With neither start nor end: "Signature and hash-chain check of every audit entry recorded for the organisation. It does not show that every request was recorded."
  • With start, end, or both: "Signature and hash-chain check of the audit entries recorded in the period. It does not show that every request was recorded."
The response also reports status (VERIFIED, NO_ENTRIES, or NOT_VERIFIED), the entry counts, and first_failed_entry_id, so you can see where the chain stops checking out. See GET /admin/compliance/verify-trail.
Verify entire trail (Python SDK): To verify the trail in-process without the API, call verify_audit_trail directly.
verify_audit_trail lists legacy entries under legacy_entries, separately from failed_entries. The trail stays "valid": false while any legacy entry is in range. To verify only the rows written since your upgrade, pass start_date:
AuditLogger() requires QWED_AUDIT_SECRET_KEY to be set in the environment, or the constructor must receive an explicit secret_key argument. Initialization fails closed otherwise.

Evidence collection

Export your organization’s verification logs for an audit period as a CSV file to provide to your auditor. API endpoint:
start and end are ISO 8601 times. A time without a zone is UTC. Without end, the period ends now. Without start, it starts 30 days before end. A start after end, including a future start with no end, returns 400 Bad Request. Each row carries the stored verdict and admission, so the file shows the decision QWED made for each request:
  • Verdict and Admission are the stored values, as GET /audit/logs returns them. A value QWED didn’t record is an empty cell.
  • Domain names the verification engine. Natural-language requests use NATURAL_LANGUAGE. Rows written before that fix read MATH, because the domain is part of the signed entry.
  • Nothing is truncated. QWED prefixes an apostrophe to any cell that starts with =, +, -, @, a tab, a carriage return, or a line feed, so a spreadsheet doesn’t run it as a formula.
  • The file holds at most the newest 1,000 rows of the period, newest first. When more rows matched, the response sets X-Export-Truncated: true, otherwise false. Split a long audit period into shorter start/end windows to collect every row.
The Chain Status column shows Chained for rows that carry a hash and Legacy for rows written before the hash chain. Chained only means a hash is present. The export does not re-verify the chain. Use GET /admin/compliance/verify/{log_id} or verify_audit_trail to verify rows. See GET /admin/compliance/export/csv for every column.

2. GDPR compliance

Data export (Article 15 - right of access)

QWED supports the Right of Access by allowing you to export all data associated with an organization or user. API endpoint: Use the CSV export endpoint to retrieve verification data. Set start and end to cover the period you need, because the default is the last 30 days and each file holds at most 1,000 rows.

Data deletion (Article 17 - right to erasure)

Note: Currently, an administrator must delete users directly in the database. A self-service deletion API is planned. To comply with a deletion request:
  1. Delete the user: Remove the user record from the database.
  2. Prune logs: For full GDPR Article 17 compliance, ensure all verification logs associated with the user are also deleted or anonymized.
Manual database process (example):
QWED does not have a built-in “Consent Management Platform” (CMP), but you can use the hash-chained audit log to track consent events. Implementing consent tracking: Log a specific “verification” event when a user grants consent. The entry is HMAC-signed and hash-chained to the previous entry for the same organization, so a later edit to it is detectable when you verify the trail. It is not tamper-proof: anyone holding QWED_AUDIT_SECRET_KEY and database write access can rewrite the chain.
Note: This treats the consent record as a “fact” verification, so it is written to the signed, hash-chained audit log.

3. Audit trail setup

Enable cryptographic logging

Cryptographic logging is enabled by default, but you must configure the signing key before the audit logger will start. The audit logger fails closed when the signing key is missing or when chain continuity cannot be established.
  1. Set the signing key: Set QWED_AUDIT_SECRET_KEY to a secure, random string (at least 32 characters). The API server refuses to start if this variable is not set, and the audit logger raises a SecurityError. qwed init generates and persists a key in .env for local projects and never rotates an existing one.
    There is no fallback or default key. If the variable is unset, the application will fail to start rather than silently writing audit entries with a predictable key.
  2. Secure storage: Store the key in a secrets manager (for example, AWS Secrets Manager or HashiCorp Vault) and inject it into the application environment.
  3. Rotation: Rotating the signing key invalidates the HMAC signatures on existing entries. Verify and archive the existing trail before rotating, then start a new trail with the new key.

Chain continuity across restarts

On startup, AuditLogger loads the most recent persisted entry hash and uses it as the new chain head. New entries are bound to the persisted hash rather than a fresh in-memory root, so restarts do not produce a discontinuous trail.
Run one writer per database. AuditLogger keeps each organization’s chain head in memory, so multiple worker processes or replicas appending to the same database are not supported. A second writer causes the next append to fail with a continuity error (500 audit persistence failed) rather than fork the chain. The shipped Dockerfile runs a single uvicorn worker.
If the in-memory chain head and the persisted chain head disagree at write time, log_verification raises a SecurityError instead of writing a misleading entry. Treat this as a tampering or replication-lag signal and investigate before restarting writes.

Upgrading to v8.0.0

On startup, v8.0.0 adds the api_key_id, verifier_version, and admission columns to an existing audit log table. Existing rows read these fields as null. Stop every process on the old release before you start v8.0.0. Once v8.0.0 has written a row, don’t roll back to an older release.

Per-organization chain isolation

Each organization has its own append-only hash chain. When verifying or appending entries, the logger:
  • Locks the chain head per organization to prevent concurrent appends from forking the chain.
  • Re-verifies the persisted chain head before appending a new entry and refuses to append if the head fails integrity checks.
  • Accepts both the current canonical payload (which covers raw_llm_output) and the legacy payload during verification, so historical entries written before that field was hashed remain verifiable.
  • Fails closed if a stored result payload is malformed JSON or if a chain entry is missing its entry_hash.

Verify log integrity

Regularly run the verification process (see “Audit trail verification” above) to detect database tampering. Verification now flags:
  • A genesis entry that references a non-null previous_hash.
  • A previous entry that is missing its hash.
  • A previous_hash that does not match the prior entry’s hash.
  • An entry whose recomputed hash or HMAC signature does not match the stored value.
Verification does not detect removal of the newest entries. Deleting entries from the end of the trail leaves a valid chain. To detect truncation, store the latest entry hash outside the database and compare it on each verification run.

Sample data

POST /audit/seed writes 10 sample rows, marked {"sample": true}, through the signed chain for development. It returns 404 and writes nothing unless the server runs with QWED_ENABLE_AUDIT_SEED=1. Leave it unset in production so sample rows never mix with a real trail.

Long-term storage

QWED uses the configured database (SQLite/PostgreSQL) for log storage. For high-volume compliance requirements:
  • Configure database backups to a WORM (Write Once, Read Many) compatible storage (e.g., AWS S3 Object Lock) for archival.

4. Compliance checklist

Use this checklist to prepare for an enterprise audit.
  • Audit Logs Verified: Run verify_audit_trail on all historical logs.
  • API Keys Rotated: Rotate any API keys older than 90 days.
  • Security Activity Summary Reviewed: Generate the security activity summary via API and confirm audit_trail.status is VERIFIED.
  • Secret Key Secured: Confirm QWED_AUDIT_SECRET_KEY is set to a production-grade value and stored in a secrets manager.
  • GDPR Export Tested: Verify that data export works for a sample organization.
  • Security Events Reviewed: Check metrics.security_events_by_type in the security activity summary for any anomalies.
  • Rate Limiting Active: Confirm rate limits are enforced to prevent abuse (DoS protection).