Skip to main content

Audit Log and Compliance

SpendOne is the AI-based platform for indirect corporate spend. Every entity change, approval, AI extraction, and export passes through a single, immutable audit log. The trail is what powers the security posture documented at spendone.com/en/ai-security: if an AI-suggested value ended up on an invoice, you can prove which document, which user, and which model produced it.

For full schemas, see the API reference.

What is logged​

The audit log records mutations on the following object types: purchase_case, purchase_request, purchase_order, invoice, subscription, approval, accountant_task, credit_card, product, supplier, user, config, import_job.

Each entry captures:

  • object_type and object_code: the entity that changed.
  • action_type: what happened, from one vocabulary shared across every object type: CREATED, UPDATED, DELETED, APPROVED, REJECTED, MATCHED, UNMATCHED, EXPORTED, IMPORTED, STATUS_CHANGED, ASSIGNED, REASSIGNED, LINKED, UNLINKED, COMMENTED, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, CONFIG_CHANGED and more. The full set, grouped, is in Pull audit logs.
  • actor_type, actor_code, actor_display_name: who performed the action. actor_type is user for a person, system for a platform worker such as the AI extraction service, and api for a token-authenticated integration.
  • ip_address, user_agent, request_id: request provenance, for forensics.
  • old_values, new_values, changed_fields: exact diff of what changed.
  • metadata: free-form JSON used to attach extraction context, model version, source document, export job id, etc.
  • occurred_at and recorded_at: wall-clock event time and persistence time.

What this covers in practice​

  • Entity changes: every create/update/delete on the object types listed above.
  • Approvals: approver decisions, rejections, reassignments, vacation substitutions.
  • AI extractions: every field the document-extraction service writes onto an invoice or purchase order.
  • Exports: CSV and FI export jobs are recorded with EXPORTED actions, so you have a record of which records left the platform and when.
  • Configuration changes: approval rules, cost center hierarchy, export profiles, user roles.

Retention​

Audit log entries are retained for the lifetime of the tenant. They are append-only at the application layer: no API exists to mutate or delete them. Database-level guarantees rely on standard backup, point-in-time-recovery, and access controls on the audit schema.

Who can access​

Audit access is gated by the standard authorization layer. Only users with administrative or audit roles can list across objects; ordinary users can read the audit log of objects they already have read access to. Cross-tenant isolation is enforced by the same tenancy middleware as every other endpoint.

Immutability guarantees​

  • No update or delete endpoints are exposed for /audit-logs.
  • The handler only registers GET routes: there is no surface area to amend a record.
  • Each entry stores recorded_at (server time at write) alongside the application-supplied occurred_at, so backdating attempts are detectable.
  • request_id ties the entry to the originating HTTP request, which lets you correlate it to ingress logs for end-to-end provenance.

For admins​

The audit log is the answer when finance, legal, or an external auditor asks "who changed this, and why?".

Inspecting a single entity​

Open any invoice, purchase order, purchase request, or subscription and switch to the History tab. You see a chronological feed of every change: the field that changed, the old and new value, the actor, and the timestamp. AI-extracted fields are tagged with the source document and the extraction model that produced them.

Under Settings → Audit Log (admin-only), you can:

  • Filter by object type and code.
  • Filter by actor (e.g. all actions taken by a leaving employee).
  • Filter by action type (e.g. show all EXPORTED events for a quarter).
  • Filter by date range.
  • Page through results.

AI traceability (admin view)​

Every AI-suggested field on an invoice is auditable. In the invoice detail view:

  • AI-extracted fields display a small marker indicating they came from extraction rather than manual entry.
  • Hovering or expanding shows the source document (the original PDF/email attachment) and the extraction run that produced the value.
  • If a user accepts, edits, or rejects the suggestion, that decision is itself recorded as a separate audit entry, so you can reconstruct the full chain: extraction → suggestion → human decision → final value.

Exporting the audit log​

