Iframe Embeds

The iframe embed system lets you render REC-CAP assessments, goals, progress, and life domains directly inside your own application. Your users see an RCMS-powered experience that fits visually inside your product.

Why iframes (and when to use the API instead)

Iframes and the JSON API serve different purposes. Use the right one for each task:

TaskUse
Provision an organization, staff member, or clientJSON API
Take a REC-CAP assessment, review results, update goalsIframe embed
Sync enrollment status, receive event notificationsJSON API / Webhooks
Display a client's assessment history or progress chartIframe embed

Partners get continuous UI improvements for free — when RCMS adds a new chart or refines a workflow, your embed picks it up on the next page load with no work on your side.

The nine embed routes

Production base URL: https://embed.measurerecovery.com. Sandbox base URL: https://embed-sandbox.measurerecovery.com. Both subdomains are isolated from the main RCMS application — their own Content-Security-Policy, cookies scoped to the embed surface, and any non-embed path redirects to a denied page.

Context 1: Staff organization-level

Add these to your staff navigation sidebar. Scoped to an organization.

EmbedToken contextURL pathRequired permission
Assessments libraryorg_assessments/embed/org/:orgId/assessmentsassessments:read
Resourcesorg_resources/embed/org/:orgId/resourcesresources:read
Goal Templatesorg_goal_templates/embed/org/:orgId/goal-templatesgoal_templates:read

Context 2: Staff client-level tabs

Add these as tabs inside your client record view. Scoped to one client.:clientId is the RCMS client_uuid (returned by POST /v1/clients).

EmbedToken contextURL pathRequired permission
Assessments tabclient_assessments/embed/clients/:clientId/assessmentsassessments:read, assessments:write
Goals tabclient_goals/embed/clients/:clientId/goalsgoals:read, goals:write
Progress tabclient_progress/embed/clients/:clientId/progressprogress:read
Life Domains tabclient_life_domains/embed/clients/:clientId/life-domainslife_domains:read, life_domains:write

Context 3: Client portal

Embed these in your client-facing application. The token is scoped to the signed-in client — they see only their own data.

EmbedToken contextURL pathWhat the client sees
My Goalsclient_portal_goals/embed/client-portal/goalsActive goals, tasks, progress bars
Assessmentclient_portal_assessments/embed/client-portal/assessmentTake or resume their REC-CAP assessment
Progressclient_portal_progress/embed/client-portal/progressScore trend, domain breakdowns, history

Client-portal embeds require the client to have a portal account — invite them via POST /v1/clients/:clientId/invite before minting. Minting a client-portal token for a client without a portal account returns 409 precondition_failed.

The embed token flow

Iframes can't use your API key directly — that would expose it in the browser. Instead, your backend mints a short-lived embed token (a signed JWT) that authorizes a specific user to see a specific embed. The iframe URL carries the token; RCMS validates it server-side and enforces permissions via Row Level Security.

Partner backend                         RCMS API                  Embed iframe
     |                                     |                            |
     | POST /v1/embed-tokens               |                            |
     | (API key + context + identity)      |                            |
     | --------------------------------->  |                            |
     |                                     | Validate key, mint JWT      |
     |                                     | (30-min TTL)                |
     | <---------------------------------  |                            |
     | { token, embed_url, expires_in }    |                            |
     |                                     |                            |
     | Render <iframe src={embed_url}>     |                            |
     | ----------------------------------------------------------->      |
     |                                     |                            |
     |                                     | Validate JWT + enforce RLS  |
     |                                     | <------------------------  |
     |                                     | Render REC-CAP UI           |
     |                                     | -------------------------> |

Mint an embed token

curl -X POST https://api-sandbox.measurerecovery.com/v1/embed-tokens \
  -H "Authorization: Bearer rcms_sb_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "X-Organization-Id: <org_public_id>" \
  -H "Content-Type: application/json" \
  -d '{
    "context": "client_assessments",
    "staff_id": "<staff auth.users UUID>",
    "client_id": "<client_uuid>",
    "permissions": ["assessments:read", "assessments:write"]
  }'

Response:

{
  "data": {
    "token": "eyJhbGciOiJFUzI1NiIs...",
    "embed_url": "https://embed-sandbox.measurerecovery.com/embed/clients/<client_uuid>/assessments?token=eyJ...",
    "expires_in": 1800
  },
  "meta": {
    "request_id": "...",
    "timestamp": "2026-06-09T19:30:00Z"
  }
}

Request body reference

