Embedded Catalog Integration
Host a TCM catalog inside your own app, bridge the linked user into a TCM session, and authenticate the iframe with postMessage.
Local portal docs
Use this guide when you want to host a TCM catalog inside your own site or app with an iframe.
If you already have a TCM auth token and only need to validate it from your backend before trusting an embedded user, start with Embedded Widget Auth.
This is the recommended path for partner services that:
- keep their own local user/session model
- already use
external_clubprovisioning - want users to open TCM listings, messaging, and trading inside the partner UI
Summary
Cross-domain embeds should not rely on third-party cookies.
Instead:
- Provision or link your users through the External Club Integration.
- Keep your OAuth client secret on your server.
- Request a server-backed bridge token for the linked TCM user.
- Deliver that token pair into the iframe with a
postMessagehandshake. - Optionally brand the embed with theme query params.
The embedded catalog then uses the delivered TCM token as its authenticated session.
If your integration cannot use postMessage and instead passes the TCM session token to another backend-controlled surface, validate it server-to-server with POST /oauth/external-club/session/resolve before creating any local session.
What you need before starting
- A registered OAuth client in the Developers portal.
- The
external_clubscope enabled on that client. - A server-backed app. Do not expose
clientSecretin the browser. - A provisioning flow that calls
POST /:uiName/external-club/user. - A stable partner user id that you store as
clubUserId. - If you want custom branding, a set of agreed brand colors and an optional helper icon URL.
If you have not completed those steps yet, start with:
Architecture
The recommended flow has three moving parts:
1. Partner app session
Your app authenticates the user with your own local session or JWT.
2. Server bridge
Your backend calls the TCM bridge endpoint using your OAuth client credentials and the linked external-club user id.
3. Iframe handshake
Your frontend waits for the embedded catalog to announce readiness, then posts the TCM token pair into the iframe.
Bridge endpoint
Endpoint
POST /oauth/external-club/sessionTenant-prefixed deployments may also expose:
POST /:uiName/oauth/external-club/sessionFor your current environments, the tenant-prefixed endpoint is:
Production: https://www.thecrimsonmarket.com/mana/oauth/external-club/session
Development: https://dev.portal.raum.au/mana/oauth/external-club/sessionAuthentication
Use HTTP Basic auth with your OAuth client credentials:
clientIdclientSecret
The client must be active and allowed to use external_club.
Request body
{
"externalUserId": "partner-user-123"
}externalUserId must match the clubUserId that was previously linked through POST /:uiName/external-club/user.
Example server request
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
const response = await fetch(`${TCM_OAUTH_API_URL}/oauth/external-club/session`, {
method: "POST",
headers: {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
externalUserId: partnerUserId,
}),
});Success response
{
"status": 201,
"message": "Created",
"data": {
"token": "tcm_user_token",
"refreshToken": "tcm_refresh_token",
"userId": "portal-user-id"
}
}Your backend should usually unwrap response.data.data.
Iframe handshake
Child iframe behavior
The catalog sends:
{
"type": "IFRAME_READY"
}The catalog also sends resize events:
{
"type": "IFRAME_HEIGHT",
"height": 1840
}Hosts should use that message to resize the iframe and avoid nested scrollbars.
Parent host response
Your app should respond with:
{
"type": "TOKEN_DELIVERY",
"token": "tcm_user_token",
"refreshToken": "tcm_refresh_token"
}Example host page
<iframe
id="tcm-catalog"
src="https://www.thecrimsonmarket.com/catalog/listings/pokemonlistings?parentOrigin=https%3A%2F%2Fpartner.example.com"
width="100%"
height="900"
style="border:0"
scrolling="no"
></iframe>
<script>
const iframe = document.getElementById("tcm-catalog");
window.addEventListener("message", async (event) => {
if (event.origin !== "https://www.thecrimsonmarket.com") return;
if (event.data?.type === "IFRAME_HEIGHT" && Number.isFinite(event.data?.height)) {
iframe.style.height = `${Math.ceil(event.data.height)}px`;
return;
}
if (event.data?.type !== "IFRAME_READY") return;
const response = await fetch("/api/trade/catalog-session", { method: "POST" }).then((res) => res.json());
const session = response.data;
iframe?.contentWindow?.postMessage(
{
type: "TOKEN_DELIVERY",
token: session.token,
refreshToken: session.refreshToken,
},
"https://www.thecrimsonmarket.com",
);
});
</script>Recommended iframe route
Use the internal catalog embed route:
https://www.thecrimsonmarket.com/catalog/listings/:subjectExamples:
https://www.thecrimsonmarket.com/catalog/listings/pokemonlistingshttps://www.thecrimsonmarket.com/catalog/listings/morelistings
Do not use the public /:uiName/listings/... route for embedded partners.
Optional branding
The catalog supports a standardized set of iframe query params so partner brands can adjust the embedded experience without forking the catalog UI.
Embedded controls
Use embed_ params when the iframe should show partner-controlled controls inside the catalog page itself. These controls scroll with normal page content and are persisted during catalog navigation.
Subject selector params
embed_subject_selector: set totrueto show a full-width subject dropdown at the top of catalog listing pages.
Currency params
embed_currency: set totrueto show a currency action section.embed_currency_subjects: comma-separated catalog subject slugs where the currency section should appear. Omit this param to show it on every subject.embed_currency_relic_type: theservice.relicscurrency or relic type key, for examplecharades-coin.embed_currency_label: display name for the currency.embed_currency_rate: optional exchange-rate text, for example100 Charades Coin = $1 AUD.embed_currency_wallet_label: wallet button text.embed_currency_buy_label: buy button text.embed_currency_info_label: optional info-link text. If omitted, the info action is hidden.
Host navigation params used by embedded controls
host_vault_url: where the wallet button sends the parent app.host_buy_currency_url: where the buy button sends the parent app. This is treated as an exact host-owned destination and can include{relicType}if the route needs the selectedservice.relicscurrency key.host_currency_info_url: where the optional info button sends the parent app.
Relative host URLs are resolved by the parent application. Absolute URLs can be used for standalone partner sites.
Stock Checker example
https://www.thecrimsonmarket.com/catalog/listings/pokemonlistings?parentOrigin=https%3A%2F%2Fwww.thecrimsonmarket.com&embed_subject_selector=true&embed_currency=true&embed_currency_subjects=pokemonlistings&embed_currency_relic_type=charades-coin&embed_currency_label=Charades%20Coin&embed_currency_rate=100%20Charades%20Coin%20%3D%20%241%20AUD&embed_currency_wallet_label=Wallet&embed_currency_buy_label=Buy%20Charades%20Coin&host_vault_url=%2Fvault&host_buy_currency_url=%2Fvault%3Ftab%3Dbuy%26relicType%3Dcharades-coin&host_currency_info_url=%2Fcharades-coinPartner currency example
Use the currency key issued by service.relics for embed_currency_relic_type. Do not expose service secrets in iframe URLs.
https://www.thecrimsonmarket.com/catalog/listings/morelistings?parentOrigin=https%3A%2F%2Fpartner.example.com&embed_subject_selector=true&embed_currency=true&embed_currency_subjects=morelistings&embed_currency_relic_type=partner-credit&embed_currency_label=Partner%20Credit&embed_currency_rate=100%20Partner%20Credit%20%3D%20%241%20USD&embed_currency_wallet_label=Wallet&embed_currency_buy_label=Buy%20Partner%20Credit&host_vault_url=https%3A%2F%2Fpartner.example.com%2Fwallet&host_buy_currency_url=https%3A%2F%2Fpartner.example.com%2Fwallet%2FbuyColor and surface tokens
theme_brandtheme_brand_darktheme_brand_deeptheme_primary_buttontheme_primary_gradient_starttheme_primary_gradient_endtheme_secondary_buttontheme_secondary_button_hovertheme_secondary_button_inactivetheme_headertheme_back_containertheme_front_containertheme_middle_container_toptheme_front_container_toptheme_container_backgroundtheme_container_background_hovertheme_bordertheme_inputtheme_input_bordertheme_input_focus_bordertheme_linktheme_club_fire_light
Helper icon token
theme_ai_helper_icon
This changes the icon used beside the "Help me Search" AI helper input.
Example branded iframe URL
https://www.thecrimsonmarket.com/catalog/listings/pokemonlistings?parentOrigin=https%3A%2F%2Fpartner.example.com&theme_brand=%23e24a2a&theme_primary_button=%23e24a2a&theme_primary_gradient_start=%23ff6b4a&theme_primary_gradient_end=%23a61f08&theme_back_container=%230b0b0f&theme_border=rgba(255,255,255,0.18)&theme_ai_helper_icon=https%3A%2F%2Fpartner.example.com%2Fmascot.webpBranding guidance
- Prefer changing only the standardized theme params, not internal catalog classes.
- Keep contrast high enough for white catalog text.
- Provide a stable public image URL for
theme_ai_helper_icon. - Pass only exact values you want to override; all other tokens fall back to TCM defaults.
Recommended implementation pattern
For production partner apps:
- authenticate the user in your own system first
- keep your OAuth
clientSecreton the server only - create a partner-only backend route such as
/api/trade/catalog-session - have that route call TCM with the current partner user id
- cache only as needed; most apps can request a fresh bridge session on page load
- listen for
IFRAME_HEIGHTand resize the iframe instead of showing an inner scrollbar - treat the iframe URL as configuration, including optional branding query params
Security requirements
- Never expose your OAuth
clientSecretin browser code. - Validate the iframe
event.originbefore answeringIFRAME_READY. - Post tokens back only to the exact TCM origin you embedded.
- Pass
parentOriginin the iframe URL when you want stricter child-side origin validation. - Treat the returned
tokenandrefreshTokenas user session material. - Sanitize or strictly control any branding query params you append to the iframe URL.
Minimum rollout checklist
- Create an OAuth client with
external_club. - Provision partner users through
POST /:uiName/external-club/user. - Add a backend route that exchanges
externalUserIdfor a TCM session. - Embed
https://www.thecrimsonmarket.com/catalog/listings/:subject. - Add an iframe page that waits for
IFRAME_READY. - Reply with
TOKEN_DELIVERYusing the bridged TCM tokens. - Listen for
IFRAME_HEIGHTand resize the iframe. - Add optional theme params if you want partner-specific branding.
- Test login, refresh, signed-out behavior, and iframe resizing.