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.

Machine-to-Machine Service Clients

Create a client_credentials service client, mint a userless token with the right audience, and call another backend on a user's behalf with X-User-Sub.

Local portal docs

A service client lets one backend call another without a logged-in user. It uses the OAuth client_credentials grant to mint a short-lived, userless token. Every token carries exactly one audience (aud) claim identifying the single resource server it may call — but one client identity can be permitted to request several audiences and pick the target per token (see The audience contract). This is the mechanism behind cross-service calls like Lucky Little Devil registering a brand candidate in megaphone, or megaphone fetching a brokered platform token from connect.

When to use a service client

Use one when a server needs to act on its own behalf, or on a user's behalf from a context where no user token exists — a queue worker, a cron job, a webhook handler, a server-to-server API call.

Do not use one for browser login. User-facing apps use a normal OAuth app client (the authorization-code flow). See Registering OAuth Applications.

How a service client differs from a normal app client:

  • Grant — a service client uses client_credentials; an app client uses authorization_code + refresh_token.
  • Who logs in — nobody logs into a service client; it *is* the backend. An app client logs in a user in a browser.
  • Token subject — a service token's sub is service:<serviceName>; an app token's sub is the user's id.
  • Redirect URIs — a service client has none; an app client requires them.
  • Audience — a service token is scoped to the one resource server it calls; an app token is scoped to the app.
  • Created by — service clients are platform-admin-only; app clients are self-serve for any developer.

Creating a service client

Service clients are platform infrastructure, so creation is admin-only. From the developer portal, open Service clients → Create service client (the nav item appears only for platform admins).

You provide:

  • Name — a human label.
  • Service name — a lowercase slug (e.g. megaphone, lld-worker). It becomes the token subject sub = service:<serviceName> and must be unique.
  • Default audience — the resource server this client's tokens target by default, chosen from the registry (e.g. service.connect, service.megaphone). It is stamped as aud when a token is requested with no resource parameter. Pick Custom… only for a target not yet in the registry.
  • Additional audiences — optional. Other registry resource servers this same client may also call. It selects the target per token via the OAuth resource parameter (RFC 8707); each token still carries exactly one aud. Leave empty for a single-target client.
  • Scopes — constrained to the union of the scopes meaningful across the selected audiences (default + additional). A pure-audience target (one that authorizes on aud alone) contributes no scopes.

On creation the client secret is shown once. Copy it immediately and store it server-side only. Any admin can later rotate the secret, edit the audience/scopes, or delete the client from its detail page; deleting it immediately revokes all of its tokens.

Minting a token

Exchange the client id + secret at the token endpoint using HTTP Basic auth. OAuth endpoints are relative to the Mana API base URL:

Development: https://dev.portal.raum.au/mana
Production:  https://www.thecrimsonmarket.com/mana
curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  https://www.thecrimsonmarket.com/mana/oauth/token
{
  "access_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "connect:read",
  "aud": "service.connect"
}

The token's sub is service:<serviceName> and its aud is the client's default audience. There is no refresh token — mint a fresh one when it expires (cache it until then).

Requesting a specific audience

If the client is permitted more than one audience, choose the target for a given token with the resource parameter (RFC 8707). It must be the default audience or one of the additional audiences, otherwise the request fails with invalid_target.

curl -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=client_credentials \
  -d resource=service.connect \
  https://www.thecrimsonmarket.com/mana/oauth/token
# -> aud=service.connect

Omit resource to get the default audience. The returned token still carries a single aud.

With the shared SDK

@crimsoncorp/connect-client ships a token provider that mints, caches, and refreshes the token for you:

import { createServiceTokenProvider } from '@crimsoncorp/connect-client';

const getAccessToken = createServiceTokenProvider({
  tokenUrl: process.env.PORTAL_TOKEN_URL, // https://www.thecrimsonmarket.com/mana/oauth/token
  clientId: process.env.MEGAPHONE_CLIENT_ID,
  clientSecret: process.env.MEGAPHONE_CLIENT_SECRET,
  scope: 'connect:read connect:write',
});

const token = await getAccessToken(); // cached + refreshed ahead of expiry

The provider forwards its optional audience as the resource parameter, so a multi-audience client creates one provider per target (each has its own cache):

const getConnectToken = createServiceTokenProvider({ ...common, audience: 'service.connect' });
const getMegaphoneToken = createServiceTokenProvider({ ...common, audience: 'service.megaphone' });

The audience contract

The audience is what stops a token minted for one service from being replayed at another. Every resource server checks the incoming token's aud against its own id and rejects a mismatch:

  • A token with aud=service.connect is accepted by connect and rejected everywhere else.
  • The convention for the audience value is service.<name> (e.g. service.connect, service.megaphone).

Each token carries exactly one aud, so it is accepted by exactly one resource server. A single client, however, may be permitted to request several audiences: it has one default audience plus any number of additional audiences, and picks the target per token with the resource parameter. So a backend that calls two services (like lld → both connect and megaphone) uses one service client with both audiences — no need for a separate client and secret per target. The permitted set is shown on the client's detail page.

A common cause of "401 invalid token audience" is a token whose aud doesn't match the server it's calling — confirm you requested the right resource, and that the target is in the client's permitted audiences on its detail page.

Delegation: acting on a user's behalf

A service token is userless, but a backend often needs to act for a specific user — for example, a worker publishing a clip to *that player's* connected socials. Pass the user's TCM sub in an X-User-Sub header alongside the service token:

Authorization: Bearer <service token, aud=service.connect>
X-User-Sub: <the target user's TCM sub>

The resource server honors X-User-Sub only for allowlisted service clients carrying the right scope (for connect, connect:read), and ignores it otherwise — impersonation is a deliberate, restricted capability, not a default. With the SDK:

const connections = await client.listConnectionsForUser(userSub, { platform: 'youtube' });
const { access_token } = await client.getTokenForUser(userSub, connectionId);

Worked example: LLD → megaphone

Lucky Little Devil uses one service client to reach two services — it reads a player's social connections from connect and asks megaphone to publish/curate. It is created here (Service clients section) with default audience service.megaphone and additional audience service.connect, scopes connect:read connect:write.

When a game finishes and its Short publishes, LLD registers the clip as a brand-curation candidate in megaphone — server to server, no user present:

  1. Its candidate worker mints a megaphone-targeted token (createServiceTokenProvider({ audience: 'service.megaphone' }), cached) and POSTs to megaphone's /api/service/candidates with:

``text Authorization: Bearer <token, aud=service.megaphone> X-User-Sub: <the clip owner's TCM sub> ``

  1. megaphone validates the audience, reads X-User-Sub as the publishing identity, and records the candidate.
  2. For connect calls (e.g. listing the player's connections) the same client mints a separate token with resource=service.connect — a different aud from the same identity.

If megaphone is down the call simply retries on its queue — the service-client contract (mint → call → validate aud) is the same regardless of the payload.

Operational notes

  • Treat the client secret like any other server credential: store it server-side, never in the browser, and rotate it on suspected exposure (rotation invalidates the previous secret immediately).
  • The audiences and scopes are editable after creation, but changing the default audience or removing an additional one repoints or narrows where the client can call — only do it deliberately.
  • Every platform admin can see and manage every service client; the creating admin is recorded for audit.