Embeds

An embed is the real MGX interface, framed inside your product. Show a grower every live bid on their listings and let them accept, counter, or decline right there; show them their trades; let them talk to a counterparty — the same screens, the same rules, the same trade. You don't rebuild any of it, and you don't send your users to another site to close a deal.

Try it

The panel below is a real embed, framed inside a pretend partner page. Switch between all three with the buttons on the left: accept, counter or decline a bid; open a trade; read and answer a message. Flip the theme, or drop the write scope to see the read-only version. View My Bids opens whichever embed you picked the other way — inside a dialog your page owns.

Watch the counterparty as you go. On the trades embed only a settled trade names one; in messages, an elevator is named but a plain buyer is just “Buyer” until both invoices are paid.

The embed is a mini app, not a set of pop-ups: every step replaces the panel and the header carries a back button, right down to the date picker. Nothing floats over the frame, so nothing can be clipped by a frame whose height your page controls.

Note what accepting and countering ask for: the bid's terms are shown in full, and the seller has to tick an explicit acknowledgement that the deal is binding before the button unlocks. A counter can move the price, the quantity, and the delivery window; anything left alone carries over from the original bid.

AcmeAg — Your Bidsyour page
mgx iframe below
Bids Received3 bids · by price
Hard Red Spring Wheat · No. 1 CWRS
240 MT
Prairie Grain TerminalElevator
Delivered · expires in 2 days
$318.50/MT
120 MT
Buyer
FOB farm · expires in 6 days
$311.00/MT
240 MT
Desi Chickpeas
352 MT
BuyerCountered
FOB farm
$232.00/MT
162 MT
No. 1 · $232.00No. 2 · $220.46
Powered by My Grain ExchangeOpen dashboard
the same embed in display: "modal"
postMessage events your page receives
Accept, counter, or decline a bid above to see the messages.
This is a preview. It renders the same interface a grower sees, driven by sample data — nothing here reaches MGX and no trade is made. A real embed is framed from your own origin and needs a signed-in grower's token; follow the two steps below to get one running.

How it works

The whole flow exists to keep the grower's access token off the browser.

  1. Your user signs in with Login with MGX. You store their access token on your server, as usual.
  2. Your page asks your own backend for an embed session.
  3. Your backend calls POST /v1/embed/sessions with that token and gets back a URL containing a single-use ticket. The ticket has 120 seconds to be opened.
  4. Our loader script puts that URL in an iframe. The framed page spends the ticket once, over POST, and keeps the access token in memory for the life of the frame.
  5. The frame talks to you over postMessage — height changes, and an event every time a bid is accepted, countered, or declined.

The token never reaches your page's JavaScript, never lands in a URL, and is never written to browser storage. Reloading the frame starts with no credential at all.

Getting enabled

Embeds are not self-service. An embed is a live accept/decline surface, so MGX turns it on per client along with an allowlist of the origins that may frame it:

  • embed_enabled — off by default.
  • Embed origins — the exact origins your embed will be framed from, e.g. https://app.example.com. One leading wildcard label is supported: https://*.example.com. These become the page's frame-ancestors policy, so any other site that tries to frame it gets a blank frame.

Contact MGX with your client ID and the origins you need. Each embed also needs its own scopes on the client — see Available embeds. The session endpoint checks the scopes for the embed you ask for, so a client approved for trades but not messages simply cannot open the messages embed.

Step 1: a session endpoint

Add one route to your own backend. It runs as the signed-in user, calls MGX with their token, and returns the response untouched. This is the only place the access token appears.

Session endpoint

app.post('/mgx/embed-session', requireLogin, async (req, res) => {
  const response = await fetch('https://api.mygrainexchange.com/v1/embed/sessions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${await mgxTokenFor(req.user)}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      embed: 'bids-received',
      // Trust your own origin, not one the browser sent you.
      origin: 'https://app.example.com',
      theme: req.body.theme === 'dark' ? 'dark' : 'light',
    }),
  })

  res.status(response.status).json(await response.json())
})

Step 2: drop in the loader

Put a placeholder where the bid book should appear, point it at the route you just wrote, and load the script once per page.

