Webhooks
A webhook is a serviceendpoint row (contract 8, Webhook) and a processing step on it: when the step's message runs, xrmverse
sends the execution context to your HTTP receiver. Webhooks need no Microsoft SDK on the receiver's side and no plug-in assembly,
which makes them the way to extend xrmverse without a .NET Framework plug-in (see Plug-ins for those). Only
contract 8 is supported; every other contract and authentication type is refused (400 0x80040203) at Create and Update.
Registering a webhook
An endpoint and a step, with any of the three clients.
CLI
xrmd webhook create --name new_hook --url https://hooks.example.test/in --auth header --value X-Api-Key=s3cr3t
xrmd plugin step add --webhook new_hook --message Create --entity account --stage 40 --mode async
xrmd webhook list
xrmd webhook show new_hook
xrmd webhook update new_hook --url https://hooks.example.test/v2
xrmd webhook delete new_hook --yes
--auth is key (one --value, sent as the query parameter code), header or query (--value K=V, repeatable). The CLI builds
the settings XML. list and show print the url without its query string (a query may carry a key) and never the auth value: the
server does not return it, isauthvalueset tells whether one is stored. plugin step add takes --webhook <name> instead of --type
(exactly one of them, and --config is refused with --webhook); plugin list shows webhook steps under a "Webhooks" heading (in
-o json they follow the plug-in rows, with a webhook property). Exit codes: 5 refused by the server, 4 not found, 2 usage.
Web API
POST /api/data/v9.2/serviceendpoints
{ "name": "new_hook", "contract": 8, "authtype": 5, "url": "https://hooks.example.test/in",
"authvalue": "<settings><setting name=\"X-Api-Key\" value=\"s3cr3t\" /></settings>" }
POST /api/data/v9.2/sdkmessageprocessingsteps
{ "name": "new_hook: Create of account", "stage": 40, "mode": 1, "rank": 1,
"eventhandler_serviceendpoint@odata.bind": "/serviceendpoints(<id>)",
"sdkmessageid@odata.bind": "/sdkmessages(<id>)", "sdkmessagefilterid@odata.bind": "/sdkmessagefilters(<id>)" }
SDK (SOAP): create an Entity("serviceendpoint") with contract = OptionSetValue(8), authtype, url and authvalue, then a
step whose eventhandler is EntityReference("serviceendpoint", id) and whose plugintypeid is empty. The Plug-in Registration
Tool's "Register New WebHook" does the same.
Columns and rules
| Column | Rule |
|---|---|
contract |
8 (required) |
authtype |
4 Webhook Key, 5 Http Header, 6 Http Query String |
url |
absolute http or https, no user info, no fragment; port 80 or 443 unless Pipeline:WebhookAllowAnyPort is true |
authvalue |
required on Create, write-only (never returned). Type 4: any non-blank string. Types 5 and 6: <settings><setting name="K" value="V" />…</settings> with at least one setting and non-blank names |
isauthvalueset |
read-only Boolean |
messageformat, messagecharset, connectionmode, userclaim |
null, or: messageformat 2 (Json), messagecharset 0 (Default) or 1 (UTF8), connectionmode 1 (Normal), userclaim 1 (None); the others apply to Azure Service Bus and are refused |
namespaceaddress, path, solutionnamespace, saskeyname |
must be null |
A header name must be an HTTP token, not Content-Type, Content-Length, Host or Transfer-Encoding, and not start with x-ms-;
header values must be printable ASCII. A step on a webhook has eventhandler = the endpoint and no plugintypeid and no
configuration; stage, mode, filter, filtering attributes, images, rank, impersonating user and asyncautodelete follow the plug-in
step rules, and so does a custom API's allowedcustomprocessingsteptype. Deleting a webhook deletes the endpoint, its steps and
their images; its queued jobs are cancelled (3/32). No step runs on a message of serviceendpoint, not even one without a
table filter (its messages are not events). A step's handler changes through an Update of eventhandler; Associate and
Disassociate over plugintype_sdkmessageprocessingstep or serviceendpoint_sdkmessageprocessingstep are refused (400). The table is serviceendpoint
(object type code 4618, see Tables in every organization).
The request sent
POST {url} with the RemoteExecutionContext as the JSON body (Content-Type: application/json; charset=utf-8), produced by the
DataContractJsonSerializer over the SDK types, so the shape is the one the SDK defines. The SDK stays bring-your-own: webhooks need
it installed on the server like plug-ins do (see Installing the Microsoft SDK).
| Header | Value |
|---|---|
x-ms-dynamics-organization |
the organization name (Dataverse sends its domain name) |
x-ms-dynamics-entity-name |
PrimaryEntityName |
x-ms-dynamics-request-name |
MessageName |
x-ms-correlation-request-id |
CorrelationId |
Authentication: type 4 adds the query parameter code; type 5 adds the headers; type 6 adds the query parameters. The existing query
parameters of the url are kept. A body over 256 KB (262,144 bytes) is sent again trimmed of ParentContext, InputParameters,
PreEntityImages and PostEntityImages, with the header x-ms-dynamics-msg-size-exceeded: true. The sender honours the standard
HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment settings of .NET.
Outcomes, retries and where failures show
A 2xx answer is success. Anything else is a delivery failure: 500 0x80040224, message
Endpoint unavailable: webhook '{name}' {reason}. where the reason is returned {status} {reason phrase},
did not answer within {n} s or could not be reached ({error}). The message never contains the query string, a header or the auth
value.
- Timeout per attempt:
Pipeline:WebhookTimeout, a time span, default00:01:00, must be positive and at most24.20:31:23.647(int.MaxValuemilliseconds). - One immediate extra attempt only for 502, 503 and 504.
- Retryable: any 5xx (after that extra attempt), a timeout and a connection or TLS error. Final: 3xx (no redirect is followed) and 4xx.
| Step | Failure | Result |
|---|---|---|
| synchronous | any | the request fails with the fault and the transaction rolls back; the POST cannot be recalled, so the receiver may already have acted |
| asynchronous | retryable | the job is retried with the async queue's backoff (Pipeline:AsyncMaxRetries, default 3), then ends 3/31 |
| asynchronous | final | the job ends 3/31 at once |
For asynchronous steps the failure shows in the system job: asyncoperation.message holds the fault message and owningextensionid
is the step:
GET /api/data/v9.2/asyncoperations?$filter=_owningextensionid_value eq <step id>&$select=statecode,statuscode,message
asyncautodelete on a step deletes the succeeded system job (plug-in and webhook steps). No plugintracelog rows are written for
webhook steps. The async job runs as the owner of its row, as for plug-ins (see Plug-ins).
Configuration
| Key | Default | Meaning |
|---|---|---|
Pipeline:WebhookTimeout |
00:01:00 |
time to wait for each attempt; a positive TimeSpan of at most 24.20:31:23.647 |
Pipeline:WebhookAllowAnyPort |
false |
allow urls on ports other than 80 and 443 (tests, development, on-premises receivers) |
Known limits
- Contracts other than 8 and
IServiceEndpointNotificationServiceare not supported. Service Bus queue and topic and Event Hub contracts: #84. - Webhooks and their steps cannot be added to a solution (component 95), and a solution export never contains them, and a solution
import that holds a
ServiceEndpointsnode or a step withEventHandlerTypeCode4618 is refused: register them per organization. #85. x-ms-dynamics-organizationis the organization name, not Dataverse's domain name.- The
authvalueis stored unencrypted inmeta.serviceendpoint_secretof the organization database, outside the table so that no read path returns it: protect access to the database and its backups. #86. - The automatic
AsynchronousStepPrimaryNameimage of an asynchronous step is not added. #87. - The sync failure code
0x80040224is not verified against Dataverse (Learn names the error dialog, not the code), and neither is the retry of async jobs beyond the one extra attempt (see the checklist). TriggerServiceEndpointCheckis not served.- Collection-valued navigation properties are not modelled:
serviceendpoint_sdkmessageprocessingstepis not in$metadata, and$expandand the navigation path return 400. The single-valuedeventhandler_serviceendpointof a step works. #88. - The web admin Plug-ins page lists plug-in steps only; use
xrmd webhookandxrmd plugin list.
The manual acceptance checklist is
docs/superpowers/acceptance/webhook-checklist.md.