Webhook AidboxTopicDestination
This functionality is available starting from version 2410 and requires FHIR Schema validation engine to be enabled.
Aidbox version compatibility
| Aidbox | Profile URL |
|---|---|
| ≥ 2604 | http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile |
| < 2604 | http://aidbox.app/StructureDefinition/aidboxtopicdestination-webhook-at-least-once |
Examples below use the ≥ 2604 form. On older Aidbox, swap the meta.profile URL. The webhook destination ships with Aidbox core — no separate connector JAR.
This page describes an AidboxTopicDestination, which allows sending events described by an AidboxSubscriptionTopic to a specific HTTP endpoint.
The webhook AidboxTopicDestination works in the following way:
- Aidbox stores events in the database within the same transaction as the CRUD operation.
- After the CRUD operation, Aidbox collects unsent messages (refer to the
maxEventNumberInBatchparameter) from the database and sends them to the specified endpoint via a POST request. - If an error occurs during sending, Aidbox will continue retrying until the message is successfully delivered.
Configuration
To use Webhook with #aidboxsubscriptiontopic you have to create #aidboxtopicdestination resource.
You need to specify the following profile:
http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile
Available Parameters
| Parameter name | Value type | Description |
|---|---|---|
endpoint * | valueUrl | Webhook URL. |
timeout | valueUnsignedInt | Timeout in seconds to attempt notification delivery (default: 30). |
keepAlive | valueInteger | The time in seconds that the host will allow an idle connection to remain open before it is closed (default: 120, -1 - disable). |
maxMessagesInBatch | valueUnsignedInt | Maximum number of events that can be combined in a single notification (default: 20). |
header | valueString | HTTP header for webhook request in the following format: <Name>: <Value>. Zero or many. |
* required parameter.
Examples
POST /fhir/AidboxTopicDestination
content-type: application/json
accept: application/json
{
"resourceType": "AidboxTopicDestination",
"meta": {
"profile": [
"http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile"
]
},
"kind": "webhook-at-least-once",
"id": "webhook-destination",
"topic": "http://example.org/FHIR/R5/SubscriptionTopic/QuestionnaireResponse-topic",
"parameter": [
{
"name": "endpoint",
"valueUrl": "https://aidbox.requestcatcher.com/test"
},
{
"name": "timeout",
"valueUnsignedInt": 30
},
{
"name": "maxMessagesInBatch",
"valueUnsignedInt": 20
},
{
"name": "header",
"valueString": "User-Agent: Aidbox Server"
}
]
}
Status Introspection
Aidbox provides $status operation which provides short status information of the integration status:
GET /fhir/AidboxTopicDestination/<topic-destination-id>/$status
content-type: application/json
accept: application/json
{
"resourceType": "Parameters",
"parameter": [
{
"valueDecimal": 2,
"name": "messageBatchesDelivered"
},
{
"valueDecimal": 0,
"name": "messageBatchesDeliveryAttempts"
},
{
"valueDecimal": 2,
"name": "messagesDelivered"
},
{
"valueDecimal": 0,
"name": "messagesDeliveryAttempts"
},
{
"valueDecimal": 0,
"name": "messagesInProcess"
},
{
"valueDecimal": 0,
"name": "messagesQueued"
},
{
"valueDateTime": "2024-10-03T07:23:00Z",
"name": "startTimestamp"
},
{
"valueString": "active",
"name": "status"
},
{
"name": "lastErrorDetail",
"part": [
{
"valueString": "Connection refused",
"name": "message"
},
{
"valueDateTime": "2024-10-03T08:44:09Z",
"name": "timestamp"
}
]
}
]
}
Response format:
| Property | Type | Description |
|---|---|---|
messageBatchesDelivered | valueDecimal | Total number of batches that have been successfully delivered. |
messageBatchesDeliveryAttempts | valueDecimal | Number of batch delivery attempts that failed. It represents the overall failed delivery attempts. |
messagesDelivered | valueDecimal | Total number of events that have been successfully delivered. |
messagesDeliveryAttempts | valueDecimal | Number of delivery attempts that failed. It represents the overall failed delivery attempts. |
messagesInProcess | valueDecimal | Current number of events in the buffer being processed for delivery. |
messagesQueued | valueDecimal | Number of events pending in the queue for send. |
startTimestamp | valueDateTime | AidboxTopicDestination start time in UTC. |
status | valueString | AidboxTopicDestination status is always active, which means that AidboxTopicDestination will try to send all received notifications. |
lastErrorDetail | part | Information about errors of the latest failed attempt to send an event. This parameter can be repeated up to 5 times. Includes the following parameters. |
| valueString | Error message of the given error. |
| valueDateTime | Timestamp of the given error. |
Counter semantics
Most $status fields live in memory on the Aidbox instance that handled the request and are not aggregated across replicas. Only messagesQueued is read from the database and is therefore consistent across the cluster.
| Field | Storage | Cluster-wide? |
|---|---|---|
messagesDelivered | In-memory atom on the instance that delivered the message | No |
messageBatchesDelivered | In-memory atom | No |
messagesDeliveryAttempts | In-memory atom | No |
messageBatchesDeliveryAttempts | In-memory atom | No |
messagesInProcess | In-memory atom | No |
lastErrorDetail | In-memory ring buffer of the last 5 errors per instance | No |
startTimestamp | Time the sender initialized on the local instance | No |
status | Always active (constant) | — |
messagesQueued | Computed from the events table in PostgreSQL | Yes |
Consequences:
- All in-memory counters reset to
0andstartTimestampis updated on Aidbox restart. This is expected, not an error. - In a highly available deployment, calling
$statusthrough a load balancer hits one replica per request — the visible counters change as the load balancer rotates. Two consecutive calls returningmessagesDelivered=6andmessagesDelivered=0are both correct: each instance reports the deliveries it performed. - The sum of
messagesDeliveredacross all instances ≈ total deliveries. Use that as the cluster-wide view. messagesQueuedis the only field safe to read through the load balancer.
To get a stable per-replica view, query each pod directly (Service ClusterIP, headless DNS, port-forward, etc.) instead of going through the load balancer. The AidboxTopicDestination resource itself (and the underlying sender lifecycle) is synchronized across instances via PostgreSQL LISTEN / NOTIFY — since 2603 the notification is sent on the same transaction as the create / update / delete, so non-creator instances see the new state once the transaction commits.
For long-term delivery metrics prefer the receiver-side view (the system that consumes the webhooks) — it is naturally consistent and not affected by Aidbox restarts.
fhirPathCriteria examples
fhirPathCriteria is evaluated for every CRUD operation that matches the topic's resource and supportedInteraction. The expression has access to:
%current— the resource as it will be after the operation.nullfor delete.%previous— the resource as it was before the operation.nullfor create.
Both bindings are available for update. Use them to encode "transition" rules — fire only when something specific changed.
Detect that a specific identifier was just added:
%current.identifier.where(type.coding.code = 'LUMID.PROD').exists()
and %previous.identifier.where(type.coding.code = 'LUMID.PROD').exists().not()
Detect a status change to final:
%current.status = 'final' and %previous.status != 'final'
Detect a value transition (any change to birthDate):
%current.birthDate != %previous.birthDate
Defensive form that also fires on create (treat absent %previous as "no value"):
%current.status = 'final'
and (%previous.empty() or %previous.status != 'final')
If fhirPathCriteria is left empty, every matching CRUD operation fires the trigger.
Troubleshooting
Internal server error: AidboxTopicDestination ... has no sender associated with it.
The destination resource exists in the database but the in-memory sender (the component that actually pushes events to the webhook) is not running on the instance that served the $status call. Possible causes:
| Cause | Diagnosis | Fix |
|---|---|---|
| Sender failed to initialize at startup | Aidbox logs contain aidbox.topics/init-topic-service-exception or a stack trace from aidbox.topics.core/start-topic-service | Restart Aidbox after fixing the underlying issue (most often a malformed AidboxSubscriptionTopic.fhirPathCriteria or unreachable secrets) |
AidboxSubscriptionTopic referenced by topic cannot be resolved | Check the topic value matches an existing AidboxSubscriptionTopic.url exactly (canonical match — see the canonical matching rules in the validator docs) | Fix the URL or create the missing AidboxSubscriptionTopic |
fhirPathCriteria fails to compile | Aidbox logs contain a FHIRPath parse error referring to the topic | Validate the expression — see examples above |
| Multi-instance: destination created on another instance very recently | Wait one second and retry — the LISTEN/NOTIFY propagation is sub-second on a healthy cluster | If the error persists across retries, treat it as a sender-init failure on the local instance and check logs. Pre-2603 builds had a known race here — upgrade if you are on an older version |
Webhook is not firing
Walk through these checks in order — most "missing webhook" reports are fhirPathCriteria mismatches, not transport problems.
- Confirm events are being enqueued.
GET $statuson the destination —messagesQueuedormessagesDeliveredshould increase after a CRUD operation that should match. If neither moves, the trigger did not fire and the rest of the chain is irrelevant. - Verify the topic wiring.
AidboxTopicDestination.topicmust equalAidboxSubscriptionTopic.url, and the topic'strigger.resourceandtrigger.supportedInteractionmust include the resource type and interaction (create/update/delete) you are testing. Recall that PATCH maps toupdate. - Test the FHIRPath expression in isolation. Run the same
fhirPathCriteriaagainst the resource usingPOST /$fhirpath(or any FHIRPath playground) with%currentand%previousbound to representative resources. A common trap: forgetting%previous.empty()for thecreatecase. - Check delivery errors. If
messagesDeliveredis stuck belowmessagesQueued, look atlastErrorDetailin$statusfor the HTTP-level reason (TLS, DNS, 4xx / 5xx from the receiver). Aidbox retries indefinitely — a misconfigured endpoint produces a steadily growingmessagesDeliveryAttempts. - Verify endpoint reachability from Aidbox. From inside the Aidbox container:
curl -v <endpoint>. Webhook URLs that work from your laptop may be unreachable from a Kubernetes pod (private DNS, network policies, egress proxy — see proxy configuration if Aidbox is behind a corporate proxy). - In multi-instance deployments, query
$statuson each instance. A working delivery on one instance and a stuck queue on another indicates a sender that never initialized on that instance — see the table above.
For richer per-event logs, set enableLogging: true on the destination — Aidbox writes one AidboxSubscriptionStatus record per delivery attempt (success or failure) to stdout.