Skip to main content
Connect tokens give your app real access to real money. Take these seriously.

Client secret rules

Your client_secret is the most sensitive credential in your Connect integration. Treat it like a private key. Never:
  • Include it in frontend JavaScript or HTML
  • Put it in a mobile app binary
  • Commit it to version control (.env files included)
  • Log it or send it in error reports
  • Expose it in API responses
Always:
  • Store it as an environment variable
  • Use it only in server-side token exchange
  • Rotate it immediately if you suspect it was exposed

State parameter (CSRF protection)

Always generate a fresh state value per authorization request and validate it on callback:
import crypto from 'crypto';

// On redirect
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state; // store in server-side session

// On callback, reject if it doesn't match
if (req.query.state !== req.session.oauthState) {
  return res.status(400).send('Invalid state — possible CSRF attack');
}
Without state validation, an attacker can trick a user’s browser into completing an OAuth flow the attacker controls.

Token storage

Store access and refresh tokens encrypted at rest. See the Token Management page for encryption helpers in Node.js and Python. Don’t store tokens in:
  • Browser localStorage or sessionStorage
  • Unencrypted database columns
  • HTTP cookies without HttpOnly and Secure flags

Handle 401 from user revocation

Users can revoke your app’s access from their Monei settings at any time. When this happens, any API call with that token returns 401. Your app must handle this gracefully:
async function callMoneiApi(userId, endpoint) {
  const token = await getValidAccessToken(userId);

  const res = await fetch(`https://api.monei.cc/api/v1${endpoint}`, {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (res.status === 401) {
    // User revoked access. Clear tokens and prompt reconnect
    await db.users.update(userId, {
      moneiAccessToken:  null,
      moneiRefreshToken: null,
      moneiTokenExpiry:  null,
      moneiScopes:       [],
    });
    throw new Error('MONEI_ACCESS_REVOKED');
  }

  return res.json();
}

Production checklist

Before going live, confirm every item:
  • client_secret is stored as an environment variable, not in code
  • client_secret is not committed to version control
  • Separate credentials for dev and production environments
  • Redirect URIs registered for production domain (not just localhost)
  • state parameter generated fresh per request using crypto.randomBytes or secrets.token_hex
  • state validated on every callback before processing the code
  • error query param handled on callback, user-friendly message shown
  • Token exchange happens server-side only
  • Authorization code exchanged immediately after callback (expires in 10 minutes)
  • Access tokens stored encrypted at rest
  • Refresh tokens stored encrypted at rest
  • Token expiry stored and checked before each API call
  • Auto-refresh implemented with 5-minute buffer before expiry
  • 401 from API handled by clearing tokens and prompting reconnect
  • Token revocation called when user disconnects from your platform
  • scopes field read from token response (not assumed from request)
  • Granted scopes stored per user
  • UI disables or hides features the user hasn’t granted
  • 403 from API handled gracefully, not shown as a raw error
  • Re-authorization path built for upgrading individual scopes

Errors & Rate Limits

Full error reference and rate limit table