getNodi

Templates / OAuth token refresh

Refreshing an OAuth token before it lapses

Access tokens usually last an hour. Refreshing them on a schedule rather than at the moment of use means the token in your store is always valid, and the refresh failing is an alert rather than an outage.

The recipe

Request
POST https://{{domain}}/oauth/token
Schedule
Every 45 minutes (*/45 * * * *)
Body
grant_type=refresh_token&refresh_token={{refresh_token}}&client_id={{client_id}}
Timeout
30s
Attempts
3
Counts as success
2xx and body contains access_token

You supply

Authorization server host
auth.example.com
Refresh token
rt_1a2b3c…stored encrypted
Client ID
abc123

Why this job exists

The textbook approach is refresh-on-401: make the call, and if the provider rejects the token, refresh and retry. It works, and for a single client it is the right design.

It degrades badly under concurrency. Ten workers hit an expired token at once, ten refreshes fire, and providers that rotate refresh tokens on use — which is now most of them — invalidate the other nine. The result is an account that has to be reconnected by hand, discovered when someone reports that an integration stopped.

Refreshing on a schedule that sits comfortably inside the token lifetime turns this into one predictable request. Every consumer reads a token that is already valid, and if the refresh itself fails you learn about it while the current token still has time left on it.

Setting it up

This recipe posts a standard RFC 6749 refresh grant as form-encoded data — the shape nearly every provider accepts. The success criteria check that the response body contains access_token, which is what distinguishes a real refresh from a 200 carrying an error object:

The request this sends

POST /oauth/token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=…&client_id=…

Where the provider requires a client secret too, add it as a secret placeholder value in the body. If it expects Basic authentication instead of client_id in the body, move the credentials to an Authorization header. Whatever comes back has to be stored, so in practice most people point this at their own endpoint, which performs the exchange and writes the result away.

Checking it actually works

  • Check that the response body contains access_token and a sensible expires_in — the built-in body check covers the first.
  • Confirm something persists the new token. A refresh whose response is discarded is a scheduled way of consuming refresh tokens for nothing.
  • Note the expires_in the provider actually returns and set the cadence to roughly two-thirds of it.

Where it goes wrong

Rotating refresh tokens

Many providers return a new refresh token with each exchange and invalidate the old one immediately. If you do not store the new one, the next run fails with invalid_grant and the connection must be re-authorised by a human. This is the single most common way this job breaks.

Refreshing too close to expiry

A 45-minute cadence against a 60-minute token leaves a comfortable margin. A 59-minute cadence leaves none: one slow run or one retry and consumers see an expired token. Refresh at around two-thirds of the lifetime.

Treating a 200 as success

OAuth error responses are not consistently non-2xx. Several large providers return 200 with {"error":"invalid_grant"}. The body check is what catches that, and removing it makes the job green while the integration is dead.

Questions

Is it safe to store a refresh token here?

Secret placeholder values are encrypted at rest and never rendered back into the UI or logs. That said, the tidier arrangement is to keep the token in your own store and have this call your endpoint, which reads it, refreshes, and writes back.

What if the refresh token expires entirely?

Then a person has to re-authorise; no scheduler can fix that. What this buys you is finding out on the day it happens, from a failure alert, rather than from a customer.

Should I keep refresh-on-401 as well?

Yes. This reduces how often that path is taken; it does not make the path unnecessary. Belt and braces is correct here.

Running it here

This is a preset in the product, not an illustration. Pick it in the dashboard, fill in the 3 values above, and the job is created, scheduled and enabled — with retries, a record of every run showing status, latency and response, and an alert the first time one fails. Secret values are encrypted at rest and never rendered back.

The free plan runs 3 jobs hourly and needs no card. Where a recipe wants a finer cadence than your plan allows, it is slowed to the fastest schedule you are permitted rather than rejected.