OAuth2
Sign users into Relu from your own product and call the API on their behalf.
This page covers your product obtaining a token from Relu and calling Relu. If Relu will obtain a token from your LMS or scan-intake product, see Expose a managed Inbox source.
OAuth2 lets your product sign a user into their own Relu account and then act as that user — both for REST calls and inside the iframe viewer. The user consents once; you get an access token that carries their identity, capped to the scopes your integration is allowed.
Use this instead of user tokens when your users already have (or should have) Relu accounts of their own. Nothing has to be minted per order, and the orders stay owned by the user rather than by your integration.
Getting a client
OAuth2 clients are provisioned per partner — they are not self-service. Email support@relu.ai with the redirect URIs you need and we will send back:
- a client ID
- a client secret
- the list of scopes enabled for your integration
The client is a confidential client, so the secret must stay on your server. Do not attempt the flow from a browser-only app.
The flow
Standard authorization code flow. Two steps.
1. Redirect the user
https://login.relu.ai/authorize
?response_type=code
&client_id=<your_client_id>
&redirect_uri=<your_redirect_uri>
&audience=https://api.automate.relu.ai
&scope=openid%20offline_access%20read:orders%20update:orders
&state=<cryptographically_random_state>The audience parameter is required — without it Auth0 issues an opaque token that the Relu API cannot read.
Ask for offline_access if you want a refresh token. Request only the scopes you need: anything outside your integration's allow-list is silently dropped from the issued token, not an error.
Generate state fresh for every authorization attempt from a cryptographically secure source, and store it against the browser session that started the flow.
The user signs into Relu and sees a consent screen naming your integration and the scopes requested. On approval they are sent back to your redirect_uri with ?code=...&state=....
Validate state before exchanging the code
Compare the returned state against the value you stored for that session,
and reject the callback if it is missing or does not match. Skipping this lets
an attacker replay their own authorization response into a victim's session,
binding that session to the attacker's Relu account.
2. Exchange the code
From your server:
curl -X POST https://login.relu.ai/oauth/token \
-u "<client_id>:<client_secret>" \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=<your_redirect_uri>{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "v1.M8t...",
"expires_in": 7200,
"token_type": "Bearer"
}Access tokens last 2 hours. Refresh tokens rotate on each use, idle out after 100 days, and expire after 1 year.
Refreshing
curl -X POST https://login.relu.ai/oauth/token \
-u "<client_id>:<client_secret>" \
-d grant_type=refresh_token \
-d refresh_token=<refresh_token>Rotation means the response contains a new refresh token. Store it and discard the old one, or the next refresh fails.
Calling the API
Send the access token as a bearer token:
curl https://api.automate.relu.ai/orders \
-H "Authorization: Bearer <access_token>"Every request is attributed to the signed-in user, not to your integration — the same as if they had called the API themselves.
Bearer, not X-API-Key
OAuth2 access tokens go in Authorization: Bearer. The X-API-Key header is
only for API keys and user
tokens; sending an access token there
always returns 401.
Using it in the iframe
Pass the access token straight to createViewerHost with type: 'oauth2'. There is no token exchange step — the viewer sends it as a bearer token for you.
const host = createViewerHost({
container: document.getElementById('viewer')!,
viewerUrl: 'https://implant.automate.relu.ai?orderId=<order_id>',
integratorId: 'your-integrator-id',
auth: {
type: 'oauth2',
token: accessToken,
// Called when the viewer sees the token rejected. Hit YOUR OWN backend —
// refreshing needs your client secret, so it can't happen in the browser —
// and return the fresh access token. The SDK pushes it to the viewer.
onTokenRefresh: async () => {
const res = await fetch('https://your-app.example.com/relu/refresh', {
method: 'POST',
});
return (await res.json()).access_token;
},
},
onReady: (api, appInfo) => console.log('Connected to', appInfo),
});Because access tokens expire after 2 hours and a viewer session can outlive that, always supply onTokenRefresh for OAuth2 embeds. Without it the iframe stops working mid-session when the token expires.
Note that the request which triggered the refresh is not replayed — only subsequent calls use the new token. If a save fails because the token had just expired, the user has to repeat it.
onTokenRefresh is skipped for insufficient_permissions: those come from your client's scope allow-list, which a refreshed token cannot change, so onAuthRequired fires instead.
Requires @relu-bv/iframe-api 0.8.0 or newer on both sides. If the viewer origin you embed serves an older build, the SDK refuses to hand over the credential and reports an AUTH_MODE_UNSUPPORTED error through onError rather than letting the viewer mis-send it.
Which auth should I use?
| Situation | Use |
|---|---|
| Your users have their own Relu accounts | oauth2 |
| Your users have no Relu account, you own the orders | User tokens |
| Server-to-server, no user in the loop | API keys |
Scopes
Your integration's allow-list is fixed when the client is provisioned; a token can never exceed it, no matter what the user is personally allowed to do. Requesting more just yields a narrower token.
See Scopes for what each one covers. If the viewer reports missing permissions, check the scopes on your client before anything else — that is the usual cause.
Troubleshooting
401on every call, you sent the token asX-API-Key. UseAuthorization: Bearer.401with a token that looks fine, theaudienceparameter was missing from the authorize request, so Auth0 issued an opaque token instead of a JWT for the Relu API.403on some endpoints only, that scope is not on your integration's allow-list. Email support to have it added.- Iframe loads then goes blank after ~2 hours, the access token expired and no
onTokenRefreshwas supplied. onErrorwithAUTH_MODE_UNSUPPORTED, the viewer origin you embed is older than 0.8.0 and cannot present an OAuth2 token. Point at an updated origin, or useapiTokenauth there.- Refresh returns
invalid_grant, you reused a rotated refresh token. Always persist the new one from each refresh response.