← Back to blog

Oracle Fusion REST API: OAuth Token Expiration and Refresh (Why a Working Integration Starts Throwing 401s)

By Mostafa Mansour 8 min read Oracle Fusion CloudREST APIOAuthAuthenticationTroubleshooting

A pattern that shows up repeatedly on Cloud Customer Connect and the Oracle Forums: an integration authenticates fine, runs for a while, then starts throwing 401 Unauthorized with no code change on either side (“401 Unauthorized error when using Fusion Data extraction REST API”; “401 Unauthorized when attempting to access Fusion Applications HCM REST API with OAuth”). Our authentication guide covers the three ways to authenticate and the 401-vs-403 split, but treats token expiry as one row in a table. This post is the deeper dive: what actually expires, how to refresh it, and the two errors — invalid_grant and “token has already been consumed” — that trip up most refresh implementations.

Access tokens are short-lived on purpose

An OAuth 2.0 access token issued by your Fusion instance’s identity domain (IDCS, or OCI IAM on newer tenancies) is not meant to outlive a single work session. Oracle’s own recommended default is 3,600 seconds (1 hour) for access token expiration, and it’s a setting the identity-domain administrator controls per confidential application — treat it as configurable, not hardcoded, and check your app’s actual token settings before assuming a number. The refresh token that comes with it lives longer but is still capped: Oracle recommends 604,800 seconds (7 days) for refresh token expiration, and that ceiling cannot be raised past 7 days regardless of app configuration — a detail that matters if you’re writing something that runs weekly rather than continuously, since a refresh token that sits unused for a week-plus dies on its own schedule, independent of whether it was ever “consumed.”

This is fine for interactive use and for short scheduled jobs. It becomes a problem the moment you write a long-running or continuously-polling integration (an Atom feed consumer, a sync daemon, a webhook receiver reacting to business events) that expects the same credential to keep working for hours or days. It won’t — and the failure mode is a clean 401, indistinguishable at a glance from a wrong password or a clock-skew problem, which is exactly why the two Fusion threads above start with someone assuming they broke something.

Not getting a refresh_token at all? Check your scope before your grant type

Before troubleshooting the refresh flow itself, make sure your original token request is actually configured to receive a refresh token. Two mistakes cause this, and both look identical from the outside — an access_token with no refresh_token field anywhere in the response:

Missing offline_access in the scope. For the OAuth 2.0 Authorization Code flow — the one used by user-facing integrations and by OIC connections to Fusion — Oracle’s identity domain only issues a refresh token if the scope explicitly includes offline_access alongside the Fusion resource scope. Leave it out and you’ll get a working access token with no way to refresh it, which is easy to misread as “refresh isn’t supported for this flow” when it’s actually just a missing scope value.

A scope borrowed from the wrong Oracle Cloud service. The scope has to be the exact value configured on your resource application inside your identity domain (IDCS or OCI IAM) — don’t construct or guess it, and don’t reuse a scope example from a different Oracle Cloud service’s docs (OCI’s other REST APIs use their own distinct scope namespaces). A scope that doesn’t belong to your Fusion instance will either fail the token request outright or return a token that doesn’t actually grant access to Fusion HCM/FSCM resources. A real, working example from Oracle’s own integration guidance, combining a Fusion resource scope with offline_access:

scope=urn:opc:resource:fa:instanceid=<your-instance-id> urn:opc:resource:consumer::all offline_access

Treat that as a pattern, not a literal string to copy — the exact scope value is whatever your identity domain admin configured when the resource was added to your confidential application, and it’s visible on the application’s Resources/Scopes tab. If your response is missing refresh_token, this is the first thing to check, before assuming anything is wrong with the refresh call itself.

The refresh_token grant

If your token request used a grant type that returns a refresh token — the Authorization Code flow does when offline_access is in scope per the section above, and the user-assertion flow typically does as well; a bare client-credentials grant typically does not, since there’s no user session to refresh — the token response includes a refresh_token alongside the access_token. Use it to get a new access token without repeating the full authentication flow:

