I think I found the solution myself. I used Azure Functions to create proxy that gateways the webhook.
QBO Webhook that goes to a shorten URL created by Azure
|
Azure Function : HTTP Trigger action
passing the request to the real URL issued by Power Automate: When-HTTP-Received trigger
|
Receive in the PAutomate flow to run
Tech Overview:
HTTP Trigger action in Azure Function app with a code that passes a short URL to the real long URL
Detail:
1. Input the real long URL to your new Azure Functions app.
After a new Azure Functions app is created, go to Azure Portal's Environment Variables in the right blade to input this parameter:
FLOW_URL = the entire Power Automate's trigger URL issued by "When a HTTP request is received" (the new long URL, copy/paste exactly, including api-version=...&sig=...). Update this URL whenever in the future the URL is renewed.
FORWARD_SECRET = (optional) a random long string you’ll use to validate incoming requests or to add to the forwarded request. (You can ignore in the test phase)
2. Save and let settings restart the app.
Why: putting the long Flow URL in app settings keeps secrets out of code and makes it easy to rotate if MS rotates the Flow URL.
3. Create the HTTP function
Add "HTTP Trigger" function in Functions tab in your app's overview page & name what you like.
Set Authorization level to "Function" - This enforces a function key appended as ?code=... in the public URL. If you absolutely cannot include the key in Intuit's webhook URL, choose Anonymous, but that’s less secure.
Create the function.
4. Paste the proxy code (in way of Node.js)
Go to the function → Code + Test → replace index.js (or default) with the code below. This code:
- reads the raw request body (preserves JSON or raw bytes),
- forwards the request (POST) to FLOW_URL (from app settings),
- forwards important headers (except host),
- adds an optional x-intuit-forward-secret header,
- observes a timeout and returns the Flow response status/body to the caller.
// index.js Azure Function (Node 18+)
module.exports = async function (context, req) {
context.log('Intuit webhook proxy received request');
const flowUrl = process.env.FLOW_URL;
const forwardSecret = process.env.FORWARD_SECRET || '';
if (!flowUrl) {
context.log.error('FLOW_URL not configured in App Settings');
context.res = { status: 500, body: 'Server misconfigured' };
return;
}
// Use rawBody when available to preserve original payload exactly
const body = (req && req.rawBody) ? req.rawBody : (req.body ? JSON.stringify(req.body) : '');
// Build headers to forward
const outgoingHeaders = {};
// copy incoming headers but skip host and content-length (set by fetch)
for (const h of Object.keys(req.headers || {})) {
if (h.toLowerCase() === 'host' || h.toLowerCase() === 'content-length') continue;
outgoingHeaders[h] = req.headers[h];
}
// Ensure content-type is present if incoming had JSON
if (!outgoingHeaders['content-type'] && req.headers && req.headers['content-type']) {
outgoingHeaders['content-type'] = req.headers['content-type'];
}
// Inject a secret header for Flow-side validation (optional)
if (forwardSecret) {
outgoingHeaders['x-intuit-forward-secret'] = forwardSecret;
}
// Add a small custom header so Flow knows this request came via the proxy
outgoingHeaders['x-forwarded-by'] = 'azure-function-intuit-proxy';
// Use fetch (Node 18+ - Azure Functions supports it). Apply a timeout using AbortController.
const controller = new AbortController();
const timeoutMs = 15000; // 15s
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(flowUrl, {
method: 'POST',
headers: outgoingHeaders,
body: body,
signal: controller.signal
});
clearTimeout(timeout);
const respText = await res.text();
context.log(`Forwarded to Flow, received status ${res.status}`);
// Return the same status and body back to Intuit
context.res = {
status: res.status,
headers: {
// propagate Flow's content-type if present
'content-type': res.headers.get('content-type') || 'text/plain'
},
body: respText
};
} catch (err) {
clearTimeout(timeout);
context.log.error('Error forwarding to Flow:', err.message || err);
// If the fetch was aborted due to timeout, return 504
const status = err.name === 'AbortError' ? 504 : 502;
context.res = {
status: status,
body: `Proxy error: ${err.message || 'unknown error'}`
};
}
};
Save the file.
If your runtime does not support [fetch], you can use the [node-fetch] package via [package.json]. The in-portal editor supports a simple JavaScript function as shown.
5. Get & Use the function URL for QBO Webhook
Hit Get function URL at the top pane of index.js editor → copy the default key url (it will look like https://<app>.azurewebsites.net/api/<YourAzureFuncAppName>?code=<FUNCTION_KEY>).
Paste the URL to your QBO's app's webhook destination.
6. Test.
You can confirm if that worked successfully with checking the flow trigger action's header. You'll see the newly-added "X-Forwarded-By: <YourAzureFuncAppName>" and other parameters.
---
I also tried Azure Front Door earlier, but for the minimum cost (practically free) & simple code management I chose Azure Functions. Although I needed a bit of code writing (works with JS/C#/Java etc) I found this way efficient and safe.
I hope this will be helpful for anyone with the same issue!