ReluAutomate
Authentication

OAuth2

Sign users into Relu from your own product and call the API on their behalf.

API integration Iframe + draft orders

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?

SituationUse
Your users have their own Relu accountsoauth2
Your users have no Relu account, you own the ordersUser tokens
Server-to-server, no user in the loopAPI 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

  • 401 on every call, you sent the token as X-API-Key. Use Authorization: Bearer.
  • 401 with a token that looks fine, the audience parameter was missing from the authorize request, so Auth0 issued an opaque token instead of a JWT for the Relu API.
  • 403 on 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 onTokenRefresh was supplied.
  • onError with AUTH_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 use apiToken auth there.
  • Refresh returns invalid_grant, you reused a rotated refresh token. Always persist the new one from each refresh response.

On this page