Aidbox Docs

Webhook AidboxTopicDestination

This functionality is available starting from version 2410 and requires FHIR Schema validation engine to be enabled.

Aidbox version compatibility

AidboxProfile URL
≥ 2604http://health-samurai.io/fhir/core/StructureDefinition/aidboxtopicdestination-webhookAtLeastOnceProfile
< 2604http://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 maxEventNumberInBatch parameter) 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 nameValue typeDescription
endpoint *valueUrlWebhook URL.
timeoutvalueUnsignedIntTimeout in seconds to attempt notification delivery (default: 30).
keepAlivevalueIntegerThe time in seconds that the host will allow an idle connection to remain open before it is closed (default: 120, -1 - disable).
maxMessagesInBatchvalueUnsignedIntMaximum number of events that can be combined in a single notification (default: 20).
headervalueStringHTTP 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
200 OK
{
 "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:

PropertyTypeDescription
messageBatchesDeliveredvalueDecimalTotal number of batches that have been successfully delivered.
messageBatchesDeliveryAttemptsvalueDecimal

Number of batch delivery attempts that failed.

It represents the overall failed delivery attempts.

messagesDeliveredvalueDecimalTotal number of events that have been successfully delivered.
messagesDeliveryAttemptsvalueDecimal

Number of delivery attempts that failed.

It represents the overall failed delivery attempts.

messagesInProcessvalueDecimalCurrent number of events in the buffer being processed for delivery.
messagesQueuedvalueDecimalNumber of events pending in the queue for send.
startTimestampvalueDateTimeAidboxTopicDestination start time in UTC.
statusvalueStringAidboxTopicDestination status is always active, which means that AidboxTopicDestination will try to send all received notifications.
lastErrorDetailpartInformation about errors of the latest failed attempt to send an event. This parameter can be repeated up to 5 times. Includes the following parameters.

lastErrorDetail

.message

valueStringError message of the given error.

lastErrorDetail

.timestamp

valueDateTimeTimestamp 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.

FieldStorageCluster-wide?
messagesDeliveredIn-memory atom on the instance that delivered the messageNo
messageBatchesDeliveredIn-memory atomNo
messagesDeliveryAttemptsIn-memory atomNo
messageBatchesDeliveryAttemptsIn-memory atomNo
messagesInProcessIn-memory atomNo
lastErrorDetailIn-memory ring buffer of the last 5 errors per instanceNo
startTimestampTime the sender initialized on the local instanceNo
statusAlways active (constant)
messagesQueuedComputed from the events table in PostgreSQLYes

Consequences:

  • All in-memory counters reset to 0 and startTimestamp is updated on Aidbox restart. This is expected, not an error.
  • In a highly available deployment, calling $status through a load balancer hits one replica per request — the visible counters change as the load balancer rotates. Two consecutive calls returning messagesDelivered=6 and messagesDelivered=0 are both correct: each instance reports the deliveries it performed.
  • The sum of messagesDelivered across all instances ≈ total deliveries. Use that as the cluster-wide view.
  • messagesQueued is 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. null for delete.
  • %previous — the resource as it was before the operation. null for 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:

CauseDiagnosisFix
Sender failed to initialize at startupAidbox logs contain aidbox.topics/init-topic-service-exception or a stack trace from aidbox.topics.core/start-topic-serviceRestart Aidbox after fixing the underlying issue (most often a malformed AidboxSubscriptionTopic.fhirPathCriteria or unreachable secrets)
AidboxSubscriptionTopic referenced by topic cannot be resolvedCheck 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 compileAidbox logs contain a FHIRPath parse error referring to the topicValidate the expression — see examples above
Multi-instance: destination created on another instance very recentlyWait one second and retry — the LISTEN/NOTIFY propagation is sub-second on a healthy clusterIf 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.

  1. Confirm events are being enqueued. GET $status on the destination — messagesQueued or messagesDelivered should increase after a CRUD operation that should match. If neither moves, the trigger did not fire and the rest of the chain is irrelevant.
  2. Verify the topic wiring. AidboxTopicDestination.topic must equal AidboxSubscriptionTopic.url, and the topic's trigger.resource and trigger.supportedInteraction must include the resource type and interaction (create / update / delete) you are testing. Recall that PATCH maps to update.
  3. Test the FHIRPath expression in isolation. Run the same fhirPathCriteria against the resource using POST /$fhirpath (or any FHIRPath playground) with %current and %previous bound to representative resources. A common trap: forgetting %previous.empty() for the create case.
  4. Check delivery errors. If messagesDelivered is stuck below messagesQueued, look at lastErrorDetail in $status for the HTTP-level reason (TLS, DNS, 4xx / 5xx from the receiver). Aidbox retries indefinitely — a misconfigured endpoint produces a steadily growing messagesDeliveryAttempts.
  5. 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).
  6. In multi-instance deployments, query $status on 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.

Last updated: