How to subscribe to audit events
This functionality is available starting from version 2609. If you used the security.audit-log.* settings before 2609, see Migrate from the security.audit-log settings.
Aidbox publishes audit events to a built-in AidboxSubscriptionTopic. You receive them by creating an AidboxTopicDestination on that topic. This tutorial uses a webhook destination; Kafka, GCP Pub/Sub, and the other destination kinds work the same way.
Objectives
- Subscribe a webhook to the audit events topic.
- Produce an audit event and inspect the notification.
- Check delivery status and stop audit logging.
- Filter audit events.
- Store audit events in the
AuditEventtable and search them. - Publish an audit event from your application.
Not all operations generate AuditEvents. See Audit coverage and Known limitations for details.
The audit events topic
| Property | Value |
|---|---|
| Topic URL | http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP |
| Event format | FHIR R4 AuditEvent with IHE BALP profiles |
The topic ships with Aidbox, so there is no AidboxSubscriptionTopic resource to create. Aidbox produces audit events while at least one destination is subscribed to the topic. To keep the events in Aidbox, see Store audit events in the AuditEvent table.
Create a destination
Create an AidboxTopicDestination with the audit topic URL in the topic element. Replace the endpoint value with the URL of your receiver.
POST /fhir/AidboxTopicDestination
content-type: application/json
accept: application/json
{
"resourceType": "AidboxTopicDestination",
"id": "audit-log-webhook",
"meta": {
"profile": [
"http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile"
]
},
"kind": "webhook-at-least-once",
"topic": "http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP",
"content": "full-resource",
"parameter": [
{
"name": "endpoint",
"valueUrl": "https://audit.example.com/events"
}
]
}
Aidbox responds with 201 Created and starts producing audit events. See Webhook AidboxTopicDestination for the full parameter list, including custom headers, batch size, and timeouts.
Produce an audit event
Run any audited operation, for example create a patient:
PUT /fhir/Patient/pt-1
content-type: application/json
accept: application/json
{
"name": [{"given": ["John"], "family": "Smith"}]
}
Inspect the notification
Aidbox sends a POST request to the endpoint. The body is a FHIR Bundle of type history. The first entry is an AidboxSubscriptionStatus, and every other entry carries one AuditEvent. The example below shortens the AuditEvent:
{
"resourceType": "Bundle",
"type": "history",
"timestamp": "2026-09-30T10:07:55Z",
"entry": [
{
"resource": {
"resourceType": "AidboxSubscriptionStatus",
"status": "active",
"type": "event-notification",
"notificationEvent": [
{
"eventNumber": 1,
"focus": {
"reference": "AuditEvent/0199bf3c-8a2e-7c41-9d3e-5b7a0a1d2c3e"
}
}
],
"topic": "http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP",
"topic-destination": {
"reference": "AidboxTopicDestination/audit-log-webhook"
}
}
},
{
"fullUrl": "https://aidbox.example.com/fhir/AuditEvent/0199bf3c-8a2e-7c41-9d3e-5b7a0a1d2c3e",
"request": {
"method": "POST",
"url": "/fhir/AuditEvent"
},
"resource": {
"resourceType": "AuditEvent",
"id": "0199bf3c-8a2e-7c41-9d3e-5b7a0a1d2c3e",
"meta": {
"profile": [
"https://profiles.ihe.net/ITI/BALP/StructureDefinition/IHE.BasicAudit.PatientCreate"
]
},
"type": {
"system": "http://terminology.hl7.org/CodeSystem/audit-event-type",
"code": "rest",
"display": "RESTful Operation"
},
"subtype": [
{
"system": "http://hl7.org/fhir/restful-interaction",
"code": "create",
"display": "create"
}
],
"action": "C",
"outcome": "0",
"entity": [
{
"what": {
"reference": "Patient/pt-1"
}
}
]
}
}
]
}
Aidbox assigns a UUIDv7 to each AuditEvent id, and every destination receives the event with the same id. Use it to deduplicate events on the receiver, because an at-least-once destination can deliver the same event twice.
An operation that touches several patients produces one AuditEvent per patient reference. A search yields one AuditEvent for every patient that each returned resource references: 100 Observations of 100 patients yield 100 events, and 100 Observations of one patient yield 100 events too.
See Notification shape for the id-only and empty content modes.
Check delivery status
Use the $status operation to see how many events Aidbox delivered and how many wait in the queue:
GET /fhir/AidboxTopicDestination/audit-log-webhook/$status
If the endpoint is unavailable, the webhook destination keeps events in the queue and retries until delivery succeeds.
Stop audit logging
Delete the destination:
DELETE /fhir/AidboxTopicDestination/audit-log-webhook
Aidbox drops the undelivered events of a deleted destination. Wait until messagesQueued in $status reaches zero before the DELETE if you need them. Once the topic has no destinations, Aidbox stops producing audit events.
Filter audit events
A destination receives every audit event unless you add filterBy. This destination forwards create, update, and delete events and drops reads, searches, and the rest:
POST /fhir/AidboxTopicDestination
content-type: application/json
accept: application/json
{
"resourceType": "AidboxTopicDestination",
"id": "audit-log-changes-webhook",
"meta": {
"profile": [
"http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile"
]
},
"kind": "webhook-at-least-once",
"topic": "http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP",
"content": "full-resource",
"filterBy": [
{
"filterParameter": "action",
"comparator": "eq",
"value": "C,U,D"
}
],
"parameter": [
{
"name": "endpoint",
"valueUrl": "https://audit.example.com/events"
}
]
}
The audit events topic defines three filter parameters: type, subtype, and action. See Filter audit events for their values.
- Comma-separated values in one filter match when any of them matches.
- Several filters match when all of them match. For example,
type=restandsubtype=deleteselect REST delete events. - Aidbox checks the filters before it builds an event and does not build an event that no destination accepts.
- You cannot change
filterByon an existing destination. To change filters, delete the destination and create it again.
Store audit events in the AuditEvent table
Aidbox can keep audit events in its own AuditEvent table, where you search them with the FHIR API and browse them in Aidbox UI. You turn this on with an audit-events-recorder destination or with the security.audit-log.enabled setting. Both require Aidbox to run FHIR R4 or R4B.
Aidbox stores the events without validation, so you do not need to install the BALP package. The recorder writes each event before the operation that produced it completes. If the write fails, the operation fails.
Create a recorder destination
Create an AidboxTopicDestination of kind audit-events-recorder on the audit events topic. The recorder takes no parameters and accepts filterBy to store part of the events.
POST /fhir/AidboxTopicDestination
content-type: application/json
accept: application/json
{
"resourceType": "AidboxTopicDestination",
"id": "audit-events-recorder",
"meta": {
"profile": [
"http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-auditEventsRecorderProfile"
]
},
"kind": "audit-events-recorder",
"topic": "http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP"
}
Aidbox rejects the destination with 422 when:
- the
topicis not the audit events topic; - Aidbox runs a FHIR version other than R4 or R4B;
- another
audit-events-recorderdestination exists orsecurity.audit-log.enabledis on. Aidbox runs one recorder at a time.
The recorder works alongside destinations of other kinds, so you can store events in Aidbox and forward them to a webhook at once. Delete the recorder to stop storing events.
Use the security.audit-log.enabled setting
Set security.audit-log.enabled to true and restart Aidbox:
BOX_SECURITY_AUDIT_LOG_ENABLED: true
At startup, Aidbox runs a built-in recorder destination with id security-audit-log-enabled. The built-in recorder stores every event. Aidbox keeps it in memory, so GET /fhir/AidboxTopicDestination does not list it. To stop it, disable the setting and restart Aidbox. With a FHIR version other than R4 or R4B, Aidbox starts, logs an error, and stores no events. In Multibox, the setting defaults to true for every box.
To manage the recorder through the API or to filter what it stores, disable the setting, restart Aidbox, and create a recorder destination.
Search stored events
Read the latest events with the FHIR API:
GET /fhir/AuditEvent?_sort=-date
Password change events carry DICOM subtype 110139:
GET /fhir/AuditEvent?subtype=110139
To browse events in the UI, open the Audit Events page of Aidbox UI.
Delete AuditEvents store the reference to the deleted resource as ResourceType/id/_history/versionId, for example Observation/obs-1/_history/2. A search by entity=Observation/obs-1 does not return delete events.
Record events from other systems
Aidbox works as an Audit record repository (ARR) for FHIR AuditEvent resources:
POST /fhir/AuditEventrecords an event;GET /fhir/AuditEventreturns stored events.
Aidbox stores the AuditEvent resources that clients create next to the events of the recorder. It does not publish them to the audit events topic. To publish an event to the topic, use $aidbox-audit.
Publish an audit event from your application
Aidbox produces audit events for the operations it serves. For the actions your own application performs, such as showing a patient record in a frontend or exporting data outside the FHIR API, build an AuditEvent and publish it with the $aidbox-audit operation. Every destination of the topic receives it next to the events of Aidbox, and the recorder stores it when one runs. The operation is available since version 2610.
Pass the event in the resource parameter and the topic URL in the topic parameter:
POST /fhir/$aidbox-audit
content-type: application/json
accept: application/json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "resource",
"resource": {
"resourceType": "AuditEvent",
"type": {
"system": "http://terminology.hl7.org/CodeSystem/audit-event-type",
"code": "rest"
},
"subtype": [
{
"system": "http://hl7.org/fhir/restful-interaction",
"code": "read"
}
],
"action": "R",
"recorded": "2026-10-12T10:00:00Z",
"outcome": "0",
"agent": [
{
"who": {"reference": "Practitioner/pr-1"},
"requestor": true
}
],
"source": {
"observer": {"display": "Patient portal"}
},
"entity": [
{
"what": {"reference": "Patient/pt-1"}
}
]
}
},
{
"name": "topic",
"valueCanonical": "http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP"
}
]
}
Aidbox responds with 200:
{
"resourceType": "OperationOutcome",
"id": "informational",
"issue": [
{
"code": "informational",
"diagnostics": "AuditEvent sent to topic http://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP"
}
]
}
The webhook from Create a destination receives the event in the same Bundle shape as the events of Aidbox, with a UUIDv7 in id in place of the one you sent. With the recorder from Create a recorder destination, the event appears in GET /fhir/AuditEvent.
- Aidbox replaces the
idof the event and keeps the rest as you sent it. It does not validate the event against the BALP profiles. - The
filterByof each destination applies to thetype,subtype, andactionof the event. TheC,U,Dfilter from Filter audit events drops the read event above. - Aidbox returns
200even when the topic has no destinations. It returns422when the body is not aParametersresource, a parameter is missing, the resource is not an AuditEvent, or the topic is not the audit events topic.
Performance considerations
Audit logging costs nothing while the topic has no destinations. With a destination, the cost grows with the number of events per request. Aidbox skips the events that no destination accepts, so filters cut this cost.
The webhook-at-least-once and audit-events-recorder destinations write every event to the Aidbox database before the request returns:
- Create, update, and delete operations store the event in the same transaction as the resource change.
- Read and search operations add one database write per event. A search that returns resources of many patients pays for each of them.
Migrate from the security.audit-log settings
Since 2609, Aidbox treats the security.audit-log.* settings as follows:
| Setting | Since 2609 |
|---|---|
security.audit-log.enabled | Stores audit events in the AuditEvent table through a built-in recorder, see Use the security.audit-log.enabled setting. |
security.audit-log.repository-url, security.audit-log.flush-interval, security.audit-log.max-flush-interval, security.audit-log.batch-count, security.audit-log.request-headers | Deprecated and ignored. Aidbox logs a warning at startup when any of them is set. |
Before 2609, with repository-url set, Aidbox sent events to the external repository and kept none in its database. Since 2609, security.audit-log.enabled stores every event in the database and sends nothing outside. To keep forwarding events to the repository, create a webhook destination with the same endpoint. If you do not need a copy in Aidbox, disable security.audit-log.enabled.
Move custom headers from security.audit-log.request-headers to header parameters of the destination, and the batch size from security.audit-log.batch-count to maxMessagesInBatch. The webhook destination retries until delivery succeeds, so flush-interval and max-flush-interval have no counterpart.
The payload changed. The deprecated sender posted a Bundle of type collection that contained AuditEvent resources only. A topic destination posts a Bundle of type history with an AidboxSubscriptionStatus in the first entry. Check that your repository accepts this shape before you switch.