Webhooks

Dragon Copilot uses a pub/sub model to deliver AI output to your integration. To enable this communication, provision a webhook using the provisioning service. The webhook will be notified when relevant events occur within the processing pipeline. When your webhook receives a notification, you can use the retrieval service to retrieve the detailed data associated with the notification. For example, when ambient speech is captured during a provider/patient encounter, Microsoft will notify the webhook when the transcript of the encounter is generated. The integrator can then retrieve the transcript.

The diagram below depicts this workflow.

dragon data exchange workflow

Be aware of the following:

  • A separate subscription must be provisioned for each customer. The URL of the webhook can be the same for multiple customers, as the notification contains a customer identifier. The per-customer subscription is required to support both filtering and authorization.
  • The lifetime of the webhook subscription is not limited; you can decide how long it lasts.
  • You can only subscribe to events for a customer if the customer has a license for your Dragon Copilot integrated product. For example, to enable Partner 1 to register a webhook to receive Dragon Copilot messages for Customer A, Customer A must have a license for 'Dragon Copilot for Partner 1'.
  • The webhook must be an HTTPS endpoint exposed to the public internet.

Messages

When Dragon Copilot generates an event, it POSTs a message to your webhook URL. It identifies your webhook URL by looking up the webhook subscriptions you've provisioned for that customer. If you used the optional EhrInstanceId or ProductId parameters when you created the subscription, this is also used to filter the webhook subscriptions and identify the corresponding webhook URL for notification delivery.

Sample Request

Headers:

  {
    "Content-Type": ["application/cloudevents+json; charset=utf-8" ],
    "x-ms-request-id": ["a0deffcc-3cbc-4df6-a954-6cd001d5cdea"  ],
    "traceid": [ "a0deffcc-3cbc-4df6-a954-6cd001d5cdea"],
    "Customer-Id": ["4b2a8719-51c2-4756-aaf7-d40850d5e008" ],
    "x-signature":["+nx2XgfuWokr3apBT6NN2MC7BVbJNwF7KVTDiUdVbIE="], // only added for HMAC based subscriptions
    "DynamicHeaderName": [ "value of the mapped property from the payload"], //custom header
    "StaticHeaderName": [ "StaticHeaderValue" ], // custom header
  }

Body:

   {
      "id": "74c5e852-0e7a-4fcd-b50a-a761146d60d9",
      "source": "ad043372-b587-40ff-b444-f225757ab1a2",
      "partnerid": "c5e2cb45-46d4-4f3f-8599-6d08d04fa3d3",
      "type": "encounter_data_ready_complete",
      "data":
      {
         "schemaVersion": "1",
         "dataVersion": "{\"major\":2,\"minor\":2,\"revision\":0,\"metadata\":{\"DAXCore:DataSource\":\"AI-DAXOne\",\"DAXCore:DataOptimizationPending\":\"true\",\"DAXCore:DataOptimizationDone\":\"false\",\"DAXCore:DataOptimizationStartTime\":\"2024-01-29T14:15:25.749309686Z\",\"DAXCore:DataOptimizationTimeout\":\"210\"},\"quality\":\"Unspecified\"}",
         "customerId": "00000000-1111-2222-3333-444444444444",
         "correlationId": "2f1e75db-a4fa-4aa0-aea2-95ad125e356f",
         "retrievalUrl": "https://<partnerapi-service-host>/retrieval/notifications/74c5e852-0e7a-4fcd-b50a-a761146d60d9?api-version=2&customerId=00000000-1111-2222-3333-444444444444",
         "feedbackUrl": "https://<partnerapi-service-host>/feedback/notifications?api-version=2&customerId=00000000-1111-2222-3333-444444444444",
         "userId": "6eb28cbf-cb07-4340-8695-fe01e8eec25f"
      },
      "time": "2023-09-11T18:30:58.0787659+00:00",
      "specversion": "1.0",
      "datacontenttype": "application/json",
      "subject": "ad043372-b587-40ff-b444-f225757ab1a2",
      "eventfamily": "dax",
      "ehrinstanceid": "4fa0d8ac-693a-4bf1-845e-22b815987e2a",
      "productid": "03f7077b-4f0e-41b8-a79c-f37926efcb8d",
      "traceparent": "00-f004beb60030171ebb5f3826b4afa16b-990d1bc5c0094d95-00"
   }

Header Properties

Property Description
Content-Type Message content type
x-ms-request-id Unique identifier used to identify and trace a specific request or session across different systems and services
traceid Unique identifier used to uniquely identify and trace a specific request or session across different systems and services
x-signature A digital signature added to the headers only for HMAC based subscriptions.
DynamicHeaderName A dynamic header added to the request sent to the webhook. The value is populated from the corresponding property in the message/event payload e.g. subject, eventFamily etc. It also supports dot-notation e.g. data.customerId , data.correlationId
See Custom webhook delivery headers.
StaticHeaderName A static header added to the request sent to the webhook. Value remains constant.
See Custom webhook delivery headers.

