For AI agents: the documentation index is at /docs/aidbox/llms.txt. A Markdown version of this page is available at /docs/aidbox/tutorials/security-access-control-tutorials/how-to-subscribe-to-audit-events.md or by requesting it with the Accept: text/markdown header.
Aidbox Docs

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 AuditEvent table 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

PropertyValue
Topic URLhttp://health-samurai.io/fhir/core/StructureDefinition/AuditEventsR4BALP
Event formatFHIR 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 = rest and subtype = delete select 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 filterBy on 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 topic is not the audit events topic;
  • Aidbox runs a FHIR version other than R4 or R4B;
  • another audit-events-recorder destination exists or security.audit-log.enabled is 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/AuditEvent records an event;
  • GET /fhir/AuditEvent returns 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 id of the event and keeps the rest as you sent it. It does not validate the event against the BALP profiles.
  • The filterBy of each destination applies to the type, subtype, and action of the event. The C,U,D filter from Filter audit events drops the read event above.
  • Aidbox returns 200 even when the topic has no destinations. It returns 422 when the body is not a Parameters resource, 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:

SettingSince 2609
security.audit-log.enabledStores 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-headersDeprecated 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.

Last updated: