# Guide to Azure Logic Apps Webhooks: Features and Best Practices

Azure Logic Apps is Microsoft's workflow integration service, and a common way for a workflow to notify your systems is to call them over HTTP. An approval completes, a file lands in storage, a scheduled check finds something, and the workflow's HTTP action posts the result to your endpoint. That call is a webhook in all but name, and receiving it reliably means dealing with Logic Apps' authentication options, retry policy and asynchronous request pattern.

This guide covers what a Logic Apps HTTP call contains, how to authenticate it, how retries and the 202 polling pattern work, and the best practices for receiving Logic Apps calls in production.

## What are Azure Logic Apps webhooks?

Logic Apps has no webhook subscription system of its own. Outbound calls come from the built-in **HTTP** action (or HTTP trigger), which Microsoft describes as the way "your workflow can send outbound calls to other services and systems". The workflow author sets every part of the request: the method, the URI, the headers, the body and the authentication. The body is usually an expression over earlier steps' outputs.

That has two consequences. First, Logic Apps adds no event type, envelope or delivery ID, so anything your receiver needs to route or deduplicate on has to be put there by the workflow. Second, Logic Apps doesn't sign requests. Authentication comes from the HTTP action's **Authentication** setting:

| Authentication type | What your endpoint receives |
| --- | --- |
| Basic | `Authorization: Basic <base64(username:password)>` |
| Raw | An `Authorization` header with a value the author writes, sent as is |
| Client certificate | An X.509 client certificate presented during the TLS handshake |
| Active Directory OAuth | A Microsoft Entra ID access token |
| Managed identity | A Microsoft Entra ID access token for the logic app's identity |

Basic authentication is the one a shared-secret receiver can check, and it's what Hookdeck's Azure Logic Apps source type verifies.

## Azure Logic Apps webhook features

| Feature | Details |
| --- | --- |
| Sender | The HTTP action or HTTP trigger in a Consumption or Standard workflow |
| Method | The author's choice |
| Body | The author's choice: JSON, text, form-urlencoded or multipart, with the `Content-Type` set in the action's headers |
| Event types | None. Add your own field if you need to route |
| Signature | None |
| Authentication | Basic, Raw, Client certificate, Active Directory OAuth or Managed identity |
| Dropped headers | `Accept-*` (except `Accept-version`), `Allow`, most `Content-*`, `Cookie`, `Expires`, `Host`, `Last-Modified`, `Origin`, `Set-Cookie`, `Transfer-Encoding` |
| Retries | Default policy: up to 4 retries at exponential intervals, on a timeout or a `408`, `429` or `5xx` response |
| Asynchronous pattern | On by default for the HTTP action in stateful workflows. A `202` makes the action poll for a result |
| Outbound timeout | 120 seconds (multitenant), 225 seconds by default (single-tenant) |
| Source IPs | Per region, listed on Microsoft's "Limits and configuration for Azure Logic Apps" page |

## Common events

Logic Apps has no event catalogue. A request goes out whenever a workflow reaches its HTTP action, so the events are whatever your workflows are built to report, such as an approval completing or a file landing in storage.

Because Logic Apps adds no event type or ID, decide on a small envelope and use it in every workflow that calls your endpoint:

```json
{
  "type": "invoice.approved",
  "run_id": "@{workflow().run.name}",
  "data": { "invoice_id": "INV-1042" }
}

```

`type` and `run_id` here are your own convention. The run name identifies one workflow run. A run can call your endpoint more than once, so combine it with `type` or a business key to deduplicate when the HTTP action's retry policy sends the same request again.

## Setting up Azure Logic Apps webhooks

1. In the workflow designer, add the **HTTP** action after the step that produces your data.
2. Set **Method** to `POST` and **URI** to your endpoint URL.
3. Under **Headers**, add `Content-Type: application/json`, and any header your receiver needs to route or deduplicate on, for example `x-workflow-run-id` with the value `@{workflow().run.name}`.
4. Set **Body** to the JSON you want to send. Include a field that names what happened, such as `"type": "invoice.approved"`, because Logic Apps won't add one.
5. Under **Authentication**, choose **Basic** and enter a username and password. Store them in secured parameters or Key Vault rather than in the workflow definition.

