Login with MGX button

Give growers and buyers a familiar one-click way to connect their MGX account to your product. The button starts the authorization-code flow: the user signs in on MGX, picks the team your integration may act as, approves the scopes you asked for, and lands back on your callback with a code.

Three ways to add it, in order of least effort:

  1. Script embed — one <div> and one <script>. Generates state (and PKCE) for you.
  2. HTML and CSS — link our stylesheet and paste the markup. You build the authorize URL.
  3. Static images — SVG/PNG files for emails, CMS content, or anywhere you can't add CSS.

You need an OAuth client with a registered redirect URI. Create one in your MGX dashboard — click your name in the top-right corner and choose Developers.

Button builder

Pick a style, drop in your client details, and copy the snippet. The preview is the real button rendered with the hosted stylesheet.

Preview only — add a client ID and redirect URI to test it

Theme
Size
Shape
Label
Client ID
Redirect URI
Scopes
Test it end-to-end. Paste your client ID above and click Use the docs test callback (https://developers.mygrainexchange.com/login-button/callback is accepted on every MGX client — nothing to register), then click the live preview. You'll sign in on MGX and get the authorization code back here with a ready-made token exchange. Seeing invalid_client? The redirect URI above isn't registered on that client (it must match exactly), or the client ID is wrong. This builder talks to https://dashboard.mygrainexchange.com.
<div data-mgx-login
     data-client-id="YOUR_CLIENT_ID"
     data-redirect-uri="https://example.com/oauth/callback"
     data-scope="openid profile email"
     data-popup="true"></div>
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js" async></script>

<!-- On the page with the button: -->
<script>
  MGXLogin.onLogin(function (login) {
    // login.code, login.state, login.stateValid, login.codeVerifier
  })
</script>

<!-- On your callback page (https://example.com/oauth/callback): -->
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js"></script>
<script>MGXLogin.completePopup()</script>

Testing it

The preview above is inert until you give it a client ID and redirect URI — then it becomes a real button. To try the whole flow without writing any code:

  1. Paste your client ID into the builder and click Use the docs test callback. This callback is accepted on every MGX OAuth client — nothing to register.
  2. Click the live preview. Sign in, pick a team, approve the scopes.

MGX opens in a popup (untick Popup to test the full-page redirect instead). When you approve, the result comes straight back into the builder: state is verified, the code (and PKCE verifier, if enabled) is shown, and you get a copy-paste token exchange to run from your server. The docs site never sees your client secret.

If MGX responds with invalid_client / "Client authentication failed" with your own redirect URI, it isn't registered on that client (it has to match character-for-character, including the scheme and port) or the client ID is wrong.

Script embed

Drop a placeholder element where the button should appear and load the script once per page. It renders the button, links the hosted stylesheet, and builds the authorize URL with a fresh random state every time the page loads.

<div data-mgx-login
     data-client-id="YOUR_CLIENT_ID"
     data-redirect-uri="https://example.com/oauth/callback"
     data-scope="openid profile email bids.write"></div>

<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js" async></script>
  • Name
    data-client-id
    Type
    string
    Description

    Your OAuth client ID. Required.

  • Name
    data-redirect-uri
    Type
    string
    Description

    Where MGX sends the user after they approve. Must exactly match a redirect URI registered on the client. Required.

  • Name
    data-scope
    Type
    string
    Description

    Space-separated scopes. Defaults to openid profile email.

  • Name
    data-theme
    Type
    dark | light | red
    Description

    Colour scheme. Defaults to dark.

  • Name
    data-size
    Type
    sm | md | lg
    Description

    Button height: 36px, 44px, or 52px. Defaults to md.

  • Name
    data-shape
    Type
    rounded | pill
    Description

    Corner style. Defaults to rounded.

  • Name
    data-block
    Type
    boolean
    Description

    Stretch to the full width of the container.

  • Name
    data-label
    Type
    string
    Description

    Button text. Defaults to Login with MGX. See the approved labels below.

  • Name
    data-pkce
    Type
    boolean
    Description

    Generate a PKCE code_verifier/code_challenge pair. Required for public clients (no client secret). The verifier is stored in sessionStorage for your callback page.

  • Name
    data-state
    Type
    string
    Description

    Supply your own state instead of a random one.

  • Name
    data-prompt
    Type
    login | consent
    Description

    Force the user to re-authenticate or re-approve scopes.

  • Name
    data-popup
    Type
    boolean
    Description

    Open MGX in a centred popup window instead of navigating away. Your callback page calls MGXLogin.completePopup() and the page with the button listens with MGXLogin.onLogin() — see below.

Popup mode

With data-popup="true" the user never leaves your page. The popup loads MGX, and after approval your callback page hands the result back to the opener and closes itself:

<div data-mgx-login data-popup="true"
     data-client-id="YOUR_CLIENT_ID"
     data-redirect-uri="https://example.com/oauth/callback"></div>
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js" async></script>

<script>
  window.addEventListener('load', function () {
    MGXLogin.onLogin(function (login) {
      if (login.error || !login.stateValid) { /* show an error */ return }
      // Send login.code (and login.codeVerifier, if PKCE) to your backend
      fetch('/api/mgx/exchange', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: login.code, codeVerifier: login.codeVerifier }),
      })
    })
  })
