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_typeandobject_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_CHANGEDand more. The full set, grouped, is in Pull audit logs.actor_type,actor_code,actor_display_name: who performed the action.actor_typeisuserfor a person,systemfor a platform worker such as the AI extraction service, andapifor 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_atandrecorded_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
EXPORTEDactions, 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
GETroutes: there is no surface area to amend a record. - Each entry stores
recorded_at(server time at write) alongside the application-suppliedoccurred_at, so backdating attempts are detectable. request_idties 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.
Cross-entity audit search
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
EXPORTEDevents 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
| Method | Path | Purpose |
|---|---|---|
| GET | /audit-logs | List 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.pdf | Download 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:
GET /audit-logs/object/{object_type}/{object_code}?limit=100.- Render entries chronologically, computing the diff client-side from
changed_fields+old_values/new_values.
Compliance export for a period:
GET /audit-logs?start_date=2026-01-01T00:00:00Z&end_date=2026-04-01T00:00:00Z&limit=100&offset=0.- Page until
offset + limitreachespagination.total. - Persist the result set in your archive.
Forensic investigation on a user:
GET /audit-logs?actor_code=USR-...&start_date=...&end_date=....- Optionally narrow by
action_type=EXPORTEDto 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_typeset to a system actor (the extraction worker, not a human user).metadatacontaining the source document reference, the extraction job/run id, and the extraction model identifier where applicable.changed_fieldslisting 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?":
GET /audit-logs/object/invoice/{invoice_code}to retrieve the full history.- Filter to entries where the field of interest appears in
changed_fields. - The earliest such entry whose
actor_typeis the extraction service is the AI origin. Itsmetadatapoints 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.