What was happening
I had a Power Query pulling data from a REST API. Tested the endpoint in Postman β 200 OK, data came back perfectly. But Excel kept throwing this:
DataSource.Error: Web.Contents failed to get contents from 'https://api.example.com/data' (403): Forbidden.
Expression.Error: Access to the resource is forbidden
The endpoint wasn't down. The API key was valid. Postman worked. Power Query refused.
After digging in, I found three completely separate root causes that all produce the same 403 β and the order you eliminate them matters.
Root causes (in order of likelihood)
- Authentication headers not being sent β Power Query silently drops custom headers in some configurations
- Formula Firewall blocking cross-source queries β Power Query's privacy settings treat combining two data sources as a security violation
- Credentials stored incorrectly at the data source level β Excel caches bad credentials and keeps sending them
- API-side IP or referrer restrictions β less common, but possible if the API key is scoped to specific origins
Step 1 β Confirm headers are actually being sent
Nine times out of ten, this is the culprit. You're passing an API key in Web.Contents headers, but Power Query silently ignores them when it falls back to its built-in credential system.
Open Advanced Editor and check your M code. This pattern looks right but breaks in practice:
// Headers may get dropped without any warning
let
Source = Web.Contents(
"https://api.example.com/data",
[
Headers = [
#"Authorization" = "Bearer YOUR_TOKEN",
#"Content-Type" = "application/json"
]
]
),
Result = Json.Document(Source)
in
Result
Split the URL into a base and a RelativePath. This stops Power Query from caching the full URL with credentials baked in β which is what confuses the auth system:
let
BaseUrl = "https://api.example.com",
Source = Web.Contents(
BaseUrl,
[
RelativePath = "data",
Headers = [
#"Authorization" = "Bearer YOUR_TOKEN",
#"x-api-key" = "YOUR_API_KEY"
],
ManualStatusHandling = {403, 401}
]
),
ResponseCode = Value.Metadata(Source)[Response.Status],
Result = if ResponseCode = 200 then Json.Document(Source)
else error "HTTP " & Text.From(ResponseCode) & " β check auth headers"
in
Result
ManualStatusHandling is the key addition here. Instead of Power Query converting the 403 into an opaque crash, you get the actual HTTP status code back β something you can inspect and act on.
Step 2 β Clear and reset data source credentials
Excel caches credentials per data source URL. Connect anonymously once β or with a wrong token β and it keeps sending those stale credentials on every refresh, even after you fix the M code.
- In Excel, go to Data β Get Data β Data Source Settings
- Find the URL in the list (often just the base domain, e.g.
api.example.com) - Click Clear Permissions β not Edit, clear it entirely
- Close and reopen the query
- When prompted, pick the right credential type: Anonymous if your API key travels in M headers, or Web API if the API uses standard key-based auth
This trips up a lot of people. Power Query runs two separate auth layers: the headers in your M code, and the credential store. When they conflict, the credential store wins. Your custom headers may never even reach the server.
Step 3 β Fix the Formula Firewall (Privacy Levels)
Mixing your web API with any other data source β a local Excel table, a SharePoint list, a database β triggers the Formula Firewall. Power Query treats this combination as a security risk and blocks it, sometimes producing the exact same 403 error.
Other times you'll see this more explicit message instead:
Formula.Firewall: Query 'Query1' (step 'Source') references other queries or steps,
so it may not directly access a data source. Please rebuild this data combination.
Set privacy levels explicitly to fix it:
- Go to File β Options β Trust Center β Trust Center Settings β Privacy
- Or inside the query editor: File β Options and Settings β Query Options β Privacy
- Set the web source privacy level to Public (or Organizational on a corporate network)
- Set local file sources to Private or Organizational
During debugging, you can shortcut this entirely:
- Query Options β Privacy β Always ignore Privacy Level settings
Don't leave that enabled in production if you're mixing sensitive sources. But it's the fastest way to confirm the Firewall is actually the problem before you spend time tuning privacy levels.
Step 4 β Use query parameters instead of URL string concatenation
Some APIs reject API keys embedded directly in the URL string. They expect the key as a proper query parameter β and Power Query handles these two constructions very differently under the hood. Use the Query option:
let
Source = Web.Contents(
"https://api.example.com",
[
RelativePath = "v1/data",
Query = [
api_key = "YOUR_API_KEY",
format = "json"
],
Headers = [
#"Accept" = "application/json"
]
]
),
Result = Json.Document(Source)
in
Result
Bonus: Power Query treats this as a static source rather than a dynamic one, so you won't get extra security prompts on every scheduled refresh.
Step 5 β Check IP restrictions on the API side
Still getting 403? The problem may not be in Excel at all. Enterprise APIs often restrict access by IP address. When Power Query refreshes via Power BI Service or SharePoint Online, the request comes from Microsoft's Azure datacenter β not your laptop. The API sees an unknown IP and blocks it.
Test this by running the same query from a different network, or ask the API provider to pull their access logs and see what IP was rejected. If that confirms it, you'll need to either whitelist Microsoft's datacenter IP ranges or route refreshes through an on-premises data gateway that sends from an allowed IP.
Verification
Once you've applied a fix, make sure it actually held:
- Temporarily add
ManualStatusHandling = {403}and surfaceResponse.Statusβ you should see 200, not 403 - Remove the status check and do a full refresh β no error
- For scheduled refreshes in Power BI or SharePoint, trigger a manual refresh from the service and check the refresh history
- Reopen Data Source Settings and confirm the correct credential type is saved against your URL
Quick reference
- Headers dropped silently β use
RelativePath+ManualStatusHandling - Cached wrong credentials β Data Source Settings β Clear Permissions
- Formula Firewall blocking β set Privacy Levels or temporarily disable the Firewall
- API key in URL string β use
Query = [...]option instead - IP restriction β whitelist Microsoft datacenter IPs or use an on-premises gateway
Lessons learned
Here's what makes this error so aggravating: the API isn't broken. Postman proves it. The 403 lives entirely inside Power Query's handling of auth, privacy settings, and credential caching β none of which is visible in the M code itself.
Going in sequence β headers first, then credential cache, then Formula Firewall β finds the cause almost every time. And ManualStatusHandling is genuinely underused; it's the difference between "Access to the resource is forbidden" and an actual HTTP status code you can diagnose.

