# Microsoft Dynamics 365 (Dataverse) 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:

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/microsoft-dynamics-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/microsoft-dynamics-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/microsoft-dynamics-webhooks/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](https://github.com/hookdeck/webhook-skills/blob/main/skills/microsoft-dynamics-webhooks/references/verification.md).

## The Payload: RemoteExecutionContext

```json
{
  "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)

| `MessageName` | Fires when |
| --- | --- |
| `Create` | A row is created in the step's table |
| `Update` | A row is updated (optionally only for the step's filtering attributes) |
| `Delete` | A 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

```bash
# 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

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/microsoft-dynamics-webhooks/references/setup.md).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/microsoft-dynamics-webhooks/references/overview.md) - How Dataverse webhooks work, RemoteExecutionContext, events, headers, limits
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/microsoft-dynamics-webhooks/references/setup.md) - Registering the webhook and steps in the Plug-in Registration Tool
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/microsoft-dynamics-webhooks/references/verification.md) - Why there is no signature, the HttpHeader pattern, gotchas