Welcome to The Crimson Market Developers Portal

Build powerful integrations with The Crimson Market API. Create OAuth applications, manage your credentials, and access our comprehensive documentation to bring trading functionality to your platform.

Developers Documentation

TCM OAuth Integration Docs

SDK-backed guidance for @crimsoncorp/oauth-react, plus portal-specific setup details for apps, scopes, and redirect registration.

External Club Integration

Provision external-club users, validate bcrypt password hashes, and understand webhook enrichment on /oauth/userinfo.

Local portal docs

This guide documents the current Portal.Service contract for partner apps that provision or link users through the external_club flow.

Summary

Partners can call POST /:uiName/external-club/user to create or update a linked portal user. If hub integration is enabled, Portal.Service also creates or logs in the hub user and adds them to the configured hub club.

The OAuth client used for this request must be active and allowed to use the external_club scope.

Partners can also:

  • call POST /oauth/external-club/session to mint a TCM session token for a linked user
  • call POST /oauth/external-club/session/resolve to validate that token later and resolve the linked TCM user from their own backend

If you are looking for the simplest hosted-widget validation flow, use Embedded Widget Auth.

Required request contract

Endpoint

POST /:uiName/external-club/user

Required fields

  • userName
  • email
  • clubName
  • clubUserId
  • hubClubId
  • clientId
  • clientSecret
  • encryptedPassword

Optional fields

  • webhookEventEndpoint
  • avatarUrl

encryptedPassword must be a bcrypt hash. Portal.Service rejects non-bcrypt values.

Example payload

{
  "userName": "charades_diablo4traders",
  "email": "[email protected]",
  "encryptedPassword": "$2b$10$.................................................",
  "clubName": "charadescollective",
  "clubUserId": "113769124091223507890",
  "hubClubId": "11735",
  "clientId": "tcm_xxx",
  "clientSecret": "your-oauth-client-secret",
  "webhookEventEndpoint": "https://pokecharades.example.com/api/v1/public/external-club/user-context",
  "avatarUrl": "https://partner.example.com/path/to/google-avatar.webp"
}

Validation and behavior

  • The OAuth client credentials are validated before user provisioning starts.
  • The client must include external_club in allowedScopes.
  • Existing users can be matched by the partner mapping (clubName plus clubUserId) or by normalized email.
  • Portal.Service persists the external-club metadata, the OAuth client id, and the stored password hash on the linked user.
  • If avatarUrl is supplied, Portal.Service stores it on the linked user and may attempt a best-effort Hub avatar sync when the Hub member does not already have a profile image.
  • The response includes the resolved hubUserId, hubClubId, oauthClientId, and the stored password hash.

Hub avatar syncing is non-blocking. A failed avatar upload does not fail the external-club provisioning request or the partner login flow.

Base URL and tenant path

The root base URL should not include the tenant path. The uiName segment is part of the request route.

EXTERNAL_CLUB_BASE_URL=https://www.thecrimsonmarket.com
EXTERNAL_CLUB_UI_NAME=mana

Resulting endpoint:

https://www.thecrimsonmarket.com/mana/external-club/user

Development base URL:

EXTERNAL_CLUB_BASE_URL=https://dev.portal.raum.au
EXTERNAL_CLUB_UI_NAME=mana

Development endpoint:

https://dev.portal.raum.au/mana/external-club/user

Embedded session bridge

Use this when your site needs to create a TCM-authenticated session for a linked external-club user.

Create a session token

POST /oauth/external-club/session

Tenant-prefixed deployments may also expose:

POST /:uiName/oauth/external-club/session

Authenticate with HTTP Basic auth using your OAuth clientId and clientSecret.

Request body:

{
  "externalUserId": "partner-user-123"
}

Success response:

{
  "token": "tcm_user_token",
  "refreshToken": "tcm_refresh_token",
  "userId": "portal-user-id"
}

The returned token is a TCM session token for the linked user. Keep it short-lived and treat it like a login credential.

Keeping embedded iframes logged in

Partner sites should keep their own users logged in with their own session system, then use the external-club bridge to create a TCM session only when an embedded TCM iframe needs it.

Do not depend on cross-domain browser cookies for embedded auth. TCM-owned pages may use cookies such as authToken, refreshToken, tcm_ips_token, and tcm_ips_user_id when the user is already browsing on a TCM domain, but a partner domain cannot reliably set or refresh those cookies for thecrimsonmarket.com. Modern browsers also restrict third-party cookies inside iframes.

The recommended iframe login flow is:

  1. The partner backend provisions or repairs the linked user with POST /:uiName/external-club/user.
  2. The partner frontend loads the TCM iframe.
  3. The iframe posts { "type": "IFRAME_READY" } to the parent window.
  4. The parent calls a partner-owned backend endpoint, for example /api/trade/catalog-session.
  5. That backend endpoint authenticates the local partner user, then calls POST /:uiName/oauth/external-club/session with HTTP Basic auth and the current partner user id as externalUserId.
  6. The backend returns the bridged TCM token and refreshToken to the parent page.
  7. The parent posts the token pair back into the iframe with { "type": "TOKEN_DELIVERY", "token": "...", "refreshToken": "..." }.
  8. The iframe uses the delivered token as its authenticated TCM session.

Example parent-side handoff:

const iframe = document.getElementById("credit-iframe") as HTMLIFrameElement | null;
const tcmOrigin = "https://www.thecrimsonmarket.com";

window.addEventListener("message", async (event) => {
  if (event.origin !== tcmOrigin) return;
  if (event.source !== iframe?.contentWindow) return;
  if (event.data?.type !== "IFRAME_READY") return;

  const response = await fetch("/api/trade/catalog-session", { method: "POST" });
  const payload = await response.json();
  const session = payload.data || payload;

  iframe.contentWindow?.postMessage(
    {
      type: "TOKEN_DELIVERY",
      token: session.token,
      refreshToken: session.refreshToken,
    },
    tcmOrigin,
  );
});

The Charades Collective implementation follows this pattern for catalog, vault, and wallet embeds. Email/password and Google users both authenticate to Charades first. Charades then uses the stored external-club link to request a TCM bridge session and delivers that session to the iframe. If the local login path has not finished creating the external-club link yet, the backend bridge endpoint should repair the link before requesting the TCM session.

Resolve an external-club token for a wallet embed

Use this endpoint when a TCM-owned wallet surface, such as an IPS wallet iframe, needs to resolve a bridge token that was minted by a partner external-club client. This keeps the partner external-club secret separate from the wallet/core client secret.

The resolving wallet app must have the wallet_embed scope enabled in Developers. In the app settings, enable the Wallet Embed checkbox.

Endpoint

POST /oauth/external-club/session/delegate/resolve

Tenant-prefixed deployments may also expose:

POST /:uiName/oauth/external-club/session/delegate/resolve

For production IPS wallet embeds, use:

POST https://www.thecrimsonmarket.com/mana/oauth/external-club/session/delegate/resolve

Authentication

Use HTTP Basic auth with the wallet/core OAuth client credentials:

  • clientId
  • clientSecret

The wallet/core client must be active and allowed to use wallet_embed.

Request body

{
  "token": "partner_external_club_bridge_token"
}

Validation behavior

  • Portal.Service verifies the bridge token signature and expiry.
  • The token must resolve to a real portal user.
  • That user must be linked to an external-club OAuth client.
  • The source external-club client must still be active and allowed to use external_club.
  • The resolving wallet/core client does not need to match the source external-club client, but it must have wallet_embed.

Success response

{
  "userId": "portal-user-id",
  "userName": "partner_member",
  "email": "[email protected]",
  "hubUserId": "ips-member-id",
  "delegatedTo": {
    "clientId": "wallet-core-client-id",
    "scope": "wallet_embed"
  },
  "externalClub": {
    "clubName": "charadescollective",
    "externalUserId": "charades-user-id",
    "hubClubId": "hub-3456",
    "oauthClientId": "charades-external-club-client-id"
  }
}

Example server request

const credentials = Buffer.from(`${walletClientId}:${walletClientSecret}`).toString("base64");

const response = await fetch(
  "https://www.thecrimsonmarket.com/mana/oauth/external-club/session/delegate/resolve",
  {
    method: "POST",
    headers: {
      Authorization: `Basic ${credentials}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      token: bridgeTokenFromIframe,
    }),
  },
);

This endpoint is intended for server-to-server wallet validation. Do not call it directly from browser code and do not expose the wallet/core client secret.

Resolve an embedded session token

Use this when your own backend receives a TCM session token from another page, embedded experience, or redirect-driven integration and needs to validate it before establishing a local session.

Endpoint

POST /oauth/external-club/session/resolve

Tenant-prefixed deployments may also expose:

POST /:uiName/oauth/external-club/session/resolve

Authentication

Use HTTP Basic auth with your OAuth client credentials:

  • clientId
  • clientSecret

The client must be active and allowed to use external_club.

Request body

{
  "token": "tcm_user_token"
}

Validation behavior

  • Portal.Service verifies the token signature and expiry.
  • The token must resolve to a real portal user.
  • That user must still be linked to the same external-club OAuth client that is calling the endpoint.
  • If the token is invalid, expired, or belongs to a user linked to a different client, the request is rejected.

Success response

{
  "userId": "portal-user-id",
  "userName": "partner_member",
  "email": "[email protected]",
  "externalClub": {
    "clubName": "partnerclub",
    "externalUserId": "partner-user-123",
    "hubClubId": "hub-3456",
    "oauthClientId": "tcm_xxx"
  }
}

Example server request

const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");

const response = await fetch(`${TCM_OAUTH_API_URL}/oauth/external-club/session/resolve`, {
  method: "POST",
  headers: {
    Authorization: `Basic ${credentials}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    token: tcmSessionToken,
  }),
});

This endpoint is intended for server-to-server validation. Do not expose the OAuth client secret or session resolution request directly from the browser.

OAuth userinfo enrichment

When a linked external-club user later authenticates with OAuth, Portal.Service can append partner-owned data under externalClubContext in /oauth/userinfo.

The enrichment call is optional. If no webhookEventEndpoint is stored, or the callback fails, normal OAuth login still succeeds and Portal.Service falls back to its normal userinfo behavior.

Signed callback request

Portal.Service signs the enrichment callback with the OAuth client secret stored for the linked user.

POST https://pokecharades.example.com/api/v1/public/external-club/user-context
x-external-club-client-id: tcm_xxx
x-external-club-timestamp: 1741702982
x-external-club-signature: <hmac_sha256_hex>
{
  "clubName": "charadescollective",
  "externalUserId": "113769124091223507890"
}

Resulting `/oauth/userinfo` shape

{
  "sub": "67d0...",
  "tcmid": "11735",
  "email": "[email protected]",
  "externalClubContext": {
    "clubName": "charadescollective",
    "externalUserId": "113769124091223507890",
    "externalClubUser": {
      "isSubscribed": true,
      "tierName": "Gold",
      "billingCycle": "monthly"
    }
  }
}

Operational notes

  • The enrichment webhook receives only partner lookup context and must return only partner-owned data.
  • The external_club scope does not add fixed userinfo fields by itself; it enables the provisioning and enrichment flow.
  • Keep the OAuth client secret server-side. It is required for provisioning requests and for signing webhook enrichment callbacks.
  • If you want to host the TCM catalog inside your own app, continue with Embedded Catalog Integration.
  • For general client creation and redirect URI setup, see OAuth App Setup.