Skip to main content

Webhook

Use the Webhook trigger when you want a flow to start in response to an HTTP call from outside Business Central – a third-party callback, a custom integration, a Slack bot, anything that can POST JSON. The flow can run fully asynchronously (the caller gets an immediate empty 200) or synchronously up to a Respond to Webhook step (the caller waits for your flow's response).

Typical examples include:

  • Receiving a payment-gateway callback and updating the matching invoice.
  • Letting a partner portal trigger a sales order import.
  • Acting as a SOAP/REST bridge that transforms an inbound payload and forwards it to another system.

What happens when the flow runs

  1. The caller POSTs {activity, secret, payload} to the AutoFlow webhook endpoint.
  2. AutoFlow looks up the registered webhook flow by activity.
  3. It hashes the supplied secret with SHA-256 and compares (in constant time) against the hash on file. Mismatches return HTTP 401 with body Unauthorized; no execution is started and the supplied secret is not logged anywhere.
  4. The flow starts, with the payload exposed as the trigger output Payload (a string containing the JSON body, exactly as the caller sent it).
  5. Depending on the trigger's Response Mode:
    • Instant return: the caller receives the trigger's default response immediately; the flow continues asynchronously via a JobQueueEntry.
    • Wait for Response step: the flow runs synchronously inside the request thread until a Respond to Webhook step yields, the configured timeout fires, or the flow finishes. After yield/timeout, any remaining steps continue asynchronously.
caution

Sleep and other time-based deferral steps must appear after the Respond to Webhook step in the flow. The synchronous response window cannot be paused – a deferral before Respond will surface as a 500 error to the caller.

The endpoint

There is one central endpoint for all webhook flows. It is exposed as an OData V4 unbound function on the codeunit web service mse365AFWebhook:

POST <bc-server>/<instance>/ODataV4/mse365AFWebhook_Invoke?company=<company-name>

The exact host depends on your environment (BC SaaS, on-prem, Cosmo Alpaca container). The Flow card surfaces the resolved URL in its Webhook Call section once the flow is published.

Authentication uses the standard BC web-service mechanism (see Authentication below for the two supported paths and the permissions each needs). The endpoint takes three parameters in the JSON body:

FieldTypeRequiredDescription
activitystringyesIdentifies which flow to invoke. Stored lowercase; matching is case-insensitive.
secretstringyesPer-flow shared secret. Compared against the SHA-256 hash on the trigger configuration.
payloadobjectyesArbitrary JSON delivered to the flow as the trigger output Payload.

The response is wrapped in OData's standard envelope; the value field is a JSON string with three nested fields:

{
"@odata.context": "<server>/ODataV4/$metadata#Edm.String",
"value": "{\"statusCode\":201,\"contentType\":\"application/json\",\"body\":\"{\\\"ok\\\":true}\"}"
}

Parse value to get the statusCode, contentType, and body your flow returned. The outer HTTP layer is always 200 on success; real HTTP non-2xx is reserved for infrastructure errors:

  • 404 when no flow is registered for the supplied activity.
  • 500 when an uncaught exception escapes the runner.

Authentication

The endpoint is a standard Business Central web service, so the caller authenticates with one of BC's standard mechanisms. There are two practical paths, and each needs the right permissions or the call fails after the secret check — the flow still has to read and write BC tables to run.

For an external system, register a Microsoft Entra application and call the endpoint with an OAuth client-credentials token (scope https://api.businesscentral.dynamics.com/.default). Setup:

  1. In your Microsoft Entra tenant, register an app and create a client secret (or certificate).
  2. In Business Central, open Microsoft Entra Applications, add the app by its client ID, grant consent, and set State to Enabled.
  3. On that same card, assign the permission set mse365 AF Integr. (AutoFlow Integration (S2S)) to the app.

That last step is essential: a service-to-service caller has no Business Central user licence, so — unlike a signed-in user — it does not implicitly get access to the AutoFlow application tables. mse365 AF ALL alone is not sufficient for an Entra app; mse365 AF Integr. adds the flow-runtime access (execution state, the Job Queue entry that runs the flow, stored credentials) the licence would otherwise supply. See Permission sets for the rationale.

Web Service Access Key / Basic Auth — legacy

Alternatively, authenticate as a licensed Business Central service user with that user's Web Service Access Key (Basic Auth), as in the sample call below. Because this is a real licensed user, no extra AutoFlow permission set is required beyond what the user already has to run flows. Use this only where service-to-service auth is not available — it ties the integration to a licensed named user and a long-lived key.

Configure the trigger

Open the flow editor, pick When a webhook is received, and complete the configuration card.

Description

State what triggers this flow. Shows up in trigger lists and execution history.

Activity

  • Purpose: The unique key the caller sends. One published flow per activity.
  • Tips: Use a stable, namespaced name like crm.customer.updated or payment-gateway.charge-succeeded. The value is stored lowercase, so myFlow and MyFlow collide as duplicates at publish time.

Secret

  • Purpose: Authenticates the caller. Without a matching secret, the call returns 401 and never starts an Execution.
  • Tips: Click the Secret field (or its assist-edit button) to generate a new 32-character random value. The plaintext is shown once in a dialog; copy it into your caller's secret store immediately. Only the SHA-256 hash is persisted in the flow configuration.
  • A secret is required – the configuration is invalid until one is set.

Response Mode

  • Instant return (default): caller gets the default empty response immediately; the flow runs in the background. Use for fire-and-forget integrations.
  • Wait for Response step: caller waits for the flow to reach a Respond to Webhook step (or for the sync timeout to fire). Use when the caller needs synchronous feedback from the flow.

Sync Timeout (seconds)

  • Purpose: How long the synchronous phase of a Wait for Response call may run before the caller receives the default response and the flow continues asynchronously.
  • Default: 30 seconds. Visible only when Response Mode is Wait for Response.

Default Response Status Code / Content-Type

  • Purpose: What the caller sees when the flow ends without reaching a Respond to Webhook step (timeouts, no Respond at all, Instant return mode).
  • Defaults: 200 and application/json.

Do not log payload

  • Purpose: Omit the inbound payload from the execution log. Use for webhooks that carry sensitive data (PII, payment details).

Responding to the caller

When Response Mode is Wait for Response step, the trigger holds the caller's HTTP request open until the flow signals back. That signal is the Respond to Webhook step, available from the Essentials palette. Add it wherever you want the caller unblocked – usually as early as possible, with the heavy work queued behind it.

In Instant return mode the step is a no-op: the caller already received the default response before the flow started, so any Respond to Webhook step in the flow is ignored when the flow runs.

Typical patterns:

  • Acknowledge first, work later: return {"received": true} immediately, then do the slow downstream work asynchronously.
  • Synchronous validation: run a quick check and return a verdict (e.g. 422 with {"approved": false, "reason": "..."}) to a partner portal.
  • API bridge: forward a transformed payload to another system and return that system's reply as the webhook response.

What happens when Respond to Webhook runs

  1. The runner evaluates the configured Status Code, Content-Type, and Body (with SmartFields and SmartFormulas resolved).
  2. The values are signalled to the webhook endpoint as the wrapper's statusCode, contentType, and body fields.
  3. The runner exits the synchronous loop. Any steps after Respond to Webhook continue asynchronously via a JobQueueEntry – the request is already over from the caller's perspective.

If the flow finishes (or times out at the trigger's Sync Timeout) without ever reaching a Respond to Webhook step, the caller receives the trigger's configured Default Response Status Code and Content-Type with an empty body.

Configure the Respond to Webhook step

Add Respond to Webhook to the flow and fill in the configuration card.

Description

State what response this step delivers – it shows up in the execution log next to the step.

Status Code

  • Purpose: The HTTP status code returned to the caller via the wrapper's statusCode field.
  • Range: 200..599. 1xx codes are not valid as a final HTTP response and are rejected by validation.
  • Tips: Use 200/201 for success, 4xx for caller errors (422 for validation), 5xx for backend problems.

Content-Type

  • Purpose: The MIME type of the body. Returned to the caller via the wrapper's contentType field.
  • Tips: application/json is the most common; use text/plain for diagnostic strings, application/xml for SOAP-style replies.

Body

  • Purpose: The response body. Supports SmartFields and SmartFormulas so you can compose the payload from upstream step outputs.
  • Tips: For JSON, build the body with a SmartFormula like ${\"customerNo\":\"${trigger.Payload.customerNo}\",\"approved\":true} to avoid hand-escaping quotes.

Sample call

curl -X POST \
"https://<bc-host>/<env>/ODataV4/mse365AFWebhook_Invoke?company=CRONUS%20AG" \
-u 'serviceuser:<web service access key>' \
-H 'Content-Type: application/json' \
-d '{
"activity": "crm.customer.updated",
"secret": "<the 32-char value you copied at rotate time>",
"payload": { "customerNo": "C-001", "name": "Acme" }
}'

Response (HTTP 200, body):

{
"@odata.context": "https://.../ODataV4/$metadata#Edm.String",
"value": "{\"statusCode\":200,\"contentType\":\"application/json\",\"body\":\"\"}"
}

The caller parses out statusCode and body from value.

Best practices

  • Use HTTPS only – your secret is in the request body.
  • Rotate on compromise – the Rotate Secret action invalidates the previous secret as soon as you save the configuration.
  • Don't log the payload if it carries personal or financial data.
  • Use distinct activity names per integration – they're effectively your endpoint paths.
  • Respond fast. When using Wait for Response, place Respond to Webhook early in the flow so the caller isn't blocked by slow downstream work; long-running steps go after it.
  • Pick deliberate response status codes. Don't reuse 200 for failure cases – 4xx/5xx make caller-side error handling tractable.
  • Don't echo the secret. It is not exposed as a flow output, but be careful not to include parts of the inbound payload in the response that you wouldn't want the caller to see reflected back.