</script>

The script also exposes a small API on window.MGXLogin for single-page apps that render the button themselves:

// Render into any element
MGXLogin.render(document.querySelector('#connect'), {
  clientId: 'YOUR_CLIENT_ID',
  redirectUri: 'https://example.com/oauth/callback',
  scope: 'openid profile email bids.write',
  theme: 'light',
  pkce: true,
})

// Or just get a URL and redirect yourself.
// prepare() generates state (+ PKCE) and stores them in sessionStorage.
const url = await MGXLogin.prepare({
  clientId: 'YOUR_CLIENT_ID',
  redirectUri: 'https://example.com/oauth/callback',
  pkce: true,
})
window.location.assign(url)

HTML and CSS

If you'd rather not load a script, link the stylesheet and use the markup directly. The mark is an inline SVG so it inherits the button's text colour — no extra image request.

<link rel="stylesheet" href="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.css">

<a class="mgx-login mgx-login--light"
   href="https://dashboard.mygrainexchange.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&scope=openid%20profile%20email&state=RANDOM_STATE">
  <svg class="mgx-login__mark" viewBox="0 0 880.1 825.2" aria-hidden="true">
    <!-- paste the paths from https://dashboard.mygrainexchange.com/login-with-mgx/mgx-mark.svg -->
  </svg>
  <span class="mgx-login__label">Login with MGX</span>
</a>

Compose the look with modifier classes:

ClassEffect
mgx-loginBase button. Dark theme, 44px tall.
mgx-login--lightWhite background, grey border, red mark.
mgx-login--redMGX red background.
mgx-login--sm / mgx-login--lg36px / 52px tall.
mgx-login--pillFully rounded corners.
mgx-login--blockFull-width, centred content.

The builder above generates the full markup, including the inline mark, for any combination.

Static images

For emails, CMS pages, or README files, use a plain link around a hosted image. SVGs stay crisp at any size; the PNGs are 188×44 at 1x and 376×88 at 2x.

<a href="https://dashboard.mygrainexchange.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&scope=openid%20profile%20email&state=RANDOM_STATE">
  <img src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx-dark.svg"
       alt="Login with MGX" width="188" height="44">
</a>
Login with MGX, darklogin-with-mgx-dark.svg
Login with MGX, lightlogin-with-mgx-light.svg
Login with MGX, redlogin-with-mgx-red.svg

Every variant is also available as .png (1x) and @2x.png, and the bare mark as mgx-mark.svg. All files live under https://dashboard.mygrainexchange.com/login-with-mgx/. When inlined, the dark half of the mark uses currentColor and the red half stays MGX red, so it adapts to each theme automatically.

Handling the callback

MGX redirects to your redirect_uri with code and state. On your server:

  1. Verify state matches the value you generated when rendering the button. Reject the login otherwise.
  2. Exchange the code for tokens at /oauth/token. This needs your client secret (or the PKCE verifier for public clients), so it must happen server-side.
  3. Read the identity from /oauth/userinfosub (stable MGX user id), email, name, mgx_team_id (the team the user chose), mgx_roles.
  4. Persist the tokens against your user. Access tokens last 15 days; refresh tokens last 30 days and are rotated on every refresh, so always store the newest one.