Message properties

Property Description Type
id The unique identifier of the notification. GUID
source The unique identifier of the context in which an event happened. Currently, its value is set to customerId. GUID
partnerid The Microsoft-defined identifier of the partner for whom a notification is intended. GUID
type The notification event type. String
time The time the notification was published in ISO 8601 format. DateTime
specversion The cloud event specification version. String
datacontenttype The content type of the data object. String
subject The subject of the event in the context of the event producer (identified by source). Currently, its value is set to customerId. String
eventfamily The notification event family. This can be one of the following values:
- dax
- nursing
String
ehrInstanceId (Optional) The Microsoft-defined identifier of the EHR instance for the customer for whom the notification is intended. String
productid (Optional) The Microsoft-defined identifier of the product. See Product identifiers. String
data The data envelope for the notification. Object
data.schemaVersion The version of the data envelop schema. String
data.dataVersion The payload version and metadata. JSON(string)
data.dataVersion.major The cumulative recording number from which the data was generated. For example, major version 2 was generated from recordings 1 and 2. Number
data.dataVersion.minor The aggregation state. 0 indicates no aggregation issues. Non-zero indicates that data generation restarted due to aggregation issues. Number
data.dataVersion.revision The number of the AI output when multiple outputs are generated from same inputs. A larger number indicates improved output. Number
data.dataVersion.quality The state of the AI output. This can be one of the following values:
- "Complete"
- "Intermediate"
- "Transiently Degraded"
- "Permanently Degraded"
String
data.dataVersion.metadata Product specific metadata about the AI output. String
data.customerId The Microsoft-defined identifier of the customer for whom a notification is generated. GUID
data.correlationId The partner-defined correlation identifier that was passed to the DAXKit SDK. This identifier should be a GUID. Avoid sending an identifier that could link to PHI. String
data.retrievalUrl The URL from which data associated with the notification can be retrieved. See Retrieving the data associated with a notification. String
data.feedbackUrl The URL to which associated feedback can be submitted. See Providing feedback. String
data.userId The Microsoft-defined identifier of the user. String

Custom webhook delivery headers

The APIs to manage your webhook (create subscription and update subscription) enable you to add custom headers to your webhook subscriptions. This means you can set environment-specific data or any other data to a webhook and retrieve that information later.

Use the optional customDeliveryHeaders property.

If you set headers when creating or updating your subscription, the headers are returned in the response when you retrieve information on a single webhook or list of webhooks.

Dynamic vs Static headers

User can add both dynamic and static customer headers in the request payload.

Static Header:

  • Value is constant


Example: x-static-env: production

Dynamic Header:

  • value is extracted from the event payload.
  • supports dot-notation path


Example: data.customerId

Delivery header model

"customDeliveryHeaders": [
    {
      "name": "StaticHeaderName",
      "kind": "Static",
      "value":"StaticHeaderValue"
    },
    {
      "name": "DynamicHeaderName",
      "kind": "Dynamic",
      "value": "value of the mapped property from the payload e.g. data.customerId"
    }
]

Property customDeliveryHeaders is optional. If headers are set, they are retuned in the response when retrieving information on a single webhook or list of webhooks.

Reserved headers

Azure Event Grid allows you to add up to ten custom headers per subscription. However, up-to four out of the ten headers are reserved by the system. Reserved headers can't be overridden.

The following headers are reserved:

Header name Type Mapped to payload path Notes
traceid Dynamic traceid
x-ms-request-id Dynamic traceid
Customer-Id Dynamic data.customerId
x-signature Dynamic signature Added only for HMAC subscriptions.

Null vs empty headers

If customDeliveryHeaders isn't provided, or is an empty array, the webhook subscription is updated as follows:

customDeliveryHeaders value Behavior of the UPDATE call
null (not specified). Treated as NULL. The previously set header, if any, is retained.
[] (empty array). Previously set headers are nullified.
Populated with a list corresponding to the schema described above Replace custom headers with new set.

Securing the webhook

You can use the following methods to secure your webhook:

  • A secret as a query parameter.
  • Bearer token authentication with Microsoft Entra ID.
  • A shared secret with a hash-based message authentication code (HMAC).

A secret as query parameter

You can pass a secret to the provisioning service as the accessToken parameter of the subscription creation API. When your webhook is invoked, the secret is added to the webhook destination URL as the value of the access_token query string parameter.

https://partner_end_point/webhook?access_token=<your secret>

