Learn how to configure Dataverse webhooks and Azure Service Bus bindings that fire when table events occur in your model-driven app. This lesson covers endpoint registration, step filtering, entity images, authentication, and production deployment patterns for real integration scenarios.

Picture this: your operations team just closed a high-value contract in your model-driven app. The moment that Opportunity record flips to "Won," your ERP system needs to provision a new project, your Slack workspace should ping the account team, and a third-party billing platform should queue an invoice. None of these systems live inside Dataverse. All of them need to know something happened — right now, not in an hour when someone runs a batch job.
This is exactly the problem that Dataverse webhooks and service endpoint bindings solve. They let Dataverse push event notifications outward to any HTTP endpoint or messaging service the moment a table event fires: a create, update, delete, field change, association, or custom action. You're not polling. You're not scheduling. You're wiring Dataverse into your external systems the same way modern APIs wire everything else together — with event-driven hooks.
By the end of this lesson, you'll know how to configure a webhook endpoint and a service endpoint binding, attach them to table events using plug-in registration, test the whole pipeline, and handle the operational realities of authentication, retries, and failure modes that production deployments actually encounter.
What you'll learn:
You should already be comfortable with how Dataverse tables, columns, and relationships work. You've ideally built or maintained a model-driven app before and understand the concept of server-side logic firing on table operations — if that's new territory, start with Configuring Dataverse Plug-in Steps and Business Event Handlers: Triggering Server-Side Logic on Table Operations before continuing here.
You'll need:
Before you touch a single configuration screen, you need to understand what you're actually choosing between — because the docs sometimes blur these together in confusing ways.
Webhooks are outbound HTTP POST requests. When a table event fires in Dataverse, the platform serializes the execution context (the before/after state of the record, the user who triggered the event, the entity name, the message name, etc.) into a JSON payload and POSTs it to a URL you control. Your receiving endpoint is just an HTTPS endpoint — an Azure Function, an AWS Lambda, a Node.js Express route, a Power Automate HTTP trigger, anything that can receive a POST and return a 2xx.
Service endpoint bindings are a broader concept that includes webhooks but also covers Azure Service Bus queues and topics, Azure Event Hubs, and Azure Relay. Rather than Dataverse making a direct HTTP call, it publishes the execution context as a message to a durable messaging infrastructure. Your consumers pick up messages at their own pace. This gives you decoupling, replay capability, and fan-out that a direct webhook doesn't offer.
The practical decision rule is straightforward:
Key insight
Webhooks run synchronously in the Dataverse pipeline by default when configured in pre/post-operation steps — meaning a slow or failing endpoint can block the user's save operation. Service Bus bindings run asynchronously and are far less likely to impact end-user performance. For production scenarios involving high transaction volumes, strongly prefer asynchronous service endpoint bindings.
Both mechanisms rely on the same underlying infrastructure: Service Endpoint registrations in Dataverse, surfaced through the Plug-in Registration Tool. A webhook is a type of service endpoint. The UI distinguishes them, but architecturally they're siblings.
Let's work with a concrete scenario throughout this lesson. You're building an integration for a professional services firm. When a Project record (a custom table) has its Status column changed to "Active," you want to notify an external project management platform via webhook so it can create a mirrored project. You also want to publish the full execution context to an Azure Service Bus queue for downstream audit and reporting consumers.
For development, you need an HTTPS endpoint that Dataverse can reach. The two most practical options are:
ngrok or the Dev Tunnels feature in VS Code to expose a local portHere's a minimal Azure Function (C#, isolated worker model) that acts as a webhook receiver and logs the payload:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
public class DataverseWebhookReceiver
{
private readonly ILogger<DataverseWebhookReceiver> _logger;
public DataverseWebhookReceiver(ILogger<DataverseWebhookReceiver> logger)
{
_logger = logger;
}
[Function("ProjectStatusWebhook")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
{
_logger.LogInformation("Dataverse webhook received at {Time}", DateTime.UtcNow);
var body = await req.ReadAsStringAsync();
_logger.LogInformation("Payload: {Body}", body);
// In production: deserialize RemoteExecutionContext, validate,
// and dispatch to your project management platform
var response = req.CreateResponse(HttpStatusCode.OK);
return response;
}
}
A few important points about the response behavior:
Warning
Never put long-running processing inside a synchronous webhook receiver. Acknowledge the message immediately (200 OK), push the payload to a queue or background job, and do the actual work asynchronously. A synchronous webhook step that times out will block or fail the user's Dataverse operation.
For the service endpoint binding path, create a Service Bus namespace and queue in Azure:
dataverse-project-events.DataversePublisher with Send permission only.The Plug-in Registration Tool (PRT) is where the actual wiring happens. Download it via NuGet or install through the Power Platform CLI (pac tool prt). Connect it to your environment using your admin credentials.
Connect to your environment. In the PRT, select Create New Connection, choose Office 365, enter your environment URL (e.g., https://yourorg.crm.dynamics.com), and authenticate.
Register a new Service Endpoint. From the top menu, select Register > Register New Web Hook. The dialog that opens is specifically for HTTP webhook-type service endpoints.
Fill in the registration details:
ProjectStatusWebhook (this is your display name in Dataverse)https://yourfunctionapp.azurewebsites.net/api/ProjectStatusWebhookConfigure authentication. This is where most practitioners stumble. Dataverse supports three authentication modes for webhooks:
code query parameter to the URL. This matches what Azure Functions with AuthorizationLevel.Function expects — Dataverse sends the function key as the code parameter.For our Azure Function with AuthorizationLevel.Function, use WebhookKey and paste the function's host key as the value.
Save the registration. You'll see the new webhook appear in the PRT's registered endpoints list.
Tip
Use AuthorizationLevel.Function on your Azure Function (not Anonymous) and register the function key as a WebhookKey in Dataverse. This gives you a lightweight shared secret without requiring full OAuth infrastructure. For enterprise integrations with strict security requirements, put your function behind Azure API Management and use HttpHeader authentication with a managed identity-issued token.
From the PRT top menu, select Register > Register New Service Endpoint. The dialog is different from the webhook dialog:
dataverse-project-events as the queue name. Options include Queue, Topic, and Relay.ProjectEventsServiceBusNote
The Service Bus connection string in Dataverse is stored encrypted but is visible to anyone with the System Administrator role. For production, consider using a dedicated connection string with Send-only permissions — this limits the blast radius if the credential is ever compromised. The receiving consumer uses a separate connection string with Listen permissions.
Registering the endpoint tells Dataverse "this destination exists." Attaching a step to it tells Dataverse "fire this endpoint when this specific thing happens." This two-layer model mirrors how plug-ins work, and understanding it prevents a lot of confusion.
In the PRT, right-click your ProjectStatusWebhook endpoint and select Register New Step.
Fill in the step registration dialog:
Message: This is the Dataverse message name. For standard CRUD operations: Create, Update, Delete. For association/disassociation on many-to-many relationships: Associate, Disassociate. For custom actions: the action's unique name.
Primary Entity: The logical name of the table. For our scenario: cr7f9_project (your publisher prefix will differ — check your table's logical name in make.powerapps.com under the table's Properties).
Secondary Entity: Leave blank for most operations. Used for Associate/Disassociate messages.
Filtering Attributes: This is powerful and underused. Enter a comma-separated list of column logical names. The step will only fire if at least one of these columns changed. For our scenario: cr7f9_status. Without this, every update to a Project record — even changing a note field — triggers the webhook. That's noisy and wastes calls.
Event Pipeline Stage of Execution:
Execution Mode:
For our scenario: PostOperation, Asynchronous. We want the record saved first, then notify the external system.
Deployment: Leave as Server.
Execution Order: An integer (1, 2, 3...) that controls ordering when multiple steps are registered for the same event. Lower numbers execute first.
Key insight
Filtering attributes are one of the most important performance controls available to you. If you're firing a webhook on Update without filtering attributes, you're generating external HTTP calls on every single field save — including system-generated updates from business rules, rollup recalculations, and plug-ins. Always filter to the specific columns that actually matter for your integration. You can read more about how columns are defined and tracked in Configuring Model-Driven App Column Properties: Data Types, Required Fields, and Display Settings for Dataverse Tables.
Repeat the same process for ProjectEventsServiceBus. The step configuration is identical in structure — you're just pointing it at the Service Bus endpoint instead of the HTTP endpoint.
For the Service Bus step, you might choose a broader filter (or no filter) since the messages are durable and cheap to consume — you can let downstream consumers decide what to process. This is a legitimate architectural choice: use the Service Bus as a comprehensive event log and filter at the consumer level.
Both webhooks and service endpoint bindings deliver the RemoteExecutionContext — a serialized snapshot of everything Dataverse knows about the event at the moment it fires. Understanding this payload is essential for writing good receivers.
Here's an annotated breakdown of the key fields in the JSON payload your webhook receives:
{
"BusinessUnitId": "a1b2c3d4-...",
"CorrelationId": "e5f6a7b8-...",
"Depth": 1,
"InitiatingUserId": "c9d0e1f2-...",
"InputParameters": [
{
"key": "Target",
"value": {
"__type": "Entity",
"LogicalName": "cr7f9_project",
"Id": "11223344-...",
"Attributes": [
{
"key": "cr7f9_status",
"value": {
"__type": "OptionSetValue",
"Value": 100000001
}
},
{
"key": "cr7f9_projectid",
"value": "11223344-..."
}
]
}
}
],
"MessageName": "Update",
"Mode": 1,
"OperationCreatedOn": "2024-03-15T14:23:11.0000000Z",
"OrganizationId": "55667788-...",
"OrganizationName": "contosoprofessional",
"OutputParameters": [],
"PostEntityImages": [
{
"key": "PostImage",
"value": {
"__type": "Entity",
"LogicalName": "cr7f9_project",
"Id": "11223344-...",
"Attributes": [
{ "key": "cr7f9_projectname", "value": "Contoso Network Upgrade" },
{ "key": "cr7f9_status", "value": { "__type": "OptionSetValue", "Value": 100000001 } },
{ "key": "cr7f9_clientid", "value": { "__type": "EntityReference", "LogicalName": "account", "Id": "99aabb-..." } },
{ "key": "cr7f9_startdate", "value": "2024-04-01T00:00:00Z" }
]
}
}
],
"PreEntityImages": [
{
"key": "PreImage",
"value": {
"__type": "Entity",
"Attributes": [
{ "key": "cr7f9_status", "value": { "__type": "OptionSetValue", "Value": 100000000 } }
]
}
}
],
"PrimaryEntityId": "11223344-...",
"PrimaryEntityName": "cr7f9_project",
"Stage": 40,
"UserId": "c9d0e1f2-..."
}
A few things to understand about this payload:
InputParameters.Target contains only the columns that were explicitly included in the update call — typically just the changed columns. This is not the full record. If you need the full record state, you use PostEntityImages.
Pre and Post Entity Images are snapshots you have to explicitly configure. They're not automatic. Right-click your registered step in the PRT, select Register New Image, and define what you want captured. For an Update step:
For our scenario, we'd register a PostImage with columns: cr7f9_projectname, cr7f9_status, cr7f9_clientid, cr7f9_startdate. This gives the external system everything it needs to create the mirrored project without making a second API call back to Dataverse.
Warning
Pre and post images add overhead — Dataverse has to query and serialize the record state at the right pipeline stage. Only capture the columns you actually need in your receiver. Requesting a PostImage with all columns on a wide table is wasteful and can noticeably increase payload size and processing time.
Authentication is one area where development habits frequently become production vulnerabilities. Let's be direct about the options and their tradeoffs.
You define a custom header name (e.g., X-Dataverse-Signature) and a secret value. Dataverse includes this header on every POST. Your receiver validates it:
[Function("ProjectStatusWebhook")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
{
// Validate the shared secret header
if (!req.Headers.TryGetValues("X-Dataverse-Signature", out var values)
|| values.FirstOrDefault() != Environment.GetEnvironmentVariable("DATAVERSE_WEBHOOK_SECRET"))
{
_logger.LogWarning("Unauthorized webhook call rejected");
var unauthorizedResponse = req.CreateResponse(HttpStatusCode.Unauthorized);
return unauthorizedResponse;
}
// Process the payload...
var response = req.CreateResponse(HttpStatusCode.OK);
return response;
}
Store the secret value in Azure Key Vault or Application Settings — never hardcode it.
Dataverse sends webhooks from a known set of outbound IP ranges published by Microsoft for each region. Your network firewall or Azure Function access restrictions can allowlist these ranges, adding a network-layer defense. Check the official Microsoft documentation for the current IP ranges for your Dataverse region — they do change with infrastructure updates.
For high-security environments, place your function behind Azure API Management configured with client certificate validation. This is significantly more complex but provides the strongest guarantee that calls genuinely originate from Dataverse.
Theory only takes you so far. Here's how to actually validate the pipeline is working before you go live.
The Plug-in Registration Tool has a built-in test facility. Right-click your registered step and look for the option to trigger a test. This sends a synthetic execution context to your endpoint, letting you verify connectivity and authentication without touching real records.
Create or update a Project record in your model-driven app, triggering the status change. Then:
Tip
During development, temporarily register a synchronous step pointing to a request inspection service like https://webhook.site or https://requestcatcher.com. These free services give you a live view of exactly what Dataverse sends, without any infrastructure on your end. Switch to your real endpoint and async mode before going to production.
For Service Bus, install the Service Bus Explorer tool or use the Azure portal's built-in Service Bus Explorer. After triggering a table event, check the queue for a message. The message body will contain the JSON execution context. Verify that:
Webhook and service endpoint registrations are solution-aware. They appear in the SDKMessageProcessingStep, ServiceEndpoint, and SdkMessageProcessingStepImage component types when you add them to a solution. This means you can package them with your model-driven app and deploy them through managed solutions.
Warning
When you export a service endpoint registration in a managed solution, the connection string or authentication secret is not exported. The endpoint URL and configuration are exported, but the secret value must be re-entered manually in each target environment after import. Plan this into your deployment runbook, and consider using Dataverse Environment Variables to manage the endpoint URLs per-environment so DEV, UAT, and PROD can point to different receivers without customizing the solution.
The typical deployment pattern for a production integration:
This keeps your solution clean and version-controlled while keeping secrets out of solution packages.
If you've already worked with Configuring Dataverse Table Event Plugins and Real-Time Workflows in Model-Driven Apps, you might be wondering: when do I use a webhook vs. a plug-in vs. a real-time workflow?
The decision tree looks like this:
| Scenario | Best Tool |
|---|---|
| Complex server-side business logic with database access | Plug-in (C#) |
| Simple field calculations and validations, no code | Business rules or real-time workflow |
| Notify an external HTTP endpoint about an event | Webhook (service endpoint) |
| Publish events to Azure messaging for fan-out consumption | Service Bus / Event Hubs service endpoint |
| Long-running multi-step automation triggered by an event | Power Automate cloud flow with Dataverse trigger |
Webhooks occupy a specific niche: they're for outbound notifications to external systems. They shouldn't contain logic — they should transport context. If you find yourself wanting to put conditional logic into how a webhook fires, that logic belongs in a plug-in that decides whether to invoke the webhook, or in the receiving service that decides how to handle the message.
You can even combine them: a plug-in step that fires first (PreValidation) validates the operation, then a webhook step fires (PostOperation async) to notify external systems after the commit. These coexist on the same event registration tree.
Let's build the complete pipeline described throughout this lesson. By the end of this exercise, changing a Project record's Status to "Active" will POST a webhook notification to an Azure Function and publish a message to Azure Service Bus.
Step 1: Create the receiver Function
Deploy the Azure Function from the code sample above to a function app in your Azure subscription. Note the function URL including the function key.
Step 2: Create the Service Bus infrastructure
Create a Service Bus namespace and queue dataverse-project-events. Create a Shared Access Policy DataversePublisher with Send permission.
Step 3: Register both endpoints in PRT
Connect PRT to your Dataverse environment.
ProjectStatusWebhook, URL: your function URL, Auth: WebhookKey with your function key.dataverse-project-events, Format: JSON.Step 4: Register steps on both endpoints
For the webhook:
ProjectStatusWebhook > Register New StepUpdate, Primary Entity: your project table's logical nameRegister a PostImage on this step with columns: project name, status, client lookup, start date.
For the Service Bus endpoint, register the same step configuration.
Step 5: Test
Open your model-driven app, find a Project record, change Status to "Active," and save.
Check:
Step 6: Inspect the payload
Log the full payload body in your function and inspect it in Application Insights. Verify that the PostImage contains the expected columns. Intentionally change a column that's NOT in your filtering attributes (e.g., a notes field) and confirm the webhook does NOT fire.
"The webhook fired but I got a 401 Unauthorized."
Your authentication configuration doesn't match what your endpoint expects. If you're using an Azure Function with AuthorizationLevel.Function, make sure you registered the webhook with WebhookKey and the correct function host key (not the function-level key — use the host key unless you've configured function-level keys). If you used AuthorizationLevel.Anonymous, you should use HttpHeader for a shared secret instead.
"The webhook registered fine but never fires."
Check the filtering attributes. If you configured filtering on cr7f9_status but the column's actual logical name in your environment is new_status (different publisher prefix), the filter never matches and the step silently does nothing. Verify the logical name in make.powerapps.com under the table's column list.
"The step fires on every update, not just status changes." Your filtering attributes are blank or incorrectly specified. Revisit the step registration in PRT and confirm the filtering attributes field contains the exact logical column name.
"Dataverse System Jobs shows the step in 'Failed' state." Click into the failed job to see the error. Common causes: endpoint returned a non-2xx status code, endpoint timed out (exceeded 60 seconds for synchronous steps or the async timeout), network unreachable. Check that your Azure Function URL is publicly accessible — Dataverse cannot call a localhost URL or a URL behind a corporate VPN without network configuration.
"Pre/Post images are empty in my payload." Images must be explicitly registered as separate components under the step. Registering the step does not automatically include images. Right-click the step in PRT, select Register New Image, and configure the image type and columns. After saving, re-trigger the event.
"The service endpoint message arrives in Service Bus but the body is empty." Check the User Information Sent setting on the service endpoint registration. Also verify the message format is JSON (not XML). If the entity images are missing, revisit image registration on the step.
"My webhook worked in DEV but not after deploying to PROD." The service endpoint URL and authentication credentials aren't automatically migrated with the solution. Connect PRT to PROD and update the endpoint URL and authentication values to point to the PROD Function App.
Tip
Set up an Application Insights alert on HTTP 4xx and 5xx responses from your webhook function. This gives you proactive alerting when Dataverse-initiated calls start failing — without it, you won't know notifications are dropping until a business user notices the external system is out of sync.
You've now covered the full lifecycle of Dataverse webhook and service endpoint bindings: understanding the architectural choice between direct HTTP webhooks and durable Azure messaging, registering both endpoint types in the Plug-in Registration Tool, attaching step registrations to specific table events with filtering attributes, configuring pre and post entity images, handling authentication, and deploying through solutions.
The practical takeaways are:
As a next step, explore how Configuring Dataverse Auditing and Field-Level Change History in Model-Driven Apps complements webhook-based integrations. Auditing gives you the internal Dataverse record of what changed and when; webhooks push that information to external systems. Together they give you a complete, traceable event history both inside and outside the platform.
If your integration needs move beyond simple notifications into complex server-side logic that needs to run before the Dataverse write completes, revisit plug-in architecture in Configuring Dataverse Table Event Plugins and Real-Time Workflows in Model-Driven Apps. And if you're packaging this integration for deployment across multiple environments, Solutions for Model-Driven Apps: Publishers, Managed vs Unmanaged, and Solution Layering will give you the full picture on managing solution components through the deployment lifecycle.