If you used the script embed, the pending state (and PKCE verifier) are waiting in sessionStorage. Call MGXLogin.consume() on your callback page to read and clear them, then post the code and verifier to your backend:

const params = new URLSearchParams(window.location.search)
const { state, codeVerifier } = MGXLogin.consume()

if (!params.get('code') || params.get('state') !== state) {
  throw new Error('Login was tampered with or expired — start again.')
}

await fetch('/api/mgx/exchange', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code: params.get('code'), codeVerifier }),
})

Server-side with the SDKs

Every official SDK ships the same two helpers: exchangeAuthorizationCode(...) returns a token set to persist, and fromAuthorizationCode(...) does the exchange and hands you a client already acting as that user's team. Construct later clients from the stored tokens and pass an onTokenRefresh callback so rotated refresh tokens are written back.

import { MgxClient } from '@mygrainexchange/sdk'

const MGX_HOST = 'https://dashboard.mygrainexchange.com'
const REDIRECT_URI = 'https://example.com/oauth/callback'

app.get('/oauth/callback', async (req, res) => {
  const { code, state } = req.query
  if (!code || state !== req.session.mgxState) {
    return res.status(400).send('Login expired or tampered with — start again.')
  }

  // 1. Exchange the code (server-side: needs the client secret)
  const tokens = await MgxClient.exchangeAuthorizationCode({
    clientId: process.env.MGX_CLIENT_ID!,
    clientSecret: process.env.MGX_CLIENT_SECRET!,
    code: String(code),
    redirectUri: REDIRECT_URI,
    codeVerifier: req.session.mgxCodeVerifier, // only if you used PKCE
  })

  // 2. Who is this? Which team did they pick?
  const me = await fetch(`${MGX_HOST}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${tokens.accessToken}` },
  }).then((r) => r.json())

  // 3. Persist per user — refresh tokens rotate, so store the newest set
  await db.mgxConnections.upsert({
    userId: req.user.id,
    mgxUserId: me.sub,
    mgxTeamId: me.mgx_team_id,
    accessToken: tokens.accessToken,
    refreshToken: tokens.refreshToken,
    expiresAt: tokens.expiresAt,
  })

  res.redirect('/settings/integrations')
})

// Later, act as that team. onTokenRefresh keeps the stored tokens current.
async function mgxFor(userId: string) {
  const row = await db.mgxConnections.find(userId)
  return new MgxClient({
    clientId: process.env.MGX_CLIENT_ID!,
    clientSecret: process.env.MGX_CLIENT_SECRET!,
    accessToken: row.accessToken,
    refreshToken: row.refreshToken,
    expiresAt: row.expiresAt,
    onTokenRefresh: (t) => db.mgxConnections.update(userId, t),
  })
}

const mgx = await mgxFor(req.user.id)
for await (const bid of mgx.bids.list()) console.log(bid.id, bid.status)

Prefer one call? fromAuthorizationCode (from_authorization_code / FromAuthorizationCodeAsync) does the exchange and returns the authenticated client together with the tokens to persist:

const { client: mgx, tokens } = await MgxClient.fromAuthorizationCode({
  clientId, clientSecret, code, redirectUri: REDIRECT_URI,
  onTokenRefresh: (t) => db.mgxConnections.update(userId, t),
})
await db.mgxConnections.upsert({ userId, ...tokens })
const teams = await mgx.teams.list()

Brand guidelines

A few rules keep the button recognisable across partner sites.

  • Use an approved label. Login with MGX, Sign in with MGX, Continue with MGX, or Connect MGX account. Don't abbreviate or translate "MGX".
  • Keep the mark intact. Don't recolour it outside the three themes, rotate it, add effects, or replace it with another icon.
  • Give it room. Leave clear space of at least half the button's height on every side, and never render it shorter than 36px.
  • Match your surroundings. Use light on dark backgrounds and dark or red on light ones. Pick the theme that gives the most contrast.
  • One button per action. Show it once, in the same place you show other sign-in providers. Don't stack multiple MGX buttons with different scopes.

Was this page helpful?