When the webhook receives a notification, it should retrieve and validate the secret. If the secret is updated, the notification subscription also needs to be updated. To avoid delivery failures during this secret rotation, you must make the webhook accept both old and new secrets for a limited duration before updating the event subscription with the new secret.

The partner API doesn't store secrets and they're not logged as part of the service logs/traces. The subscription API to retrieve subscription details doesn't return the secret.

Bearer token authentication with Microsoft Entra ID

This is the most secure authentication mechanism but requires the most configuration.

The requirements for bearer token authentication include the following:

  • Your webhook must be running in Azure in the context of a Microsoft Entra application (service principal or managed identity).
  • You must run a script to configure delivery of events to HTTPS endpoints protected by a Microsoft Entra application.

When the webhook receives event notifications, the HTTP Authorization header contains a bearer token issued by the Partner API's Microsoft Entra tenant. Validate the azp or appid claim (whichever is present in the token). This claim contains the application (client) ID 4962773b-9cdb-44cf-a8bf-237846a00ab7 for the Microsoft.EventGrid service that publishes the events, and you can use it to authorize the request if needed.

To use bearer token authentication, do the following in your Microsoft Entra tenant:

  1. Create a Microsoft Entra application for the webhook configured to work with your Microsoft Entra tenant (single tenant).

  2. Open the Azure Shell and select the PowerShell environment.

  3. Modify the $webhookAadTenantId value to connect to your tenant.

    $webhookAadTenantId: Microsoft Entra tenant ID for your tenant.

    PowerShell commands:

    PS /home/user>$webhookAadTenantId = "<your tenant ID>"

    PS /home/user>Connect-AzureAD -TenantId $webhookAadTenantId

  4. Open the following script: Secure WebHook delivery with Microsoft Entra Application in Azure Event Grid

    Update the value of $webhookAppObjectId to match the object ID of the application created in step 1.

    Update the value of $eventSubscriptionWriterAppId based on the environment:

    • Non-production: abdbd85f-fc72-4ca2-8fe9-dee028835f6e
    • Production: 45ac5a85-f5ea-4ef1-8566-10c4ebdc5c4c
  5. Run the script.

Be aware of the following:

  • You don't need to modify the value of $eventGridAppId.
  • In this script, AzureEventGridSecureWebhookSubscriber is set for $eventGridRoleName.
  • You must be a member of the Microsoft Entra Application Administrator role or an owner of the service principal of the webhook application in Microsoft Entra ID to execute this script.

A shared secret with an HMAC

A hash-based message authentication code (HMAC) is a type of message authentication code (MAC) for creating digital signatures using different hash algorithms, such as SHA256. HMAC can be used to simultaneously verify both the data integrity and authenticity of a message.

This approach requires you to pass the HMAC secret and algorithm to the subscription creation API in the request headers x-hmac-secret and x-hmac-algorithm. When we publish the notification to your webhook, we include the hash or signature calculated using the secret and algorithm that you shared when you created the subscription. On receiving this notification on your webhook, you should be able to extract the signature value from the x-signature request header and verify the hash using the same secret and algorithm.

Be aware of the following:

  • You can rotate your secrets using the subscription update API. To avoid delivery failures during this secret rotation, you must make the webhook support both old and new secrets for a limited duration before updating the subscription with the new secret.
  • Dragon data exchange securely stores the secrets and they're not logged as part of the service logs/traces. The subscription API to retrieve subscription details doesn't return the secret.
  • Dragon data exchange supports a single secret for all subscriptions for a given customer. For example, if you need to create more than one webhook subscription for a given customer using HMAC, the shared secret will need to be the same for all subscriptions for that customer. You can use different shared secrets for subscriptions with different customers.

Implementation prerequisites

  • You must create an HMAC-enabled webhook subscription so that Dragon data exchange can embed an x-signature header in the message posted to your webhook.
  • The HMAC secret and algorithm that you used when you created/updated the subscription must be retained and must be available when processing a webhook notification delivery.

Implementation procedure

Do the following when processing the notification delivery:

  1. Extract the x-signature header from the HTTP request headers.

    Preserve the value of the x-signature header as it will be compared with the signature that you will be generating in the next few steps.

  2. Compute the HMAC signature from the request payload.

    Extract the time field from the payload as the nonce. This value is in ISO 8601 format; you must convert it to UnixTimeMilliseconds before using at as the nonce.

    Extract the data field as data. You must serialize it before using it.

    Build the message to be used in computing the signature by combining the nonce and data with a pipe symbol: time + "|" + data.

    Compute the HMAC signature using the message built above, the secret and the algorithm that was used when creating the subscription.

  3. Compare the signature extracted in step 1 with the signature computed in step 2.

    If they match, it implies the message is authentic and its integrity is intact.

    If they don't match, it implies the message isn't authentic and might have been tampered with.

