Skip to main content
Monei gives users granular control over scope approvals. When your app requests multiple scopes, the user can approve all of them, some of them, or none of them individually. This means your app will sometimes receive fewer scopes than it requested. This is not an error, it’s by design. Building around it is a requirement, not an edge case.

How to detect what was granted

The scopes field in the token response tells you exactly what the user approved:
{
  "access_token":  "mct_...",
  "refresh_token": "mcr_...",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "scopes":        ["wallet:read", "profile:read"]
}
If you requested wallet:read wallet:send wallet:withdraw but the user only approved wallet:read and profile:read, the response will only contain those two.

Check and store granted scopes

Always inspect the scopes field immediately after token exchange and store it:
const tokens = await exchangeCodeForTokens(code);

const granted = new Set(tokens.scopes);

const hasWalletRead     = granted.has('wallet:read');
const hasWalletSend     = granted.has('wallet:send');
const hasWalletWithdraw = granted.has('wallet:withdraw');
const hasBillsPay       = granted.has('bills:pay');
const hasOfframp        = granted.has('offramp:execute');

// Store what was actually granted
await db.users.update(userId, {
  moneiAccessToken:  encrypt(tokens.access_token),
  moneiRefreshToken: encrypt(tokens.refresh_token),
  moneiTokenExpiry:  Date.now() + (tokens.expires_in * 1000),
  moneiScopes:       tokens.scopes, // ← what was actually granted
});

Show users what they can do

Don’t silently break features. Show users clearly what is available based on what they granted:
function getAvailableFeatures(scopes) {
  const granted = new Set(scopes);

  return {
    canViewBalance:   granted.has('wallet:read'),
    canSendToFriend:  granted.has('wallet:send'),
    canWithdrawToBank: granted.has('wallet:withdraw'),
    canPayBills:      granted.has('bills:pay'),
    canOfframp:       granted.has('offramp:execute'),
  };
}

// In your UI layer
const features = getAvailableFeatures(user.moneiScopes);

if (!features.canWithdrawToBank) {
  showBanner(
    'Bank withdrawal is not enabled. ' +
    'Reconnect your Monei account to enable it.'
  );
}

Re-request a specific scope

If a user tries to use a feature that requires a scope they didn’t grant, you can send them through the authorization flow again requesting only the missing scope. Monei will show just the new scope on the consent screen. Scopes already granted are not shown again.
app.get('/enable-withdrawals', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  req.session.oauthState = state;

  // Request only the missing scope
  const params = new URLSearchParams({
    client_id:    process.env.MONEI_CLIENT_ID,
    redirect_uri: process.env.MONEI_REDIRECT_URI,
    scope:        'wallet:withdraw', // only what's missing
    state,
  });

  res.redirect(`https://monei.cc/connect/authorize?${params}`);
});

What to do when a critical scope is missing

If your app cannot function at all without a scope the user declined:
  1. Don’t throw an error silently
  2. Explain which feature is unavailable and why it needs that permission
  3. Offer a clear path to re-authorize link to your /enable-[feature] route
  4. Never call an endpoint the user hasn’t granted access to. It returns 403 and erodes trust

Scopes Reference

Full table of every scope and what it unlocks

Token Management

Refresh, store, and revoke access tokens