Embed

<div data-mgx-embed="bids-received"
     data-session-url="/mgx/embed-session"
     data-min-height="240"></div>

<script src="https://dashboard.mygrainexchange.com/embed/mgx-embed.js" async></script>

That's the whole integration. The frame sizes itself, refreshes after every action, and re-mints its own session when one expires.

Or open it in a dialog

For a View My Bids button instead of a panel on the page, put the trigger attributes on any element. The embed opens in a dialog the loader owns, fills it, and scrolls internally — the version to reach for when you don't want to give up page real estate.

<button data-mgx-embed-trigger="bids-received"
        data-session-url="/mgx/embed-session">
  View My Bids
</button>

<script src="https://dashboard.mygrainexchange.com/embed/mgx-embed.js" async></script>
POST/v1/embed/sessions

Create an embed session

Mints the single-use URL. Call it from your server with a user's access token. Requires listings.read and bids.read, and embed_enabled on your client.

  • Name
    embed
    Type
    string
    Description

    Which embed to open: bids-received, trades, or messages (required).

  • Name
    origin
    Type
    string
    Description

    The origin that will frame it, e.g. https://app.example.com. Must be on your client's allowlist (required).

  • Name
    theme
    Type
    light | dark
    Description

    Colour scheme. Defaults to light.

  • Name
    locale
    Type
    en | fr
    Description

    Interface language. Defaults to en.

  • Name
    display
    Type
    inline | modal
    Description

    inline reports its height so your frame can grow with it; modal fills a dialog you own and scrolls itself. The loader sets this for you. Defaults to inline.

Request

POST
/v1/embed/sessions
curl https://api.mygrainexchange.com/v1/embed/sessions \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{ "embed": "bids-received", "origin": "https://app.example.com", "theme": "light" }'

Response

{
  "data": {
    "embed": "bids-received",
    "url": "https://dashboard.mygrainexchange.com/embed/bids-received?ticket=4DYsFWOi...",
    "expires_in": 120
  }
}

Pass the url straight to the loader — mint one per frame and don't cache it. The ticket is spent the moment a frame opens it, and goes stale after 120 seconds if it isn't. This is not the embed's lifetime: once redeemed, the frame runs on the access token behind it.

Loader reference

Attributes on the placeholder element:

  • Name
    data-mgx-embed
    Type
    string
    Description

    Which embed to render. Required.

  • Name
    data-session-url
    Type
    string
    Description

    Your own endpoint from step 1. Called with POST and same-origin credentials. Required.

  • Name
    data-theme
    Type
    light | dark
    Description

    Passed through to the session request. Defaults to light.

  • Name
    data-min-height
    Type
    number
    Description

    Starting height in pixels, before the frame reports its own. Defaults to 320.

  • Name
    data-max-height
    Type
    number
    Description

    Caps the auto-grow; the frame scrolls past it. Unset by default.

And window.MGXEmbed:

  • Name
    render(element, options)
    Type
    function
    Description

    Mount an embed into an element. Options mirror the data attributes: embed, sessionUrl, theme, minHeight, maxHeight.

  • Name
    open(options)
    Type
    function
    Description

    Open the embed in a dialog over your page. Takes embed, sessionUrl, theme, maxWidth. Returns { close, element }.

  • Name
    refresh(element)
    Type
    function
    Description

    Ask a mounted embed to refetch — useful after your own app changes a listing.

  • Name
    destroy(element)
    Type
    function
    Description

    Remove a mounted embed.

  • Name
    on(event, fn)
    Type
    function
    Description

    Subscribe to an event. Returns an unsubscribe function.

  • Name
    init()
    Type
    function
    Description

    Re-scan the DOM for data-mgx-embed elements. Runs automatically on load.

Events

