Generic Oauth2 isn't refreshing

Hi all,

I’m having trouble integrating with Jobber’s API. I have it initially authenticated and I have some functional workflows, but everything breaks in 1 hour when the access_token expires.

Jobber doesn’t require special scopes from my testing to return the refresh token, but for some reason it’s either not getting returned/stored, or n8n is otherwise not handling it.

Any suggestions?

here's my troubleshooting info pasted from the issue I made on GitHub

Here’s their documentation - https://developer.getjobber.com/docs/building_your_app/app_authorization/

I’m able to authorize initially, however after the 1 hour expiration I get the following error

Output
1 item
Authorization failed - please check your credentials
Unsupported content type: text/plain; charset=utf-8
Error details

From HTTP Request
Error code

401

Full message

Unsupported content type: text/plain; charset=utf-8
Request

{ “hidden”: “{\n “query”: “{ quotes(first: 10, filter: { status: converted}) { nodes { id quoteNumber notes(first: 5) { nodes { … on QuoteNote { message } } } } } }”\n}”, “headers”: { “content-type”: “application/json”, “x-jobber-graphql-version”: “2025-04-16”, “accept”: “application/json,text/html,application/xhtml+xml,application/xml,text/;q=0.9, image/;q=0.8, /;q=0.7”, “Authorization”: “hidden” }, “method”: “POST”, “uri”: “https://api.getjobber.com/api/graphql”, “gzip”: true, “rejectUnauthorized”: true, “followRedirect”: true, “resolveWithFullResponse”: true, “sendCredentialsOnCrossOriginRedirect”: false, “followAllRedirects”: true, “timeout”: 300000, “encoding”: null, “json”: false, “useStream”: true }
Other info
Item Index

0

Node type

n8n-nodes-base.httpRequest

Node version

4.4 (Latest)

n8n version

2.20.6 (Self Hosted)

Time

5/12/2026, 9:52:03 AM

Stack trace

NodeApiError: Authorization failed - please check your credentials at ExecuteContext.execute (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/n8n-nodes-base@file+packages+nodes-base_@aws-sdkaws-sdkaws-sdkaws-sdk+credential-providers@3.808.0_asn1.js@5_8da18263ca0574b0db58d4fefd8173ce/node_modules/n8n-nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts:825:16) at WorkflowExecute.executeNode (/usr/local/lib/node_modules/n8n/node_modules/.pnpm/n8n-core@file+package@opent@openlemetry+core_@open@opentelemetrye@opentelemetryemetry+api@1.9.0_@opentelemetry+exporter-trace-otlp_2c2e1f47b69b34bef6f634a13cbf61d9/node_modules/n8n-core/src/execution-engine/workflow-execute.ts:1048:9) at WorkflowExecute.runNode (/usr/local/lib/@opentelemetrynpmode_modules/n8n/node_modules/.@opentelemet@opentelemetryynpm/n8n-co@opentelemetry@opentelemetry@file+packages+core_@opentelemetry+api@1.9.0_@opentelemetry+exporter-trace-otlp_2c2e1f47b69b34bef6f634a13cbf61d9/node_modules/n8n-core/@opentelemetryodulesrc/ex@opentelemetryode_modulescution-engine/workflow-execute.ts:1@opentelemetry39:11) at /@opentelemetrysr/local/lib/node_@opentelemetryodules/n8n/@opentelemetryode_modules/.pnpm/n8n-core@file+packages+core_@opentelemetry+api@@opentelemetry7.9.0_@opentelemetry+exporter-trace-otlp_2c2e1f47b69b34bef6f634a13cbf61d9/node_modules/n8n-core/s@opentelemetryc/execution@opentelemetryengine/workflow-execute.ts:1687:@opentelemetry7 at /usr/l@opentelemetrycal/lib/node_modules/n8n/node_modules/.pnpm/n8n-core@file+packages+core_@opentelemetry+api@1.9.0_@opentelemetry+exporter-trace-otlp_2c2e1f47b69b34bef6f634a13cbf61d9/node_modules/n8n-core/src/execution-engine/workflow-execute.ts:2339:11
My understanding is that the 401 response should trigger a token refresh, but that doesn’t seem to be happening. Retriggering the workflow doesn’t help either.

If I go into credentials I can hit “reconnect” which will allow subsequent workflow executions to work for an hour.

To Reproduce
Configure Generic OAuth2 credential with authorization_code against a provider that rotates refresh tokens.
Run workflow successfully.
Wait until access token expiry.
Observe refresh cycle; after subsequent cycle, auth fails requiring reconnect.
Expected behavior
n8n should persist and use latest rotated refresh_token from token refresh response.
Workflows should continue without manual reconnect.

Debug Info
Debug info
core
n8nVersion: 2.20.6
platform: docker (self-hosted)
nodeJsVersion: 24.14.1
nodeEnv: production
database: sqlite
executionMode: regular
concurrency: -1
license: enterprise (production)
consumerId: 268c9581-f4d9-44ed-a7be-25c5c834d114
storage
success: all
error: all
progress: false
manual: true
binaryMode: filesystem
pruning
enabled: true
maxAge: 336 hours
maxCount: 10000 executions
client
userAgent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/148.0.0.0 safari/537.36
isTouchDevice: false
cluster
instanceCount: 1
versions: 2.20.6
instances:
instanceKey: 0cffc1ac-43d6-4469-85cf-116816732522, hostId: main-dec161f067e6, instanceType: main, instanceRole: leader, version: 2.20.6
checks:
check: hostid-clash, status: succeeded, warnings: -
check: lifecycle, status: succeeded, warnings: -
check: split-brain, status: succeeded, warnings: -
check: version-mismatch, status: succeeded, warnings: -
Generated at: 2026-05-12T17:00:18.159Z

Operating System
Ubuntu 22.04 LTS

n8n Version
2.20.6

Node.js Version
whatever is in image: docker.n8n.io/n8nio/n8n

Database
SQLite (default)

Execution mode
main (default)

Hosting
self hosted

Welcome @oxidation0917 to our community! I’m Jay and I am a n8n verified creator.

This is a known bug with Generic OAuth2 - the refresh token isn’t being stored or used properly in some cases. A practical workaround while waiting for the fix: in the Generic OAuth2 credential, make sure “Authentication” is set to “Body” (not Header) if Jobber supports it, and check that the token URL is correct and includes any required client credentials in the body. If Jobber’s access tokens are short-lived (1 hour), you can also build a manual refresh flow: a scheduled workflow that runs every 55 minutes, calls Jobber’s token endpoint via HTTP Request with grant_type: refresh_token, and updates the credential via the n8n REST API. This keeps you unblocked while the bug is resolved.

Hi @nguyenthieutoan Thank you so much for taking a look.

I’m currently using Body in the authentication settings as that’s the only way it would work. When you say check that Jobber includes required credentials in the body, do you mean to check the body of a curl?

I was thinking of the approach you suggest, using a cron trigger, but couldn’t quite map it out in my head, and via docs. Is there a way to access the stored credentials (specifically refresh_token) in the http request? I get the updating the credential via API part (I’ll have to figure out the structure), but since the request requires the refresh_token I got hung up on how to access it.

Hi @oxidation0917, happy to clarify this a bit more.

  1. About “credentials in the body”
    When I mentioned checking that Jobber includes the required credentials in the body, I meant: compare what you send from n8n with a working curl example from Jobber’s docs or from your own tests. For example, in a typical OAuth2 refresh call, the body often needs fields like:
  • grant_type=refresh_token

  • refresh_token=<your_refresh_token>

  • client_id=<your_client_id>

  • client_secret=<your_client_secret>

If your curl example works but the n8n HTTP Request node fails, you can mirror the exact same body and headers from the curl into the node.

  1. Accessing the stored refresh_token
    Unfortunately, with the current Generic OAuth2 bug, n8n is not exposing the stored refresh_token in an easy way inside regular nodes. That’s exactly why the auto-refresh is breaking. So instead of trying to “read” the refresh token from the credential at runtime, the usual workaround is:
  • Store the refresh_token somewhere you can control, for example in:

    • An n8n variable (Environment variable), or

    • A separate database/table, or

    • A simple data store like PostgreSQL/Firestore/Notion, depending on your stack.

  • Then your scheduled workflow can:

    • Read that stored refresh_token

    • Call Jobber’s token endpoint with grant_type=refresh_token

    • Update the access token in n8n via the REST API

  1. Sketch of the manual refresh flow
    Very roughly, the workflow would look like this:
  • Cron node: runs every 55 minutes

  • (Optional) Node to fetch the latest stored refresh_token from your storage

  • HTTP Request node:

    • Method: POST

    • URL: Jobber token URL

    • Auth: none (because you send everything in the body)

    • Body: grant_type=refresh_token, refresh_token=..., client_id, client_secret, etc.

  • HTTP Request node (n8n API):

    • Method: PATCH

    • URL: https://<your-n8n-url>/rest/credentials/<credential-id>

    • Auth: use your n8n API auth

    • Body: update the accessToken (and optionally the refresh token if Jobber returns a new one)

If you’d like, I can put together a concrete JSON example for both the Jobber token request and the n8n credential update payload, so you can plug them straight into your instance.

Looks like n8n may not be storing the refresh_token after the initial OAuth flow. I’d check the full token response from Jobber and confirm the refresh token is actually being returned and persisted. Sometimes providers only return it on the first authorization request.

You could also try forcing offline access / consent parameters in the OAuth config. Since you already opened a GitHub issue on GitHub, sharing the raw token response (with secrets removed) would probably help narrow it down quickly.

@nguyenthieutoan Thank you! This is great. I’m well on the way, writing a step by step for posterity, however running into issues updating via API.

Troubleshooting that didn't work

Initially I tried:

{
  "data": {
    "oauthTokenData": {
      "access_token": "access_token",
      "refresh_token": "refresh_token"
    }
  }
}

But I get

{
  "message": "request.body.data does not match allOf schema [subschema 0] with 12 error[s]:,request.body.data does not match allOf schema [subschema 0] with 1 error[s]:,request.body.data requires property \"grantType\",request.body.data does not match allOf schema [subschema 1] with 1 error[s]:,request.body.data requires property \"accessTokenUrl\",request.body.data does not match allOf schema [subschema 2] with 1 error[s]:,request.body.data requires property \"clientId\",request.body.data does not match allOf schema [subschema 3] with 1 error[s]:,request.body.data requires property \"clientSecret\",request.body.data does not match allOf schema [subschema 4] with 1 error[s]:,request.body.data requires property \"scope\",request.body.data does not match allOf schema [subschema 5] with 1 error[s]:,request.body.data requires property \"authentication\",request.body.data does not match allOf schema [subschema 1] with 2 error[s]:,request.body.data does not match allOf schema [subschema 0] with 1 error[s]:,request.body.data requires property \"serverUrl\",request.body.data does not match allOf schema [subschema 2] with 4 error[s]:,request.body.data does not match allOf schema [subschema 0] with 1 error[s]:,request.body.data requires property \"authUrl\",request.body.data does not match allOf schema [subschema 1] with 1 error[s]:,request.body.data requires property \"authQueryParameters\",request.body.data does not match allOf schema [subschema 3] with 4 error[s]:,request.body.data does not match allOf schema [subschema 0] with 1 error[s]:,request.body.data requires property \"sendAdditionalBodyProperties\",request.body.data does not match allOf schema [subschema 1] with 1 error[s]:,request.body.data requires property \"additionalBodyProperties\",request.body.data does not match allOf schema [subschema 4] with 2 error[s]:,request.body.data does not match allOf schema [subschema 0] with 1 error[s]:,request.body.data requires property \"jwksUriNotice\""
}

I tried giving it, and after a few iterations with Claude I end up with this:

{
  "data": {
    "grantType": "authorizationCode",
    "clientId": "clientId",
    "clientSecret": "clientSecret",
    "accessTokenUrl": "https://api.getjobber.com/api/oauth/token",
    "authUrl": "https://api.getjobber.com/api/oauth/authorize",
    "serverUrl": "{{MY N8N HOST?}}"
    "authQueryParameters": "",
    "scope": "",
    "authentication": "body",
    "jweEnabled": false,
    "oauthTokenData": {
      "access_token": "access_token",
      "refresh_token": "refresh_token",
      "token_type": "Bearer"
    }
  }
}

which appears to pass the schema checks, but now I get a 500 error. I don’t know what should be in serverURLso I put my n8n host.

Code	Details
500
Undocumented
Error: Internal Server Error

Response body
Download
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Internal Server Error</pre>
</body>
</html>

This is my credential structure from the database which has none of the additional parameters being demanding by the API. @David_Warner, it looks like n8n is storing refresh_token.

{
    "authUrl": "https://api.getjobber.com/api/oauth/authorize",
    "accessTokenUrl": "https://api.getjobber.com/api/oauth/token",
    "clientId": "clientId",
    "clientSecret": "clientSecret",
    "scope": "offline_access",
    "authentication": "body",
    "oauthTokenData": {
        "access_token": "access_token",
        "refresh_token": "refresh_token",
        "token_type": "Bearer"
    }
}

Hang tight. I got update via API working after playing with the Request_Body. I’ll share details soon.

Edit - Here’s what I’ve got documented so far.

A big thank you to @nguyenthieutoan for setting me on the right track. I’ve had much progress.

Hoping to make this easier for someone else (any myself) in the future I went through all the auth steps again, documenting as I went.

Initial Auth / Testing

  1. Authorization URL - Enter into browser that is authenticated with Jobber. The URL that you will be rediected to contains the authorization code, and the state as a confirmation.
https://api.getjobber.com/api/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=https:/YOUR_HOST/rest/oauth2-credential/callback&state=abc123

Response I recieved:

https://n8n.lab.atreehuman.com/rest/oauth2-credential/callback?code=CODE&state=abc123
  1. Curl with Authorization code - Adjust this CURL with your code from prior URL
curl -X POST https://api.getjobber.com/api/oauth/token -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=authorization_code&code=CODE&redirect_uri=YOUR_HOST/rest/oauth2-credential/callback"

Response:

{"access_token":"ACCESS_TOKEN","refresh_token":"REFRESH_TOKEN"}%

You are now authenticated and have 1 hour to refresh your access_token before it expires. Your refresh_token should work after that (indefinitely until rotation?). By default Jobber API rotates refresh_token upon each refresh of your access_token. You must store your refresh_token somewhere accessible for your refresh.

  1. Test API Authorization:
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" "https://api.getjobber.com/api/graphql"

Response when Authorized:

{"message":"An API version must be specified"}%

Updating n8n Credentials

  1. Create an API key under Settings > n8n API with scopes of credential:list, credential_update, and credential:read. Save the key shown on the pop-up after both to your clipboard, and a safe location. You’ll need it later when creating your cron workflow to refresh.

  2. Open the API Playground and authorize with your API key. I found I had to refresh the page after auth for the auth to take affect.

  3. Scroll down to GET /credentials click and “Try it out”

  4. Find your Jobber credential in the response data, and save its id to your notepad and clipboard

  5. Look further down and find PATCH /credentials/id, click “Try it out” and paste your id.

  6. Within “Request Body” paste the following and update it with your data. (refresh_token doesn’t really need to be here since n8n isn’t using it)

{
    "data": {
        "grantType": "authorizationCode",
        "serverUrl": "",
        "jweEnabled": false,
        "clientId": "CLIENT_ID",
        "clientSecret": "CLIENT_SECRET",
        "accessTokenUrl": "https://api.getjobber.com/api/oauth/token",
        "authUrl": "https://api.getjobber.com/api/oauth/authorize",
        "scope": "",
        "authentication": "body",
        "authQueryParameters": "",
        "oauthTokenData": {
            "access_token": "ACCESS_TOKEN",
            "refresh_token": "REFRESH_TOKEN"
        }
    }
}

You should get a Code 200 with response body:

{
    "id": "ID",
    "name": "Jobber",
    "type": "oAuth2Api",
    "isManaged": false,
    "isGlobal": false,
    "isResolvable": false,
    "resolvableAllowFallback": false,
    "resolverId": null,
    "createdAt": "2026-05-09T17:53:07.738Z",
    "updatedAt": "2026-05-16T19:27:53.231Z"
}

Now all that’s left is to move your refresh_token somewhere safe that can be accessed from a workflow, and then build a workflow that refreshes the token and updates the API.

Refreshing your access_token

curl -X POST https://api.getjobber.com/api/oauth/token -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=refresh_token&refresh_token=REFRESH_TOKEN"

Response:

{"access_token":"ACCESS_TOKEN","refresh_token":"REFRESH_TOKEN"}%

Just take that and update your credentials with the API like above, and you should be set. Now all this just needs to be made into a workflow and bandaid applied!

So, I’m still hoping to get this issue resolved, but for now we can workaround it.

Here’s a workflow I came up with. You’d have to set up a Postgres database / credentials and point those nodes at it as well as set an error reporting workflow, or disable that flag.

I don’t really like storing the credentials in the database, but currently it’s the most practical approach without a self-hosted enterprise license. Any suggestions otherwise that are more secure?

I’m open to any suggestions on the workflow too. Thanks all!

Edit - Unmarking as solution.

Well it’s kind of a solution. It makes the authentication last longer. Maybe a day now, but it seems n8n IS refreshing the token, just on its own schedule. This ends up breaking the bandaid.

Is it possible this also affects GitHub OAuth2 API credential, not just Generic OAuth2 credentials? I am using a GitHub OAuth2 credential along with GitHub nodes to fetch information. I have recently been getting a 401 error:
{ "message": "Bad credentials", "documentation_url": "https://docs.github.com/rest", "status": "401" }

I can manually resolve this by clicking the ‘reconnect’ button on the credential page. But this workaround only works for a few hours.

I’m having the same issue with an MCP OAuth2 API node. It won’t refresh the token. Any updates on the fix?

Two things to check that are often missed: first, make sure the “Refresh URL” field in the Generic OAuth2 credential is explicitly set to your provider’s token endpoint - n8n won’t infer it from the “Access Token URL” even though they’re usually the same. Second, check that your initial authorization request includes access_type=offline (or prompt=consent for Google-based providers) - without it, many providers don’t issue a refresh token at all, so n8n has nothing to use when the access token expires. You can add those under “Auth URI Query Parameters” in the credential settings.