n8n refreshes your token only when the API says 401
If the service you connect to answers an expired token with 403, 400, or a 200 that carries the error in the body, n8n never notices the token died and never refreshes it. There is a setting that fixes the 403 case, and the built-in credentials do not show it to you.
The symptom, which sounds like nothing
It worked yesterday. Today the workflow fails with something about credentials. You open the credential, reconnect it, everything runs. Tomorrow morning it fails again.
Somebody in the n8n community described the loop exactly:
The credential would work fine, and everything would be set up correctly using Client ID : Client Secret. I would come back to do more work the next day, and end up troubleshooting for an hour or more trying to understand why it was saying my credentials were expired. Refreshing the credential secret would work temporarily, but then would go right back to failing the next day.
- a user, Credentials not auto-refreshing, n8n Community, 31 August 2026
Asked what the API actually returned, the same person answered: "if I recall, it was giving me a 403 or a 400 after 24 hours had passed."
That is the whole bug, and it is not really a bug. It is a design decision with a number in it.
What n8n checks before deciding to refresh
One status code, compared for exact equality.
When a request through an OAuth2 credential fails, n8n compares the response status against a value that resolves, in order, from the credential, then from the node's options, then to a default of 401. If the numbers match, it refreshes the token and retries. If they do not, the failure is passed on as an ordinary error and the token is left alone, still expired, ready to fail the same way on the next run.
Three consequences follow directly from "exact equality against one number":
- 403 does not trigger a refresh. Neither does 400.
- A 200 with an error inside the body does not reach the retry logic at all, because as far as the HTTP layer is concerned nothing failed.
- Configuring a different code replaces 401 rather than adding to it.
That last one is not a guess. The test is named "should NOT retry on 401 when credential sets tokenExpiredStatusCode to 403 (isN8nRequest path)". You are choosing which single code means "expired", not building a list.
Which APIs do not answer 401?
More than you would guess, and several of them are ones a small business automation actually touches.
| Service | What it returns for an expired or rejected token |
|---|---|
| Google Drive, Gmail | 401. "These errors mean the request doesn't contain a valid access token", with the instruction to "refresh the access token using the long-lived refresh token" |
| Microsoft Graph | 401 for missing or invalid auth, and a documented 403 when conditional access policies apply, returned as "HTTP 403; Forbidden error=insufficient_claims" |
| HubSpot | 401 "when the authentication provided is invalid", 403 when the token lacks the scope |
| Shopify | 401 for credentials, 403 for scopes, and 402 when the shop is frozen for non-payment |
| Databricks | 403, according to n8n's own source comment |
| Adobe Marketo | HTTP 200, with 602 Access token expired in the body, as quoted by a reporter from Adobe's error-code documentation |
Google is explicit that this split is deliberate, and its API design guide states the rule that most well-behaved APIs follow: a permission problem "must error with PERMISSION_DENIED (HTTP 403)". So 403 is supposed to mean "you, but not allowed", and an expired token is supposed to be 401.
The trouble is that plenty of real services do not follow it. In the n8n issue on this subject, users report the same 403 from Microsoft Graph and from Power BI, and one reports that a Zoho API "doesn't return an 401" either. In a separate forum thread somebody reports a 403 or 400 from a PSA tool. Those are user reports rather than vendor documentation, which is exactly the position you are in when it happens to you: the only evidence is your own failed run.
The quick test. Run the failing step and look at the status code, not the message. If it is 401, this article is not your problem. If it is 403, 400, or a 200 whose body contains an error, it probably is.
Is there a setting for this?
There is, and it has a name: tokenExpiredStatusCode. It arrived in a pull
request titled "feat(core): Add configurable HTTP status code for OAuth2 token
refresh", whose summary says the quiet part plainly:
Added support for configurable HTTP status codes to trigger OAuth2 token refresh. Previously, only 401 was checked. This PR allows users to specify custom status codes (e.g., 403) that indicate token expiration for APIs that return non-standard codes.
- PR #26641, n8n on GitHub, opened 5 March 2026, merged 6 March 2026
It shipped in n8n 2.12.0 on 9 March 2026, and it works.
The request for it is older than that by almost exactly five years. In March 2021 a user asked on the forum how to reach the setting, explaining that their API "returns 403, not 401" and that they had patched their own copy of n8n to check both. A member of the n8n team replied:
Ahh, gotcha. I understand now. Good question. Currently, it's not possible to set
tokenExpiredStatusCodevia the UI. However, it should not be difficult to add. I just added it to my to-do list.
- a responder carrying the "n8n Team" title, tokenExpiredStatusCode not expired in the ui anywhere?, n8n Community, 30 March 2021
So why can you not find it in your credential?
Because on almost every built-in credential, it is deliberately removed.
The field is declared on the base OAuth2 credential, with a description that tells
you precisely what it is for: "HTTP status code that indicates the token has
expired. Some APIs return 403 instead of 401." And it carries a flag,
doNotInherit, which is honoured by the code that merges a parent credential's
fields into a child one. Any property with that flag is skipped.
The built-in OAuth2 credentials all reach that base credential, most of them directly and the rest through an intermediate one: seventy-two extend it by name, and another three dozen inherit through the Google, Microsoft, Atlassian and Facebook bases. So the field exists, and it does not reach the Google credential, the HubSpot credential, the Microsoft credential, or any of the others.
You do not have to take an outsider's word for the consequence, because n8n's own source spells it out. In the Databricks credential, above a re-declared copy of the field:
Re-declared because the base
oAuth2Apifield isdoNotInherit, so it never reaches the decrypted credential. Without it the value is always undefined and token refresh is hardcoded to 401 - Databricks returns 403 when tokens expire, so the default must be 403.
- comment in
DatabricksOAuth2Api.credentials.ts, n8n-io/n8n, read 7 September 2026
Databricks is the only extending credential that carries that workaround. Of the four hundred-odd credential files in the repository, exactly two mention the setting at all: the base itself, and Databricks.
Getting that one line into Databricks took six community pull requests, of which two were merged, which is worth knowing if your plan is to open one for your own service.
Where is the field actually visible?
On the generic credential. doNotInherit blocks inheritance downward; it does not
hide the field on the credential that declares it.
So in the HTTP Request node, choosing a generic OAuth2 credential rather than a predefined one gives you a Token Expired Status Code box you can set to 403. That is the actual fix available today, and it is the reason this is a small job rather than a fork of n8n.
The cost is that you give up the predefined credential's convenience and configure the OAuth endpoints yourself, and that you are choosing 403 instead of 401 rather than in addition to it.
Somebody did propose handling both codes, for Microsoft Teams specifically, arguing that "Microsoft Graph API returns both 401 and 403 for expired tokens depending on the endpoint and scenario". That pull request was not merged, and the comparison in the shipped code is still against a single value.
Why does the documentation not mention any of this?
Because it does not. Searching the documentation repository for the setting's name
returns nothing. Neither does searching for "token expired status", or for
doNotInherit, or for the pull request number in the changelog.
The comparison that makes this stark is one page over. The documentation for the HTTP Request node's credentials lists the fields of the generic OAuth2 credential, and it includes Ignore SSL Issues, which is declared in the same source file immediately above the one that is missing. Three separate grant-type sections list the neighbouring field. None of them lists this one.
The pull request itself predicted this. Its own merge checklist has four boxes, two of them ticked, and one of the empty ones reads "Docs updated or follow-up ticket created". Six months later that box is still an accurate description of the state of the documentation.
It is the same arrangement as a workflow that is saved but not published. The behaviour is written down in the code and nowhere the user will look, so the usual way to learn it is to lose a day to it first.
What does n8n say when people report it?
That it is expected. Twice, from the same member of the n8n organisation, almost ten months apart:
At the moment we don't support oauth connections for services that return a 403 instead of a 401 which is typically a more standard response code. We do have a request open to change this but it has not been implemented yet, For now I am going to mark this as closed as while it is annoying it isn't strictly a bug.
- a member of the n8n organisation, issue #17450, 19 August 2025
this is expected at the moment and is not something we treat as a bug although I do agree we should change this and we have the feature request ticket opened to implement this in the future.
- the same n8n team member, issue #32423, 16 June 2026
Users pushed back on the first one, and the sharpest reply is worth quoting because it names the actual design question rather than complaining:
n8n (the client) is capable of knowing the expiry status of the token (by either reading the token itself in the case of structured/transparent token, or by introspection at the time of token creation in the case of opaque token) but instead makes the API request blind expecting every API in the world to respect a single status code.
- a commenter, issue #17450, 6 October 2025
There is a hint that this is understood internally. Answering the second issue, the same n8n member mentioned "a slightly different approach in mind that remove the need for the http status check and make it more reliable with a background refresh service". They added "I don't know what the status is on that". Treat it as a direction, not a date.
What if the API answers 200?
If your service returns HTTP 200 with the error inside the body, none of the above helps, because nothing ever fails.
A user hit this with Adobe Marketo, which answers an expired token with a 200 and an error code in the payload, and summarised the position:
Token refresh is reactive only: triggered when the HTTP response status matches
tokenExpiredStatusCode(default401). [...] n8n recently merged PR #26641 [...] this helps APIs that return403, but does not help Marketo, which returns200with response-level error codes like602.
- a user, Generic OAuth2 authentication - enforce token refresh based on expiration value, n8n Community, 12 June 2026
The corresponding issue was opened and closed inside half an hour, labelled as working as expected. For this class of API, the only reliable approach is to stop waiting for a failure and refresh on a schedule instead, before the token can expire.
One more thing worth knowing about waiting for a fix
The most-resolved thread on this subject was not resolved by n8n. A user reported that Inoreader returned 403 for expired tokens and that their workflow therefore died permanently. Nearly five months later a developer from Inoreader turned up in the thread. The root cause was on their side, they had relaxed it, and they were now "sending 401 instead of 403 in this specific case".
The vendor changed its API to suit the automation tool. That is a good outcome and not a plan you can build on.
What to do when this is your problem
- Look at the status code, not the error text. 403, 400 or a 200 with an error in the body puts you here. 401 does not.
- If it is 403 and you can use the HTTP Request node with a generic OAuth2 credential, set Token Expired Status Code to 403. This is the smallest fix that exists.
- Remember it is a replacement, not an addition. After setting 403, a genuine 401 will no longer refresh. For services that use both, choose the one you actually see.
- If the API answers 200 with the error in the body, stop trying to react. Refresh the token on a schedule with a margin, or fetch a token at the start of each run and pass it as a header.
- Do not leave the reconnect-every-morning workaround in place. It works and it hides the problem, which means the failure resurfaces on a day when nobody is watching.
- Write down which service returned what. The evidence disappears from the execution log on the next retention cycle, and it is the only thing that distinguishes this from a dozen other credential problems.
When this is a job to hand over
Setting one field on one generic credential is a fifteen-minute change, and that one you should do on your own.
It becomes a job when the failing step is a predefined credential you cannot easily swap, when the API answers 200 and the whole approach has to change from reactive to scheduled, or when several workflows share one credential. That last case is the awkward one, because the fix has to land without disturbing the workflows that are currently fine. It is still a job you can scope by counting: how many credentials, how many workflows behind each, which service answers with what. A Fix is priced off that count, and the n8n work we take usually starts with it.
The shape to carry away is that a credential is not either valid or invalid. It can be dead while still looking connected, and the only reason your platform ever notices is a number it happens to be watching for. When the service picks a different number, nobody finds out until somebody checks the runs.
Sources
- n8n-io/n8n - n8n source, read 7 September 2026. The status code resolved from the credential, then the node options, then a default of 401; the exact-equality comparison at both request paths; the base OAuth2 credential's Token Expired Status Code field with its description and its
doNotInheritflag; the merge helper that skips flagged properties during credential inheritance; the Databricks credential's re-declaration and the comment explaining why; the counts of credential files extending the OAuth2 base; and the test asserting that a credential set to 403 does not retry on 401. - feat(core) - Add configurable HTTP status code for OAuth2 token refresh - n8n on GitHub, opened 5 March 2026, merged 6 March 2026, read 7 September 2026. The summary describing that only 401 was checked before, the release in 2.12.0, and the unticked documentation box in the merge checklist.
- tokenExpiredStatusCode not expired in the ui anywhere? - n8n Community, March 2021, read 7 September 2026. The original request, the user's local patch checking both codes, and the n8n Team reply adding it to a to-do list.
- Credentials not auto-refreshing - n8n Community, opened 31 August 2026, read 7 September 2026. The reconnect-every-day loop and the reporter's recollection of a 403 or 400 after twenty-four hours.
- Generic OAuth2 authentication - enforce token refresh based on expiration value instead of HTTP response code - n8n Community, June 2026, read 7 September 2026. The Marketo case answering 200 with an error code in the body, the reporter's summary of what PR #26641 does and does not fix, and the quoted Adobe error-code documentation.
- Feature Request - Add HTTP 403 support for OAuth2 token refresh in generic OAuth2Api credential - n8n Community, August 2025 to March 2026, read 7 September 2026. The Inoreader case, the twelve-hourly manual refresh used as a workaround, and the Inoreader developer's reply describing the change to return 401 in that case.
- OAuth2 Client Credentials in n8n Cloud does not refresh token after expiry (403 error) - n8n on GitHub, July 2025 onwards, read 7 September 2026. The original report, the n8n member's reply that this is annoying but not strictly a bug, the user replies naming Microsoft Graph, Power BI and Zoho, and the manual-token workaround the thread converges on.
- Generic OAuth2 credentials do not refresh expired tokens when API returns HTTP 200 - n8n on GitHub, 16 June 2026, read 7 September 2026. The issue opened and closed the same day as working as expected, and the n8n member's replies about custom credentials and about a possible background refresh service.
- Databricks OAuth2 - token refresh is hardcoded to 401, cannot handle 403 - n8n on GitHub, April 2026, read 7 September 2026. A third-party description of the inheritance flag stripping the field from every extending credential.
- fix(core, nodes-base) - Fix OAuth2 token refresh for Microsoft Teams (403 + httpRequest path) - n8n on GitHub, February 2026, read 7 September 2026. The proposal to treat both 401 and 403 as token expiry for Microsoft Graph, and its status as not merged.
- Handle API errors - Google Drive API documentation, read 7 September 2026. The 401 response for an invalid or expired access token, the instruction to refresh, and the separate meaning of 403.
- API design guide - errors - Google Cloud API design guide, read 7 September 2026. The rule that a permission problem must return PERMISSION_DENIED with HTTP 403, which is why a well-behaved API reserves 403 for permissions rather than expiry.
- Microsoft Graph error responses and resource types - Microsoft Graph documentation, read 7 September 2026. The status code table, including the 403 returned with an insufficient claims message when conditional access policies apply.
- Error handling - HubSpot API documentation, read 7 September 2026. The 401 for invalid authentication and the 403 for a token without the required scope.
- Admin REST API - Shopify developer documentation, read 7 September 2026. The status and error code table, including 401 for credentials, 403 for scopes and 402 for a frozen shop.
Broken workflow? Fix S — $300, 2 business days, fixed price.
Get my quote in 24hWritten by the Fixmation team.