Skip to main content
Access tokens expire after 1 hour. Refresh tokens let you get a new access token without the user going through the authorization flow again. This page covers the full token lifecycle.

Token types

TokenPrefixLifetimePurpose
Access tokenmct_1 hourAuthenticating API calls
Refresh tokenmcr_Long-livedGetting new access tokens

Storing tokens

Always store tokens encrypted at rest. Never store them in plain text, cookies without encryption, or client-side storage.
import crypto from 'crypto';

const ENCRYPTION_KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY, 'hex'); // 32 bytes
const IV_LENGTH = 16;

function encrypt(text) {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
  return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
}

function decrypt(text) {
  const [ivHex, encryptedHex] = text.split(':');
  const iv = Buffer.from(ivHex, 'hex');
  const encrypted = Buffer.from(encryptedHex, 'hex');
  const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
  return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
}

// After token exchange
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,
});

Refreshing the access token

Call the token endpoint with grant_type: refresh_token before the access token expires. The old token pair is invalidated immediately — store the new tokens right away.
POST /api/v1/connect/token
Content-Type: application/json

{
  "grant_type":    "refresh_token",
  "refresh_token": "mcr_raw_refresh_token_here"
}
Response:
{
  "access_token":  "mct_new_token...",
  "refresh_token": "mcr_new_refresh...",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "scopes":        ["wallet:read", "profile:read"]
}

Implementing an auto-refresh wrapper

Build a helper that checks expiry before every API call and refreshes proactively:
async function getValidAccessToken(userId) {
  const user = await db.users.findById(userId);
  const expiryBuffer = 5 * 60 * 1000; // refresh 5 minutes before expiry

  // Token is still valid
  if (user.moneiTokenExpiry > Date.now() + expiryBuffer) {
    return decrypt(user.moneiAccessToken);
  }

  // Token is expired or about to expire. Refresh it
  const res = await fetch('https://api.monei.cc/api/v1/connect/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type:    'refresh_token',
      refresh_token: decrypt(user.moneiRefreshToken),
    }),
  });

  if (!res.ok) {
    // Refresh token is invalid. User must re-authorize
    await db.users.update(userId, {
      moneiAccessToken:  null,
      moneiRefreshToken: null,
      moneiTokenExpiry:  null,
      moneiScopes:       [],
    });
    throw new Error('MONEI_REAUTH_REQUIRED');
  }

  const tokens = await res.json();

  // Store the new token pair immediately. Old one is now invalid
  await db.users.update(userId, {
    moneiAccessToken:  encrypt(tokens.access_token),
    moneiRefreshToken: encrypt(tokens.refresh_token),
    moneiTokenExpiry:  Date.now() + (tokens.expires_in * 1000),
  });

  return tokens.access_token;
}

// Usage
try {
  const token = await getValidAccessToken(userId);
  const res = await fetch('https://api.monei.cc/api/v1/wallet/me', {
    headers: { Authorization: `Bearer ${token}` },
  });
} catch (err) {
  if (err.message === 'MONEI_REAUTH_REQUIRED') {
    // Redirect user to re-authorize
    res.redirect('/connect-monei');
  }
}

Revoking a token

Revoke tokens when a user disconnects your app from their account on your platform. This immediately invalidates both the access token and its paired refresh token.
POST /api/v1/connect/token/revoke
Content-Type: application/json

{
  "token": "mct_raw_access_token_here"
}
async function disconnectMonei(userId) {
  const user = await db.users.findById(userId);

  await fetch('https://api.monei.cc/api/v1/connect/token/revoke', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token: decrypt(user.moneiAccessToken) }),
  });

  // Clear from your database regardless of revocation response
  await db.users.update(userId, {
    moneiAccessToken:  null,
    moneiRefreshToken: null,
    moneiTokenExpiry:  null,
    moneiScopes:       [],
  });
}
Users can also revoke access directly from their Monei settings at any time. When this happens, subsequent API calls with that token return 401. Your app must handle this and prompt the user to reconnect.

Security

Key storage, CSRF protection, and production checklist

Managing Grants

Let users see and manage their connected apps