curl -u '<client_id>:<client_secret>' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  --data-urlencode 'refresh_token=<the refresh token from the last response>' \
  'https://idcs-abc123.identity.oraclecloud.com/oauth2/v1/token'

A successful response looks like the original token response: a new access_token, a new expires_in, and — this is the detail that causes most bugs — usually a new refresh_token too.

Error 1: invalid_grant

Calling the refresh endpoint with a bad refresh token returns an invalid_grant error — per the OAuth 2.0 spec this is normally 400 Bad Request, though some Fusion identity-domain flows (notably JWT-bearer grants presented directly as an expired token) have been reported returning 401 Unauthorized with the same error: invalid_grant body instead. Don’t rely on the HTTP status alone to distinguish this from a plain expired-access-token 401 — check the response body’s error/error_description fields. Three separate causes collapse into the same invalid_grant error:

invalid_grant on a refresh call means “stop trying to refresh, re-run the original authentication flow” — there’s no recovery path that reuses that same refresh token.

Error 2: “the token has already been consumed”

This is the gotcha that isn’t obvious from the docs: Fusion’s identity domain issues single-use, rotating refresh tokens. Every successful refresh_token call invalidates the refresh token you just spent and returns a brand-new one in its place. If your integration code does any of the following, it will eventually hit “already consumed”:

The fix in all three cases is the same discipline: treat “refresh” as a single-writer operation (a lock or a queue, not a race), and overwrite your stored refresh token with the new one from every response — including the one that comes back with the access token you’ll actually use.

A practical pattern for long-running integrations

Waiting for a 401 before refreshing works, but it means every token expiry costs you one failed request in production. The more robust pattern:

  1. Store the access token’s expires_in (or compute an absolute expiry timestamp) alongside the token itself.
  2. Before each call, check whether the token expires within the next minute or two. If so, refresh proactively — don’t wait for the 401.
  3. If a call still comes back 401 despite a fresh-looking token (clock skew, an admin revoked the app mid-session), refresh once and retry the request exactly once. Don’t loop.
  4. On invalid_grant from the refresh call itself, stop refreshing and fall back to the full authentication flow — a refresh token that’s dead needs a new login, not another refresh attempt.

Diagnostic table

SymptomCauseFix
401 after the integration ran fine for a whileAccess token expired — this is expected behavior, not a bugRefresh proactively before expiry, or catch the 401 and refresh-then-retry once
400 invalid_grant on the refresh callRefresh token expired, revoked, or already usedRe-run the full authentication flow; there’s no way to “fix” a dead refresh token
”Token has already been consumed” specificallyTwo callers (a retry, a race between threads/processes) both tried to use the same refresh tokenMake refresh a single-writer operation; always persist the newest refresh token returned
401 even immediately after a successful token requestClock skew between your system and the identity domain, or the token simply wasn’t propagated yetCheck system clocks; for JWT trusted-issuer tokens, keep iat/exp generous enough to absorb small skew

Where OPAL fits

While you’re building and testing this refresh logic, re-running the same request by hand with a freshly minted token — to confirm the new token actually works before you wire the retry logic into production — gets tedious with raw curl. OPAL stores your credentials locally encrypted (AES-256-GCM, no cloud sync) and lets you drop in a new token and re-send the identical request from its history, so you can validate each piece of the refresh flow (does the new access token work? does the new refresh token work next time?) without hand-editing headers between attempts.


This post extends our Oracle Fusion REST API authentication guide — Basic Auth, JWT bearer, and OAuth 2.0 setup, plus 401-vs-403 troubleshooting. It’s also part of the complete Oracle Fusion API guide.

Explore Oracle Fusion APIs offline

OPAL bundles 59,000+ Oracle Fusion REST endpoints, fully searchable offline, with a visual Q Builder and Finder Builder that only offer fields the endpoint actually accepts — so your filter can't 400.

Free, no account required. Pro adds live requests and multi-step Flows.