> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monei.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# Token Management

> How to refresh, store, and revoke Monei Connect access tokens

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

| Token         | Prefix | Lifetime   | Purpose                   |
| ------------- | ------ | ---------- | ------------------------- |
| Access token  | `mct_` | 1 hour     | Authenticating API calls  |
| Refresh token | `mcr_` | Long-lived | Getting new access tokens |

***

## Storing tokens

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

<CodeGroup>
  ```javascript Node.js theme={null}
  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,
  });
  ```

  ```python Python theme={null}
  import os
  import base64
  from cryptography.fernet import Fernet

  fernet = Fernet(os.getenv("TOKEN_ENCRYPTION_KEY").encode())

  def encrypt(text: str) -> str:
      return fernet.encrypt(text.encode()).decode()

  def decrypt(text: str) -> str:
      return fernet.decrypt(text.encode()).decode()

  # After token exchange
  db.users.update(user_id, {
      "monei_access_token":  encrypt(tokens["access_token"]),
      "monei_refresh_token": encrypt(tokens["refresh_token"]),
      "monei_token_expiry":  tokens["expires_in"],
      "monei_scopes":        tokens["scopes"],
  })
  ```
</CodeGroup>

***

## 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.

```bash theme={null}
POST /api/v1/connect/token
Content-Type: application/json

{
  "grant_type":    "refresh_token",
  "refresh_token": "mcr_raw_refresh_token_here"
}
```

**Response:**

```json theme={null}
{
  "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:

<CodeGroup>
  ```javascript Node.js theme={null}
  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');
    }
  }
  ```

  ```python Python theme={null}
  import time
  import requests
  import os

  EXPIRY_BUFFER = 5 * 60  # refresh 5 minutes before expiry

  def get_valid_access_token(user_id: str) -> str:
      user = db.users.find_by_id(user_id)

      # Token is still valid
      if user["monei_token_expiry"] > time.time() + EXPIRY_BUFFER:
          return decrypt(user["monei_access_token"])

      # Token is expired or about to expire. Refresh it
      res = requests.post(
          "https://api.monei.cc/api/v1/connect/token",
          json={
              "grant_type":    "refresh_token",
              "refresh_token": decrypt(user["monei_refresh_token"]),
          },
      )

      if not res.ok:
          # Refresh token is invalid. User must re-authorize
          db.users.update(user_id, {
              "monei_access_token":  None,
              "monei_refresh_token": None,
              "monei_token_expiry":  None,
              "monei_scopes":        [],
          })
          raise Exception("MONEI_REAUTH_REQUIRED")

      tokens = res.json()

      # Store the new token pair immediately. Old one is now invalid
      db.users.update(user_id, {
          "monei_access_token":  encrypt(tokens["access_token"]),
          "monei_refresh_token": encrypt(tokens["refresh_token"]),
          "monei_token_expiry":  time.time() + tokens["expires_in"],
      })

      return tokens["access_token"]

  # Usage
  try:
      token = get_valid_access_token(user_id)
      res = requests.get(
          "https://api.monei.cc/api/v1/wallet/me",
          headers={"Authorization": f"Bearer {token}"},
      )
  except Exception as e:
      if str(e) == "MONEI_REAUTH_REQUIRED":
          # Redirect user to re-authorize
          return redirect("/connect-monei")
  ```
</CodeGroup>

***

## 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.

```bash theme={null}
POST /api/v1/connect/token/revoke
Content-Type: application/json

{
  "token": "mct_raw_access_token_here"
}
```

<CodeGroup>
  ```javascript Node.js theme={null}
  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:       [],
    });
  }
  ```

  ```python Python theme={null}
  def disconnect_monei(user_id: str):
      user = db.users.find_by_id(user_id)

      requests.post(
          "https://api.monei.cc/api/v1/connect/token/revoke",
          json={"token": decrypt(user["monei_access_token"])},
      )

      # Clear from your database regardless of revocation response
      db.users.update(user_id, {
          "monei_access_token":  None,
          "monei_refresh_token": None,
          "monei_token_expiry":  None,
          "monei_scopes":        [],
      })
  ```
</CodeGroup>

<Note>
  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.
</Note>

***

<CardGroup cols={2}>
  <Card title="Security" icon="shield-check" href="/connect/security">
    Key storage, CSRF protection, and production checklist
  </Card>

  <Card title="Managing Grants" icon="sliders" href="/connect/managing-grants">
    Let users see and manage their connected apps
  </Card>
</CardGroup>