For external auditors, list audit logs filtered by date and action type, then capture the response. Audit-log export is intentionally a pulled read; there is no scheduled "audit log out" job and no signature on the JSON response. For one purchase case, GET /purchase-requests/{code}/audit-log.pdf renders the same trail as a PDF carrying a SHA-256 content hash.

For integrators​

Endpoints​

MethodPathPurpose
GET/audit-logsList audit entries with filters: object_type, object_code, actor_code, action_type, start_date, end_date, limit, offset.
GET/audit-logs/{code}Fetch a single audit entry by its unique code.
GET/audit-logs/object/{object_type}/{object_code}List the full audit history for one specific entity (e.g. one invoice, one PO).
GET/purchase-requests/{code}/audit-log.pdfDownload the whole case-wide audit trail as a formal PDF record.

GET /purchase-requests/{code}/audit-log.pdf is the one packaged artifact: it renders the purchase request with its orders, credit cards, invoices, approval tasks, activities and comments into a single document carrying a QR deep link and a SHA-256 content hash. It needs purchase-requests:read plus object-level visibility on the case.

limit defaults to 50, max 100. Pagination uses offset, and the response carries the standard envelope: {data, pagination{limit, offset, total}}. Page until offset + limit >= pagination.total.

Response shape​

{
"data": [
{
"code": "AL-01HXYZ...",
"object_type": "invoice",
"object_code": "IV-2026-0001",
"action_type": "UPDATED",
"actor_type": "user",
"actor_code": "USR-...",
"actor_display_name": "Jane Doe",
"ip_address": "10.0.0.12",
"user_agent": "Mozilla/5.0 ...",
"request_id": "req-7c8a...",
"old_values": { "total_amount": "100.00" },
"new_values": { "total_amount": "120.00" },
"changed_fields": ["total_amount"],
"metadata": { "reason": "supplier reissued invoice" },
"occurred_at": "2026-05-05T10:32:11Z",
"recorded_at": "2026-05-05T10:32:11Z"
}
],
"pagination": { "limit": 50, "offset": 0, "total": 1 }
}

Typical integration flows​

Per-entity history view in your own UI:

  1. GET /audit-logs/object/{object_type}/{object_code}?limit=100.
  2. Render entries chronologically, computing the diff client-side from changed_fields + old_values/new_values.

Compliance export for a period:

  1. GET /audit-logs?start_date=2026-01-01T00:00:00Z&end_date=2026-04-01T00:00:00Z&limit=100&offset=0.
  2. Page until offset + limit reaches pagination.total.
  3. Persist the result set in your archive.

Forensic investigation on a user:

  1. GET /audit-logs?actor_code=USR-...&start_date=...&end_date=....
  2. Optionally narrow by action_type=EXPORTED to see what data left the platform.

AI traceability​

The metadata field on an audit entry is where AI extraction provenance lives. When the document-extraction service writes a value onto an invoice or PO, the audit entry records:

  • actor_type set to a system actor (the extraction worker, not a human user).
  • metadata containing the source document reference, the extraction job/run id, and the extraction model identifier where applicable.
  • changed_fields listing every field the extraction populated, so you can replay exactly what the AI proposed.

When a human subsequently accepts, edits, or rejects an AI suggestion, that produces a second audit entry with actor_type=user and the user's actor_code. The pair of entries, the EXTRACTED action followed by the human decision, is the auditable chain for any AI-touched field.

To answer "where did this number come from on this invoice?":

  1. GET /audit-logs/object/invoice/{invoice_code} to retrieve the full history.
  2. Filter to entries where the field of interest appears in changed_fields.
  3. The earliest such entry whose actor_type is the extraction service is the AI origin. Its metadata points to the source document and the model version. Later entries trace every human override.

This is the pattern referenced on spendone.com/en/ai-security: AI suggestions are not opaque, and they are not irrevocable. They are recorded events that sit alongside the human decisions that confirmed or overrode them. See the related reporting and analytics guide for how export events feed the same trail.