For a code sample, see: Processing an HMAC-based message.

Implementing the webhook logic

For more information on using webhooks for event delivery using cloud event schema, see: https://github.com/cloudevents/spec/blob/v1.0/http-webhook.md

Validation

  • The validation request is made at the time of provisioning a subscription and uses the HTTP OPTIONS method.

  • The implementation of this method should handle the following:

    The WebHook-Request-Origin header must be included in the validation request.

    The validation response must include the corresponding WebHook-Allowed-Origin header set with the above header value.

    Expect a Webhook-Request-Rate header in the validation request.

    The validation response must include the corresponding Webhook-Allowed-Rate header. This header can be set to an asterisk (*) indicating no request rate, or the string representation of a non-zero integer expressing the permitted request rate in "requests-per-minute".

Sample implementation of the OPTIONS method in your ASP.NET Core controller:

[HttpOptions] 
public Task<IActionResult> Options() 
{
    if (!Request.Headers.TryGetValue("WebHook-Request-Origin", out var headerValues))
    {
        return Task.FromResult<IActionResult>(new BadRequestObjectResult("Webhook validation is failed due to missing [Webhook-Request-Origin] header in the request"));
    }

    using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
    {
        // Retrieve the request origin
        var webhookRequestOrigin = headerValues.FirstOrDefault(); 

        // Respond with the origin and rate to confirm acceptance of incoming notifications
        HttpContext.Response.Headers.Add("WebHook-Allowed-Rate", "*");
        HttpContext.Response.Headers.Add("WebHook-Allowed-Origin", webhookRequestOrigin); 
    } 
    return Task.FromResult<IActionResult>(new OkResult()); 
}

Registering a webhook

Register your webhook using the provisioning service. You can use the sample code below as a model.

JWT access token

You need a JWT access token issued by your Microsoft Entra tenant to be able to provision your webhook and create a subscription with Dragon data exchange.

Example using a managed identity:

using Azure.Identity;
var scope = "<scope for non-production or production environment>";
var credential = new ManagedIdentityCredential();
var tokenRequestContext = new Azure.Core.TokenRequestContext(scopes: new[] { scope });
var token = credential.GetTokenAsync(tokenRequestContext).GetAwaiter().GetResult().Token;

The scope variable determines the value of the audience claim within the access token. Set the scope accordingly:

  • Non-production: 105be974-d66d-43c9-b813-57a967bbfd21/.default
  • Production: 105be974-d66d-43c9-b813-57a967bbfd21/.default

Example using a service principal:

var credential = new ClientSecretCredential(<your-tenant-id>, <your-client-id>, <your-client-secret>);
var tokenRequestContext = new Azure.Core.TokenRequestContext(scopes: new[] { scope });
var token = credential.GetTokenAsync(tokenRequestContext).GetAwaiter().GetResult().Token;

Sample token

The audience claim is determined by the environment scope:

  • Non-production: 105be974-d66d-43c9-b813-57a967bbfd21
  • Production: 105be974-d66d-43c9-b813-57a967bbfd21
{ 
    "aud": "<audience for non-production or production environment>", 
    "iss": "https://sts.windows.net/ed5693bc-117f-4001-a14c-5f50a530d5df/", 
    "iat": 1694573195, 
    "nbf": 1694573195, 
    "exp": 1694577095, 
    "aio": "E2FgYHj3uiYtwfCoCXu9T435TV1GAA==", 
    "appid": "6462b4d6-0a18-45ad-bd11-c2bc1659765e", 
    "appidacr": "1", 
    "idp": "https://sts.windows.net/ed5693bc-117f-4001-a14c-5f50a530d5df/", 
    "oid": "99b68f7a-88e8-4247-9339-e281147febcf", 
    "rh": "0.AVgAvJNW7X8RAUChTF9QpTDV30HAZMH7w4xNn_Ss8mW_Jz9YAAA.", 
    "sub": "99b68f7a-88e8-4247-9339-e281147febcf", 
    "tid": "ed5693bc-117f-4001-a14c-5f50a530d5df", 
    "uti": "rLdHl5gWnkWgEE7Tj0UXAA", 
    "ver": "1.0" 
}

Provisioning service call

Call the provisioning service API to register your webhook using the access token generated above.

The base URL is different for production and non-production environments; see Base URL.

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 
var baseUrl = "<base URL for non-production or production environment>";
var response = await httpClient.PostAsync(baseUrl + "subscriptions?api-version=2&customerId=<customerId>", requestJsonPayload); 
var responseString = await response.Content.ReadAsStringAsync();

Sample implementation

See Sample webhook implementation.