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:
{
"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
- In the workflow designer, add the HTTP action after the step that produces your data.
- Set Method to
POSTand URI to your endpoint URL. - Under Headers, add
Content-Type: application/json, and any header your receiver needs to route or deduplicate on, for examplex-workflow-run-idwith the value@{workflow().run.name}. - 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. - 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:
"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.
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:
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 checks the Basic credentials on every Logic Apps call, answers immediately with a
200so 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
200once 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
5xxonly for failures you want retried, and4xxfor requests that will never succeed. - Send
Retry-Afteron a429to 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
typefield (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
POSTwhen 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.
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.
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 checks the credentials, answers Logic Apps immediately, and holds every call until your service has processed it.
Get started with Hookdeck for free and receive Azure Logic Apps calls reliably in minutes.