Audit Log¶
The audit log records security-relevant changes to your Zammad system: who changed what, and when. The audit log is read-only.
List¶
Required permission: admin.audit_log
GET-Request sent: /api/v1/audit_logs
The endpoint supports pagination. The default page size is 500.
Entries are returned ordered by id (ascending). Pass ?sort_by=id
and ?order_by=DESC to return the newest entries first.
Response:
# HTTP-Code 200 Ok
[
{
"id": 1,
"user_id": 3,
"user_fullname": "Christopher Miller",
"action_type": "create",
"auditable_type": "ChecklistTemplate",
"auditable_id": 1,
"auditable_name": "Onboarding",
"value_from": {},
"value_to": {
"name": "Onboarding",
"active": true,
"sorted_item_names": [
"Buy chair",
"Coffee briefing",
"Create accounts"
]
},
"source_ip": "172.19.0.1",
"preferences": {},
"created_at": "2026-07-23T09:14:51.650Z",
"updated_at": "2026-07-23T09:14:51.650Z"
},
{
"id": 2,
"user_id": 3,
"user_fullname": "Christopher Miller",
"action_type": "update",
"auditable_type": "Setting",
"auditable_id": 280,
"auditable_name": "ai_provider",
"value_from": {
"area": "AI::Provider",
"name": "ai_provider",
"title": "AI provider",
"options": {},
"frontend": true,
"description": "Defines if the AI provider is configured.",
"state_current": {
"value": true
},
"state_initial": {
"value": false
}
},
"value_to": {
"area": "AI::Provider",
"name": "ai_provider",
"title": "AI provider",
"options": {},
"frontend": true,
"description": "Defines if the AI provider is configured.",
"state_current": {
"value": false
},
"state_initial": {
"value": false
}
},
"source_ip": "172.19.0.1",
"preferences": {
"changed_attributes": [
"state_current"
]
},
"created_at": "2026-07-23T09:14:59.501Z",
"updated_at": "2026-07-23T09:14:59.501Z"
},
{
"id": 3,
"user_id": 3,
"user_fullname": "Christopher Miller",
"action_type": "update",
"auditable_type": "Role",
"auditable_id": 2,
"auditable_name": "Agent",
"value_from": {},
"value_to": {
"permissions": [
"admin.text_module"
],
"group_permissions": {
"2nd Level": [
"full"
],
"Service Desk": [
"full"
]
}
},
"source_ip": "172.19.0.1",
"preferences": {},
"created_at": "2026-07-23T09:16:30.188Z",
"updated_at": "2026-07-23T09:16:30.188Z"
},
{
"id": 4,
"user_id": 3,
"user_fullname": "Christopher Miller",
"action_type": "update",
"auditable_type": "Setting",
"auditable_id": 265,
"auditable_name": "pgp_integration",
"value_from": {
"area": "Integration::Switch",
"name": "pgp_integration",
"title": "PGP integration",
"options": {
"form": [
{
"tag": "boolean",
"name": "pgp_integration",
"null": true,
"display": "",
"options": {
"true": "yes",
"false": "no"
}
}
]
},
"frontend": true,
"description": "Defines if PGP encryption is enabled or not.",
"state_current": {
"value": false
},
"state_initial": {
"value": false
}
},
"value_to": {
"area": "Integration::Switch",
"name": "pgp_integration",
"title": "PGP integration",
"options": {
"form": [
{
"tag": "boolean",
"name": "pgp_integration",
"null": true,
"display": "",
"options": {
"true": "yes",
"false": "no"
}
}
]
},
"frontend": true,
"description": "Defines if PGP encryption is enabled or not.",
"state_current": {
"value": true
},
"state_initial": {
"value": false
}
},
"source_ip": "172.19.0.1",
"preferences": {
"changed_attributes": [
"state_current"
]
},
"created_at": "2026-07-23T09:17:20.469Z",
"updated_at": "2026-07-23T09:17:20.469Z"
}
]
Show¶
Required permission: admin.audit_log
GET-Request sent: /api/v1/audit_logs/{id}
Response:
# HTTP-Code 200 Ok
{
"id": 3,
"user_id": 3,
"user_fullname": "Christopher Miller",
"action_type": "update",
"auditable_type": "Role",
"auditable_id": 2,
"auditable_name": "Agent",
"value_from": {},
"value_to": {
"permissions": [
"admin.text_module"
],
"group_permissions": {
"2nd Level": [
"full"
],
"Service Desk": [
"full"
]
}
},
"source_ip": "172.19.0.1",
"preferences": {},
"created_at": "2026-07-23T09:16:30.188Z",
"updated_at": "2026-07-23T09:16:30.188Z"
}
Search¶
Required permission: admin.audit_log
The search endpoint accepts the Zammad search-backend query
syntax. The simplest case is a literal substring on a single
indexed field such as auditable_name, auditable_type or
user_fullname:
GET-Request sent: /api/v1/audit_logs/search?query={search-string}
Response:
# HTTP-Code 200 Ok
[
{
"id": 3,
"user_id": 3,
"user_fullname": "Christopher Miller",
"action_type": "update",
"auditable_type": "Role",
"auditable_id": 2,
"auditable_name": "Agent",
"value_from": {},
"value_to": {
"permissions": [
"admin.text_module"
],
"group_permissions": {
"2nd Level": [
"full"
],
"Service Desk": [
"full"
]
}
},
"source_ip": "172.19.0.1",
"preferences": {},
"created_at": "2026-07-23T09:16:30.188Z",
"updated_at": "2026-07-23T09:16:30.188Z"
}
]
To filter on a specific attribute rather than substring-match the
whole record, prefix the attribute name. You can even use the logical AND
connector to narrow down the results:
GET-Request sent:
/api/v1/audit_logs/search?query=auditable_type:Setting AND user_id:3
Note
Search matches are case-sensitive and search only the indexed
attribute fields (auditable_name, auditable_type,
user_fullname and so on). The value_from and value_to
payloads are not searchable.
Note
By default the response is a bare JSON array of matching entries.
Pass with_total_count=true on the URL (or with_total_count:
true in the body of a POST request) to wrap the response in
an object that also contains the total_count. Send a POST
request when the query is too long or complex for a URL.
Field Reference¶
idInteger primary key of the audit log entry.
user_idID of the user that triggered the change.
nullwhen the entry was written by a background job without a current user.user_fullnameFull name of the user at the time the entry was written. Stored separately so it remains readable after the user account is removed.
action_typeType of the recorded change. One of:
create- a record was added.update- an existing record was modified.destroy- a record was removed.switch_to- a user took over another user’s session viaView from user's perspectiveswitch_back_to- the original session was resumed.
auditable_idID of the record that was changed.
auditable_typeClass name of the record that was changed (e.g.
Role,User,Setting,KnowledgeBase,ChecklistTemplate).auditable_nameDisplay name of the changed record at the time the entry was written. Stored separately so it remains readable after the record itself is gone.
value_fromObject (JSON) holding the previous state of the audited attributes. Empty (
{}) oncreateentries.value_toObject (JSON) holding the new state of the audited attributes. Empty (
{}) ondestroyentries.source_ipIP address that issued the underlying request.
Rails consoleorRails runneris stored when the entry was written from a maintenance script.preferencesObject (JSON) holding additional per-entry metadata. For
updateentries this contains achanged_attributesarray listing the attributes that actually changed.created_atTimestamp at which the entry was written. Audit log entries are append-only.
updated_atSame as
created_atfor audit log entries. Audit log entries are append-only.
Lifecycle¶
A scheduled task removes entries older than 12 months every day.
Use the AuditLog.cleanup in the Rails console to trigger a cleanup manually.