In the workflow's JSON definition, the action looks like this:

```json
"HTTP": {
  "type": "Http",
  "inputs": {
    "method": "POST",
    "uri": "@parameters('endpointUrl')",
    "headers": {
      "Content-Type": "application/json",
      "x-workflow-run-id": "@{workflow().run.name}"
    },
    "body": {
      "type": "invoice.approved",
      "invoiceId": "@{triggerBody()?['id']}"
    },
    "authentication": {
      "type": "Basic",
      "username": "@parameters('webhookUsername')",
      "password": "@parameters('webhookPassword')"
    }
  }
}

```

Don't put the credential in a header that Logic Apps removes. Microsoft says it drops the headers listed above "without showing any warning or error", so a custom header named, for example, `Origin` never arrives.

## Securing Azure Logic Apps webhooks

Decode the `Authorization` header, split it at the first colon (passwords can contain colons), and compare the username and password with your stored values in constant time. Return `401` if either is wrong.

```javascript
const crypto = require('crypto');
const express = require('express');

function safeEqual(a, b) {
  const left = Buffer.from(a, 'utf8');
  const right = Buffer.from(b, 'utf8');
  return left.length === right.length && crypto.timingSafeEqual(left, right);
}

function verifyBasicAuth(authorization, username, password) {
  const match = /^Basic ([A-Za-z0-9+/=]+)$/.exec(authorization || '');
  if (!match) return false;
  const decoded = Buffer.from(match[1], 'base64').toString('utf8');
  const separator = decoded.indexOf(':');
  if (separator === -1) return false;
  const userOk = safeEqual(decoded.slice(0, separator), username);
  const passOk = safeEqual(decoded.slice(separator + 1), password);
  return userOk && passOk;
}

const app = express();

app.post('/webhooks/logic-apps', express.raw({ type: '*/*' }), (req, res) => {
  const { LOGIC_APPS_USERNAME, LOGIC_APPS_PASSWORD } = process.env;
  if (!LOGIC_APPS_USERNAME || !LOGIC_APPS_PASSWORD) return res.sendStatus(500); // fail closed

  if (!verifyBasicAuth(req.get('authorization'), LOGIC_APPS_USERNAME, LOGIC_APPS_PASSWORD)) {
    return res.set('WWW-Authenticate', 'Basic').sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on req.get('x-workflow-run-id'), then process asynchronously.
  res.sendStatus(200); // 200, not 202
});

```

The same check in Python:

```python
import base64
import binascii
import hmac

def verify_basic_auth(authorization, username: str, password: str) -> bool:
    if not authorization or not authorization.startswith("Basic "):
        return False
    try:
        decoded = base64.b64decode(authorization[6:], validate=True).decode("utf-8")
    except (binascii.Error, UnicodeDecodeError):
        return False
    user, sep, pwd = decoded.partition(":")
    if not sep:
        return False
    user_ok = hmac.compare_digest(user.encode(), username.encode())
    pass_ok = hmac.compare_digest(pwd.encode(), password.encode())
    return user_ok and pass_ok

```

Basic credentials prove the caller knows the secret. They don't protect the body from tampering and they don't stop a captured request being replayed, so keep the endpoint on HTTPS.

If you use **Raw** authentication instead, Logic Apps sends the value you wrote as the `Authorization` header. Hookdeck's Logic Apps source type can verify it only if that value is itself a `Basic` header. Entra ID tokens and client certificates need validating as a JWT or at your TLS terminator, not with a string comparison.

> **Make Azure Logic Apps calls production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the Basic credentials on every Logic Apps call, answers immediately with a `200` so the HTTP action never starts polling, and durably queues each request before your service sees it.

## Azure Logic Apps webhook limitations and pain points

### A 202 starts a polling loop

**The Problem:** Your endpoint returns `202 Accepted`, and the workflow keeps calling back or never finishes the action.

**Why It Happens:** In stateful workflows the HTTP action follows the asynchronous operation pattern by default. A `202` tells it the work isn't finished, so it polls the URL in the `Location` header until it gets a non-202 response. If there's no `Location` header, Microsoft suggests turning off the action's **Asynchronous pattern** setting.

**Workarounds:**

* Answer `200` once you've stored the request, even if processing happens later.
* If you control the workflow, turn off **Asynchronous pattern** under the action's **Networking** settings.

**How Hookdeck Can Help:** Hookdeck answers Logic Apps with a `200` as soon as it has stored the request, then delivers it to your service, so the action completes straight away.

### Retries depend on the status code

**The Problem:** The same call reaches your endpoint several times, or a failed call is never retried.

**Why It Happens:** The default retry policy resends a request that times out or returns `408`, `429` or `5xx`, up to 4 times, at intervals that scale by 7.5 seconds and are capped between 5 and 45 seconds. Other failures, such as a `401` or `400`, aren't retried. Authors can change the policy per action, so you can't assume the default.

**Workarounds:**

* Return `5xx` only for failures you want retried, and `4xx` for requests that will never succeed.
* Send `Retry-After` on a `429` to space retries out; Logic Apps honours it.
* Deduplicate on a run ID header so a retried call doesn't run twice.

**How Hookdeck Can Help:** Hookdeck accepts the call on the first attempt and runs its own retries against your service, with a schedule you configure and the ability to replay any request.

### There is no event type or delivery ID

**The Problem:** Your receiver can't tell one workflow's calls from another's, or a retry from a new call.

**Why It Happens:** Logic Apps sends exactly what the author configured and nothing else.

**Workarounds:**

* Put a `type` field (or a path per workflow) in every HTTP action.
* Send `@{workflow().run.name}` in a header and combine it with a business key from the body.

**How Hookdeck Can Help:** Hookdeck filters can route on any header or body field you add, and its deduplication can drop repeats keyed on your run ID header.

### Silent header removal

**The Problem:** A header you configured never arrives, and your endpoint rejects the call.

**Why It Happens:** Logic Apps removes `Accept-*` (except `Accept-version`), `Allow`, most `Content-*`, `Cookie`, `Expires`, `Host`, `Last-Modified`, `Origin`, `Set-Cookie` and `Transfer-Encoding` without a warning. `Content-Type`, `Content-Disposition` and `Content-Encoding` survive on `POST` and `PUT` but are dropped on `GET`.

**Workarounds:**

* Use the **Authentication** setting, or a custom `x-` header, for credentials.
* Use `POST` when you send a body.

**How Hookdeck Can Help:** Hookdeck records every header that actually arrived on each request, so you can see what Logic Apps sent rather than what the designer shows.

## Best practices

### Use Basic authentication with a long random password

Generate the password with a tool such as `openssl rand -hex 32`, keep it in a secured parameter or Key Vault, and compare it in constant time on the receiving side.

### Send a type and a run ID

Add a `type` field to the body and `x-workflow-run-id: @{workflow().run.name}` to the headers. Your receiver can then route without guessing and deduplicate retries.

### Answer 200 quickly

Return `200` as soon as you've stored the request. The HTTP action fails if your server doesn't respond within the outbound timeout (120 seconds in multitenant Logic Apps, 225 seconds by default in single-tenant), and a `202` starts the polling pattern. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Treat IP filtering as an extra layer

Microsoft publishes the outbound IP addresses for each region. They're shared by every logic app in the region, so use them alongside credentials, not instead of them.

### Make handlers idempotent

Retries resend the same request. Store the run ID (or your own business key) before acting on a call. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

A Logic Apps webhook is an HTTP action that you design: Logic Apps signs nothing and adds no event type or delivery ID. Authenticate it with Basic credentials compared in constant time, put a type and a run ID into each call, answer `200` rather than `202`, and expect the default policy to retry timeouts, `408`, `429` and `5xx` responses up to four times.

[Hookdeck Event Gateway](https://hookdeck.com) checks the credentials, answers Logic Apps immediately, and holds every call until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and receive Azure Logic Apps calls reliably in minutes.