Agent skill

Microsoft Dynamics 365 (Dataverse) Webhooks Skill

Receive and authenticate Microsoft Dynamics 365 (Dataverse) webhooks registered with the Plug-in Registration Tool. Use when building a Dynamics 365 or Dataverse webhook receiver, securing it with the HttpHeader authentication option (Dataverse has no HMAC signature), parsing the RemoteExecutionContext JSON body (MessageName, PrimaryEntityName, InputParameters), handling Create, Update and Delete events, or debugging Dataverse webhook failures, timeouts and retries.

Install this skill

npx skills add hookdeck/webhook-skills --skill microsoft-dynamics-webhooks


Dynamics 365 webhooks are a Dataverse service endpoint (contract 8) plus one or more steps you register with the Plug-in Registration Tool. When a step's message fires (for example Create on contact), Dataverse POSTs the event's execution context to your URL as JSON.

This differs from most providers in three ways:

  1. There is no signature. Dataverse doesn't sign the body. There's no HMAC, signing secret or timestamp header. Instead you register the webhook with an authentication option that attaches a credential to every request.
  2. The event is in the body. The body is a serialized RemoteExecutionContext. The event name is MessageName (Create, Update, Delete) and the table is PrimaryEntityName.
  3. Only the status code matters. Dataverse ignores your response body. A non-2xx on a synchronous step fails the user's operation.

When to Use This Skill

  • How do I receive Dynamics 365 / Dataverse webhooks?
  • How do I verify Dynamics 365 webhook signatures? (You can't. Use the HttpHeader authentication option and compare the secret.)
  • How do I register a webhook in the Plug-in Registration Tool?
  • How do I handle Create, Update and Delete events for a table such as contact or account?
  • What is in the RemoteExecutionContext body, and how do I read InputParameters?
  • Why did my Dataverse webhook fail with "Endpoint unavailable"?

Verification (core): the HttpHeader authentication option

When you register the webhook, choose Authentication: HttpHeader and add a key/value pair, for example:

X-Webhook-Secret: 3f9c0e...a long random hex value...

Dataverse sends it on every request. Check it in constant time before you parse the body:

const crypto = require('crypto');

// NOT a Dataverse signature. This is a secret YOU register in the HttpHeader option.
function verifyDynamicsSecret(headerValue) {
  const expected = process.env.DYNAMICS_WEBHOOK_SECRET;
  if (!expected) throw new Error('DYNAMICS_WEBHOOK_SECRET is not set'); // fail closed
  if (typeof headerValue !== 'string') return false;
  const a = Buffer.from(headerValue);
  const b = Buffer.from(expected);
  // Length check first: timingSafeEqual throws on a length mismatch.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express lowercases header names: req.get('X-Webhook-Secret') works too.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Return 401 for a missing or wrong secret and 500 when the secret isn't configured, so the endpoint never accepts everything by accident.

Don't use WebhookKey or HttpQueryString. They put the credential in the query string (?code=... or ?Key1=Value1), where it ends up in access logs and proxies. Hookdeck's source verification doesn't cover them either. See references/verification.md.

The Payload: RemoteExecutionContext

{
  "MessageName": "Update",
  "PrimaryEntityName": "contact",
  "PrimaryEntityId": "6d81597f-0f9f-e711-8122-000d3aa2331c",
  "OperationId": "4af10637-4ea2-e711-8122-000d3aa2331c",
  "OrganizationName": "OrgName",
  "Stage": 40,
  "Mode": 1,
  "InputParameters": [
    {
      "key": "Target",
      "value": {
        "__type": "Entity:http:\/\/schemas.microsoft.com\/xrm\/2011\/Contracts",
        "Id": "6d81597f-0f9f-e711-8122-000d3aa2331c",
        "LogicalName": "contact",
        "Attributes": [
          { "key": "firstname", "value": "James" },
          { "key": "fullname", "value": "James Glynn (sample)" }
        ]
      }
    }
  ],
  "OwningExtension": { "Id": "75417616-4ea2-e711-8122-000d3aa2331c", "LogicalName": "sdkmessageprocessingstep" }
}
  • InputParameters and Attributes are arrays of {key, value} pairs, not objects. Flatten them before use. Dates look like "\/Date(1506384247000)\/".
  • Update carries only the attributes in the request (and only those matching your step's Filtering Attributes). Register images or re-fetch the row for the full record.
  • Over 256 KB, Dataverse adds x-ms-dynamics-msg-size-exceeded and removes ParentContext, InputParameters, PreEntityImages and PostEntityImages. Re-fetch the row by PrimaryEntityId through the Web API.

Dataverse also sends x-ms-dynamics-organization, x-ms-dynamics-entity-name, x-ms-dynamics-request-name and x-ms-correlation-request-id headers.

Events (MessageName values)

MessageNameFires when
CreateA row is created in the step's table
UpdateA row is updated (optionally only for the step's filtering attributes)
DeleteA row is deleted

You register one step per message and table, so you choose which events you receive. If a MessageName isn't one you handle, log it and return 200. If the body has no MessageName, return 400.

Delivery, Retries and Idempotency

  • Success is any 2xx. The timeout is 60 seconds.
  • Dataverse retries once, and only on 502, 503 or 504. Any other failure isn't retried.
  • Webhooks only support ports 80 (HTTP) and 443 (HTTPS).
  • Synchronous steps fail the user's save when your endpoint errors (an "Endpoint unavailable" dialog). The webhook call itself can't be recalled. Asynchronous steps record the failure in a System Job.
  • There's no delivery ID header. Dedupe on OperationId plus the step id (OwningExtension.Id).

Environment Variables

# REQUIRED. The value you registered under the HttpHeader option. Generate with: openssl rand -hex 32
DYNAMICS_WEBHOOK_SECRET=
# OPTIONAL. The header key you registered (case-insensitive). Default: X-Webhook-Secret
DYNAMICS_WEBHOOK_HEADER=X-Webhook-Secret

Local Development

npx hookdeck-cli listen 3000 microsoft-dynamics --path /webhooks/microsoft-dynamics

No account required. The CLI creates a guest account on first run and gives you a public HTTPS URL to use as the webhook's Endpoint URL, plus a web UI for inspecting requests.

In production, Hookdeck's Microsoft Dynamics source type checks the header for you. See references/setup.md.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →