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.
Embeds are for when you want MGX's UI. If you'd rather build your own, every action here is available as a plain endpoint — see Bids and My inventory.
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.
display: "modal"How it works
The whole flow exists to keep the grower's access token off the browser.
- Your user signs in with Login with MGX. You store their access token on your server, as usual.
- Your page asks your own backend for an embed session.
- Your backend calls
POST /v1/embed/sessionswith that token and gets back a URL containing a single-use ticket. The ticket has 120 seconds to be opened. - 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. - 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.
The 120 seconds is the ticket's redemption window, not a time limit on the embed. The frame spends it within a second of loading and then holds the access token, so a grower can work in the embed for as long as that token is valid — 15 days by default. The only thing 120 seconds constrains is how long a session URL is worth anything if it sits unused.
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'sframe-ancestorspolicy, 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())
})
Set origin from your own configuration, never from a request header or query
parameter. It is checked against your allowlist, so a caller can't widen it —
but taking it from the client is still one less thing to get right.
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>
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, ormessages(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
inlinereports its height so your frame can grow with it;modalfills a dialog you own and scrolls itself. The loader sets this for you. Defaults toinline.
Request
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
POSTand 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-embedelements. 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.bidIdanddetail.listingIdname 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.threadIdnames 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.messageis 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 }),
})
})
Treat these as a UI signal, not a system of record — a browser event can be
missed if the tab closes mid-action. Subscribe to the
webhooks for bid.accepted and trade.created if you need a
guaranteed feed.
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.readandbids.read;bids.writeadds 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.writeadds 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-ancestorsbuilt 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, nosessionStorage— 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.comdoes not coverhttps://www.app.example.comunless you registered the wildcard.
- Name
403 embeds_not_enabled- Description
embed_enabledis off for your client. Contact MGX.
- Name
403 origin_not_allowed- Description
The
originyou 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.