Every handler receives { element, detail } — the placeholder it came from, and the raw message.

  • Name
    ready
    Type
    event
    Description

    The frame authenticated and rendered.

  • Name
    bid.accepted
    Type
    event
    Description

    A bid was accepted — a trade now exists. detail.bidId and detail.listingId name it.

  • Name
    bid.countered
    Type
    event
    Description

    The seller countered. The bid stays open with the new price.

  • Name
    bid.rejected
    Type
    event
    Description

    The seller declined.

  • Name
    close
    Type
    event
    Description

    A dialog-mode embed was dismissed.

  • Name
    message.sent
    Type
    event
    Description

    The user sent a message from the messages embed. detail.threadId names the thread.

  • Name
    auth.expired
    Type
    event
    Description

    The session ended. The loader re-mints one automatically; handle this only if you want to show your own state.

  • Name
    error
    Type
    event
    Description

    Something failed. detail.message is safe to log, not to show verbatim.

Listening

MGXEmbed.on('bid.accepted', ({ detail }) => {
  // Mirror the trade into your own system.
  fetch('/mgx/trade-created', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ bidId: detail.bidId, listingId: detail.listingId }),
  })
})

Available embeds

  • Name
    bids-received
    Type
    embed
    Description

    The seller's bid inbox: every live bid across their listings, grouped by listing, best price first — with accept, counter, and decline. Needs listings.read and bids.read; bids.write adds the action buttons.

  • Name
    trades
    Type
    embed
    Description

    Trades the user is party to, with a detail view for each: side, price, quantity, status, and the counterparty once settlement reveals it. Read-only. Needs trades.read.

  • Name
    messages
    Type
    embed
    Description

    Conversations with counterparties — a thread list and the thread itself, with a composer. Needs messages.read; messages.write adds the composer. See Messages for the endpoints behind it.

All three run the same mini app: a list, views reached through it, and a back button in the header.

On the bid book, accepting shows the bid's terms — handling fees, delivery window, sample approval, any free-text terms — and requires an explicit "I understand that all bids are binding" acknowledgement, exactly as the MGX dashboard does. Countering asks for the same acknowledgement and lets the seller change price, quantity, and delivery window in one step.

Both display modes work for every embed. Inline, the frame grows and shrinks as the user moves between views; in a dialog, the panel stays put and scrolls.

More embeds are coming. If there's a screen you'd rather frame than rebuild, tell us which one.

Read-only embeds

Leave the write scope off the token and an embed renders without its actions: the bid book loses accept/counter/decline, and messages loses the composer. A live view with nothing a user can accidentally commit to — useful for dashboards, reports, and internal tools. The trades embed is read-only either way.

Security model

  • Name
    Origin allowlist
    Description

    The framed page sends Content-Security-Policy: frame-ancestors built from your allowlist. A site that isn't on it gets a blank frame, not a working accept button.

  • Name
    Single-use tickets
    Description

    A ticket is redeemable once, within 120 seconds, and only by the framed page itself. A leaked URL is worthless within two minutes and usually the moment it loads. The embed itself is not time-limited by this — it runs for the life of the access token.

  • Name
    No third-party storage
    Description

    The token lives in the frame's memory. No cookies, no localStorage, no sessionStorage — nothing for a later page load, or another script, to pick up.

  • Name
    Counterparty anonymity
    Description

    Embeds obey the same rule as the rest of the API: the counterparty stays masked until both invoices on the resulting trade are paid. That holds inside a message thread too — a grower can talk to a buyer for weeks without either side learning who the other is, exactly as they can on MGX itself.

Troubleshooting

  • Name
    Blank frame, CSP error in the console
    Description

    Your page's origin isn't on the allowlist — including scheme and port. https://app.example.com does not cover https://www.app.example.com unless you registered the wildcard.

  • Name
    403 embeds_not_enabled
    Description

    embed_enabled is off for your client. Contact MGX.

  • Name
    403 origin_not_allowed
    Description

    The origin you sent isn't on the allowlist. Check for a trailing slash or a path — send a bare origin.

  • Name
    401 ticket_expired
    Description

    The ticket was already spent or older than 120 seconds — usually a cached session URL, or a frame that was reloaded. Mint a fresh session per frame. The loader already re-mints one for you when a frame reloads, so you should only see this if you are opening frames yourself.

  • Name
    'allow-scripts and allow-same-origin' console warning
    Description

    Expected. The frame needs its own origin to authenticate; the isolation that matters is that it is cross-origin to your page.

Was this page helpful?