Skip to main content
This guide walks you through a full OAuth 2.0 integration with Monei Connect. By the end, your app will be able to redirect users to Monei, receive an authorization code, exchange it for tokens, and call the Monei API on the user’s behalf. Prerequisites: You’ve already registered your app and have a client_id and client_secret.

What we’re building

A simple web server that:
  1. Redirects a user to Monei to authorize wallet access
  2. Handles the callback and exchanges the code for tokens
  3. Calls GET /api/v1/wallet/me using the access token

Environment setup

# .env
MONEI_CLIENT_ID=mc_a3f9b2c1d4e5...
MONEI_CLIENT_SECRET=mcs_8d4e7f2a9b...
MONEI_REDIRECT_URI=http://localhost:3000/monei/callback
SESSION_SECRET=your_session_secret

The integration

import express from 'express';
import session from 'express-session';
import crypto from 'crypto';
import 'dotenv/config';

const app = express();
app.use(express.json());
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
}));

const MONEI_BASE = 'https://api.monei.cc/api/v1';

// ─── Step 1: Redirect user to Monei ──────────────────────────────────────────

app.get('/connect-monei', (req, res) => {
  // Generate a random state value to prevent CSRF attacks
  const state = crypto.randomBytes(16).toString('hex');
  req.session.oauthState = state;

  const params = new URLSearchParams({
    client_id:    process.env.MONEI_CLIENT_ID,
    redirect_uri: process.env.MONEI_REDIRECT_URI,
    scope:        'wallet:read profile:read',
    state,
  });

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

// ─── Step 2: Handle the callback ─────────────────────────────────────────────

app.get('/monei/callback', async (req, res) => {
  const { code, state, error } = req.query;

  // User denied access
  if (error) {
    return res.redirect('/dashboard?error=access_denied');
  }

  // Always validate state to prevent CSRF
  if (state !== req.session.oauthState) {
    return res.status(400).send('Invalid state parameter');
  }

  // ─── Step 3: Exchange code for tokens (server-side only) ─────────────────

  const tokenRes = await fetch(`${MONEI_BASE}/connect/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type:    'authorization_code',
      code,
      client_id:     process.env.MONEI_CLIENT_ID,
      client_secret: process.env.MONEI_CLIENT_SECRET,
      redirect_uri:  process.env.MONEI_REDIRECT_URI,
    }),
  });

  const tokens = await tokenRes.json();

  if (!tokenRes.ok) {
    console.error('Token exchange failed:', tokens);
    return res.redirect('/dashboard?error=token_exchange_failed');
  }

  // Store exactly what was granted — user may have approved fewer scopes
  req.session.moneiAccessToken  = tokens.access_token;
  req.session.moneiRefreshToken = tokens.refresh_token;
  req.session.moneiTokenExpiry  = Date.now() + (tokens.expires_in * 1000);
  req.session.moneiScopes       = tokens.scopes; // what was actually granted

  res.redirect('/dashboard?connected=true');
});

// ─── Step 4: Call the Monei API on behalf of the user ────────────────────────

app.get('/dashboard', async (req, res) => {
  const token = req.session.moneiAccessToken;

  if (!token) {
    return res.json({ connected: false });
  }

  const walletRes = await fetch(`${MONEI_BASE}/wallet/me`, {
    headers: { Authorization: `Bearer ${token}` },
  });

  const wallet = await walletRes.json();

  res.json({
    connected: true,
    scopes: req.session.moneiScopes,
    wallet: wallet.data,
  });
});

app.listen(3000, () => console.log('Running on http://localhost:3000'));

Test it locally

# Visit this in your browser
http://localhost:3000/connect-monei
You’ll be redirected to Monei’s consent screen. After approving, you land back at /dashboard with live wallet data.

What to do next

OAuth Flow Deep Dive

Understand every step in detail in state validation, error handling, edge cases

Handling Partial Grants

Users may approve fewer scopes than you requested, here’s how to handle that

Token Management

Refresh tokens before expiry, revoke on disconnect

All Scopes

Full reference of every scope and which endpoints it unlocks