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:
| Task | Use |
|---|---|
| Provision an organization, staff member, or client | JSON API |
| Take a REC-CAP assessment, review results, update goals | Iframe embed |
| Sync enrollment status, receive event notifications | JSON API / Webhooks |
| Display a client's assessment history or progress chart | Iframe 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 ten embed routes
All embeds are served from embed.measurerecovery.com — a dedicated subdomain with its own cookie, CSP, and security policies isolated from the main RCMS application.
Context 1: Staff organization-level
Add these to your staff navigation sidebar. Scoped to an organization.
| Embed | URL path | Required permission |
|---|---|---|
| Assessments library | /org/:orgId/assessments | assessments:read |
| Resources | /org/:orgId/resources | resources:read |
| Goal Templates | /org/:orgId/goal-templates | goal_templates:read |
Context 2: Staff client-level tabs
Add these as tabs inside your client record view. Scoped to one client.
| Embed | URL path | Required permission |
|---|---|---|
| Assessments tab | /clients/:clientId/assessments | assessments:read, assessments:write |
| Goals tab | /clients/:clientId/goals | goals:read, goals:write |
| Progress tab | /clients/:clientId/progress | progress:read |
| Life Domains tab | /clients/:clientId/life-domains | life_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.
| Embed | URL path | What the client sees |
|---|---|---|
| My Goals | /client-portal/goals | Active goals, tasks, progress bars |
| Assessment | /client-portal/assessment | Take or resume their REC-CAP assessment |
| Progress | /client-portal/progress | Score trend, domain breakdowns, history |
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 + embed_type + identity) | |
| ---------------------------------> | |
| | Validate key, mint JWT |
| | (30-60 min TTL) |
| <--------------------------------- | |
| { token, iframe_url, expires_at } | |
| | |
| Render <iframe src={iframe_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_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"embed_type": "clients/260/assessments",
"staff_user_id": "partner-staff-abc123",
"permissions": ["assessments:read", "assessments:write"],
"ttl_seconds": 1800
}'Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"iframe_url": "https://embed.measurerecovery.com/clients/260/assessments?token=eyJ...",
"expires_at": "2026-04-18T17:30:00Z",
"refresh_at": "2026-04-18T17:24:00Z"
}Request body reference
| Field | Type | Description |
|---|---|---|
| embed_type | string, required | Which embed to authorize (e.g. clients/260/assessments) |
| staff_user_id | string, conditional | Required for staff-context embeds (Contexts 1 & 2). Your identifier for the staff member. |
| client_user_id | string, conditional | Required for client-portal embeds (Context 3). Your identifier for the signed-in client. |
| permissions | string[], optional | Requested permission scopes. Defaults to the read-only set for the embed. |
| ttl_seconds | number, optional | Token lifetime. Default 1800 (30 min). Min 300, max 3600. |
Worked examples
Staff view: assessments tab inside a client record
Your staff user opens their client record for Agatha Harkness. 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}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
embed_type: `clients/${rcmsClientId}/assessments`,
staff_user_id: currentUser.id,
permissions: ["assessments:read", "assessments:write"],
ttl_seconds: 1800,
}),
},
);
const { iframe_url } = await response.json();
return { iframe_url };<!-- Partner frontend -->
<iframe
src={iframe_url}
title="REC-CAP Assessments"
style={{ width: "100%", height: "800px", border: 0 }}
allow="clipboard-read; clipboard-write"
/>Client portal: embed “My Goals” in your app
A client logs in to your platform. You want to show their RCMS recovery goals.
// Partner backend
const response = await fetch(
"https://api.measurerecovery.com/v1/embed-tokens",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RCMS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
embed_type: "client-portal/goals",
client_user_id: session.rcmsClientId,
permissions: ["goals:read", "goals:write"],
ttl_seconds: 1800,
}),
},
);
const { iframe_url } = await response.json();Responsive sizing
RCMS UI is responsive and adapts to its container. Partners should:
- Use
width: 100%on the iframe so it fills its parent - Set a generous minimum height (we recommend
800px) to avoid scrollbars for typical screens - Listen for the
rcms:resizepostMessage event if you want to auto-size (coming soon) - Do not fix the iframe to a specific pixel height — RCMS UI changes continuously and a fixed height will cause content to be cut off
What's guaranteed — and what isn't
| Contract item | Stable? |
|---|---|
| The 10 embed URL patterns above | Yes — versioned, changes trigger a new major version |
| Embed token request/response shape | Yes |
| Permission names (assessments:read, etc.) | Yes |
| The set of features visible in each embed | Yes — we only add, never silently remove |
| Exact pixel layout, chart positions, widget order | No — we iterate the UI |
| Colors, fonts, exact copy | No |
| New widgets or tabs added inside an embed | Added without notice — partners benefit automatically |
Security model
- Short-lived tokens. 30-minute TTL by default. If permissions change after a token is minted, a staff member retains the old permissions until the token expires.
- Refresh at 80% expiry. Use the
refresh_attimestamp in the response. Mint a new token before the old one expires; the iframe stays alive through a token swap. - Emergency revoke.If you need to cut off access immediately (offboarded staff member, compromised account), deactivate the user via your backend; RCMS's Row Level Security will deny access at the database layer regardless of outstanding tokens.
- Token stays server-side until the iframe loads. Mint the token in your backend, pass only the
iframe_urlto the browser. Never expose API keys in client-side JavaScript. - Same-origin isolation. Embeds run on
embed.measurerecovery.com, separate from the main app domain. Iframe cookies and CSP are scoped to that subdomain. - RLS is the final boundary.Even if a JWT's claims were wrong or tampered with, RCMS's database RLS policies re-verify authorization on every query. Two layers of defense.
Error handling inside the iframe
If the token is missing, invalid, or expired, the iframe renders a friendly error page that tells the user to refresh. You'll also receive a rcms:token_error postMessage event that your parent app can listen for:
window.addEventListener("message", (event) => {
if (event.origin !== "https://embed.measurerecovery.com") return;
if (event.data?.type === "rcms:token_error") {
// Mint a fresh token and swap iframe src
refreshEmbedToken();
}
});