Skip to main content
This page explains each step of the Monei Connect OAuth 2.0 Authorization Code Flow in detail including edge cases, error handling, and things that trip developers up.

Overview

Your App          User Browser           Monei
   │                    │                   │
   │── /connect-monei ──▶                   │
   │                    │── authorize ──────▶│
   │                    │    (login +        │
   │                    │   consent screen)  │
   │                    │◀─── redirect ──────│
   │◀── /callback?code ─│                   │
   │                    │                   │
   │────────────────────── POST /token ─────▶│
   │◀──────────────── access_token ─────────│
   │                    │                   │
   │────────────── GET /wallet/me ──────────▶│
   │◀─────────────── wallet data ───────────│

Step 1: Build the authorization URL

Redirect your user to:
https://monei.cc/connect/authorize
  ?client_id=mc_a3f9b2c1d4e5
  &redirect_uri=https://yourapp.com/monei/callback
  &scope=wallet:read wallet:send profile:read
  &state=a3f9b2c1...

Parameters

ParameterRequiredDescription
client_idYour app’s client ID
redirect_uriMust exactly match a registered URI including protocol, path, no trailing slash differences
scopeSpace-separated list of scopes you are requesting. See Scopes
stateStrongly recommendedRandom string you generate. Returned unchanged on callback. Validates the request wasn’t tampered with

Generating state

Always generate state fresh per request and store it in the user’s session:
import crypto from 'crypto';

const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;

Step 2: User approves on Monei

The user lands on Monei’s consent screen. If not logged in to Monei, they are prompted to log in first. The consent screen shows:
  • Your app name and logo (from registration)
  • Every scope you requested, in plain English
  • A clear allow / deny choice per scope
Users control individual scope approvals. They can approve wallet:read but deny wallet:send. Your app must handle this. See Partial Grants.

Step 3: Monei redirects back

On approval

https://yourapp.com/monei/callback?code=a8f3c2d9...&state=a3f9b2c1...
The code is short-lived, exchange it within 10 minutes or it expires.

On denial

https://yourapp.com/monei/callback?error=access_denied&state=a3f9b2c1...
Handle this gracefully and show the user what features won’t be available and offer to try again.

Validating state

Always compare the returned state to what you stored before proceeding:
const { code, state, error } = req.query;

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

if (state !== req.session.oauthState) {
  // Possible CSRF attack — reject immediately
  return res.status(400).send('Invalid state');
}

// Safe to proceed

Step 4: Exchange the code for tokens

This must happen server-side. Never expose your client_secret in frontend or mobile code.
POST /api/v1/connect/token
Content-Type: application/json

{
  "grant_type":    "authorization_code",
  "code":          "a8f3c2d9...",
  "client_id":     "mc_a3f9b2c1d4e5",
  "client_secret": "mcs_8d4e7f2a9b...",
  "redirect_uri":  "https://yourapp.com/monei/callback"
}

Response

{
  "access_token":  "mct_...",
  "refresh_token": "mcr_...",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "scopes":        ["wallet:read", "profile:read"]
}
The scopes array reflects what the user actually approved — not everything you requested. A user could have declined some scopes. Always read this field and store it. See Partial Grants.

Implementation

async function exchangeCodeForTokens(code) {
  const res = await fetch('https://api.monei.cc/api/v1/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,
    }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Token exchange failed: ${err.message}`);
  }

  return res.json();
}

Step 5: Call APIs on behalf of the user

Pass the access token as a Bearer token on every request:
const res = await fetch('https://api.monei.cc/api/v1/wallet/me', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const wallet = await res.json();
The token only permits what the user approved. Calling an endpoint outside the granted scopes returns 403 Forbidden.

Common mistakes

Using an expired code: authorization codes expire in 10 minutes. Exchange them immediately in the callback handler. Skipping state validation: always validate state. Skipping it leaves your users vulnerable to CSRF. Token exchange in the browser: your client_secret must never touch the frontend. All token exchange happens on your server. Assuming all scopes were granted: users can partially approve. Always check the scopes field in the token response. Not storing the refresh token: access tokens expire in 1 hour. Store the refresh token so you can get a new one without the user re-authorizing.

Token Management

How to refresh, store, and revoke tokens

Partial Grants

Handle users approving fewer scopes than requested