FieldTypeDescription
contextstring, requiredWhich embed to render. One of the 12 contexts listed above (e.g. client_assessments).
staff_idUUID, conditionalRequired for staff contexts (Contexts 1 & 2). The staff member's auth.users UUID — returned by POST /v1/staff as id.
client_idUUID, conditionalRequired for any client-scoped context. The client's client_uuid — returned by POST /v1/clients.
permissionsstring[], required for staff contextsEmbed permission scopes (see vocabulary below). Validated against the staff member's actual RCMS capabilities at mint time — you can never request more than the staff member has. Client-portal contexts ignore this field (the client is the auth identity).
themeobject, optionalVisual overrides. primary_color, logo_url, app_name. The hide_rcms_branding flag requires your key to have the white-label permission — contact support to enable.

Token TTL is fixed at 1800 seconds (30 minutes). The X-Organization-Id header is required when your API key is network-scoped (typical for partner integrations).

Permission vocabulary

Nine fine-grained read/write codes. Mint with the narrowest set the partner integration needs — minimum-privilege at the token level.

CodeGrants
assessments:readView completed assessments + scores
assessments:writeTake and submit assessments
goals:readView recovery plan + goals
goals:writeCreate, edit, complete goals & tasks
life_domains:readView life-domain check-ins
life_domains:writeRecord new life-domain check-ins
progress:readView score trends + history
resources:readBrowse org-curated resource library
goal_templates:readBrowse org-curated goal templates

Worked example: assessments tab inside a client record

A staff user opens their client record. You want to embed the RCMS Assessments tab inside your own page.

// Partner backend
const response = await fetch(
  "https://api.measurerecovery.com/v1/embed-tokens",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RCMS_API_KEY}`,
      "X-Organization-Id": orgPublicId,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      context: "client_assessments",
      staff_id: currentStaff.rcmsAuthUserId,
      client_id: rcmsClientUuid,
      permissions: ["assessments:read", "assessments:write"],
    }),
  },
);
const { data } = await response.json();
return { embed_url: data.embed_url };
<!-- Partner frontend -->
<iframe
  src={embed_url}
  title="REC-CAP Assessments"
  style={{ width: "100%", height: "800px", border: 0 }}
  allow="clipboard-read; clipboard-write"
/>

Worked example: client portal

A client logs in to your platform. You want to show their RCMS recovery goals.

// Partner backend (client_portal_* contexts don't take staff_id or permissions)
const response = await fetch(
  "https://api.measurerecovery.com/v1/embed-tokens",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.RCMS_API_KEY}`,
      "X-Organization-Id": orgPublicId,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      context: "client_portal_goals",
      client_id: signedInClient.rcmsClientUuid,
    }),
  },
);
const { data } = await response.json();

postMessage protocol

The embed iframe communicates with your parent app via window.postMessage. Always verify event.origin matches your embed hostname before acting on a message.

Refresh at 80% TTL

Around 24 minutes into the 30-minute token, the iframe posts rcms:token_refresh_needed. Your parent app re-mints via the API and posts the new token back:

window.addEventListener("message", async (event) => {
  if (event.origin !== "https://embed.measurerecovery.com") return;

  if (event.data?.type === "rcms:token_refresh_needed") {
    // 1. Re-mint via your backend (which holds the API key)
    const fresh = await fetch("/api/refresh-embed-token", { method: "POST" });
    const { token } = await fresh.json();

    // 2. Post the new token back to the iframe
    document.querySelector("iframe").contentWindow.postMessage(
      { type: "rcms:token_refresh", token },
      "https://embed.measurerecovery.com"
    );
  }

  if (event.data?.type === "rcms:token_error") {
    // Token validation failed — re-mint immediately
    refreshEmbedToken();
  }
});

Event reference

DirectionEventMeaning
iframe → parentrcms:token_refresh_neededToken is 80% through its TTL. Re-mint and post back via rcms:token_refresh.
iframe → parentrcms:token_errorToken is missing, malformed, expired, or rejected by Supabase. Payload includes code and message.
parent → iframercms:token_refreshNew token in token field. Iframe swaps the session in-place without reload.

Responsive sizing

RCMS UI is responsive and adapts to its container. Partners should:

What's guaranteed — and what isn't

Contract itemStable?
The 9 embed URL patterns + 12 contexts aboveYes — versioned, changes trigger a new major version
Embed token request/response shapeYes
Permission names (assessments:read, etc.)Yes
postMessage event namesYes
The set of features visible in each embedYes — we only add, never silently remove
Exact pixel layout, chart positions, widget orderNo — we iterate the UI
Colors, fonts, exact copyNo
New widgets or tabs added inside an embedAdded without notice — partners benefit automatically

Security model

Next steps

Getting Started

Request an API key and make your first call.

API Reference

Full request/response documentation for every endpoint.