Skip to content

External embeds: workflow runtime and editor

This document is the canonical integration guide for embedding Madoo in external systems. It covers both browser surfaces:

  • Workflow runtime embed: an end-user widget that runs one published workflow.
  • Workflow editor embed: an embedded editor that can create, save, edit, and run workflows in a workspace.

Use this document whenever code changes touch embed token creation, embed authentication, iframe routes, or postMessage contracts.


External integrations must never expose a Madoo API key or OAuth client secret in the browser. The host application uses its backend to mint a narrow embed token, then passes only that embed token to the iframe.

External backend External browser Madoo
---------------- ---------------- -----
POST /api/v1/auth/token
client_id + client_secret
|
v
POST /api/v1/embed/.../tokens -> returns embed token
|
v
Render iframe with token -> /embed/... or /embed/editor
|
v
Communicate with iframe -> window.postMessage(...)

There are two token types:

Token type Endpoint JWT subject Scope Main use
Runtime embed token POST /api/v1/embed/tokens embed One workflow, optional interface, org, workspace, allowed origins End-user workflow execution
Editor embed token POST /api/v1/embed/editor/tokens embed_editor Org, workspace, roles, permissions, allowed origins Embedded workflow authoring

Both token types are JWTs signed by Madoo. Both can be revoked by JTI through the same revocation endpoint.

Declarative Apps: this contract still covers only workflow runtime and workflow editor embeds. Declarative Apps are not embeddable in V1. Their manifest uses minimumRendererVersion internally to reject unsupported layout capabilities safely; it does not change the URLs, tokens, or postMessage messages described here. A future OR-14 App embed must define its own explicit version handshake instead of implicitly reusing this workflow contract.

The Declarative App editor’s explained hidden-element placeholders are also an internal authoring adapter. They do not change iframe rendering, embed tokens, postMessage, or customer-runtime visibility: surfaces without that adapter continue to omit conditionally hidden elements completely.

The contextual Logic panel and authoring contract 2.21 are likewise internal App-authoring surfaces. Editing an operation’s declarative success outcomes changes the next hosted App manifest after save/publish, but adds no iframe command, token claim, external URL, or postMessage vocabulary to this V1 embed contract.

The editor’s copied /apps/{appId} URL is not an external share or embed URL. It opens the hosted Declarative App route for an authenticated user whose current workspace grants App use access. Anonymous and cross-workspace customer distribution remain outside the current contract.


First obtain a normal Public API bearer token with client credentials:

Terminal window
BASE_URL="https://testing-api.madoo.ai"
TOKEN=$(curl -s -u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials" \
"$BASE_URL/api/v1/auth/token" | jq -r .access_token)

The API key behind CLIENT_ID / CLIENT_SECRET determines the organization and workspace used by the embed token. The workspace is not accepted from the browser or from the embed-token request body.

Required permissions:

Operation Required permission
Create runtime workflow token ws:executions:create (WsExecutionsCreate)
Create editor token ws:workflows:manage (WsWorkflowsManage)
Run from embedded editor ws:executions:create must also be present in the embedded context
Save/create workflows from embedded editor ws:workflows:manage must be present in the embedded context

For runtime tokens, Madoo copies only the narrow permissions needed by the iframe, and only when the current API key / request context already has them: ws:workflows:read for workflow metadata, ws:assets:read for reading runtime assets, ws:assets:manage for runtime asset upload/management, ws:executions:create for starting workflow runs, and ws:executions:read for polling runtime execution status and outputs. This lets the runtime iframe access the required APIs without broadening privileges.

An API key’s AllowedIPs also applies to requests made with its API-key-sourced embed tokens. Browser requests are checked against their own resolved client IP. Behind a configured trusted edge this is the same address used by token exchange and rate limiting (Api:RateLimiting:ClientIpHeader, trusted only through the recorded local proxy); otherwise it is the forwarded remote IP. User-sourced portal tokens have no parent API-key allowlist.

What an embed token can call. An embed token sits in a page anyone can open, so Madoo limits it to what its iframe needs, whatever its permissions say:

  • A runtime token reaches only the routes of the runtime iframe: the embed bootstrap (/api/embed/*), the definition and interface of its own workflow, starting a run of that workflow (and of its interface, when the token names one), reading, polling and cancelling the runs it started, the catalog its fields use, and input uploads. Another workflow of the workspace answers 404; any other route answers 403 EMBED_TOKEN_SURFACE.
  • An editor token reaches the authoring surfaces of the embedded editor (workflows, runs and their execution details, assets, catalog, JSON schemas, templates), never identity, members, API keys, billing or notifications (403 EMBED_TOKEN_SURFACE).
  • No embed token can be exchanged for a session token.

For editor tokens, Madoo copies the parent API-key context into the embed token: owner user id, email verification state, organization role, workspace role, and explicit permissions. This is intentional: the embedded editor calls normal workspace APIs, so normal authorization middleware must be able to resolve the same effective permissions.


Use a runtime embed when an external end user should fill workflow inputs and generate outputs, without seeing the Madoo editor.

POST /api/v1/embed/tokens
Authorization: Bearer <public_api_access_token>
Content-Type: application/json

Request body:

Field Type Required Notes
workflow string yes Published workflow id. Accepts wf_... or a raw GUID.
allowed_origins string[] yes External origins allowed to host the iframe, for example https://app.example.com.
interface string or null no Custom interface id. Omit or null for the default workflow interface.
expires_in_minutes integer or null no >= 1. Null means practical never-expiry (100-year JWT expiry).
max_executions integer or null no >= 1. Null means unlimited. Counts the runs the token started: a request refused before its run starts (invalid or missing input, an asset that is gone, not enough credits, a usage limit) spends nothing, and a retry with the same idempotency key spends nothing more.
prefilled_inputs object or null no Reserved in the token contract; runtime UI initialization should currently use madoo:init / madoo:setValues.
end_user_id string or null no Max 128 chars, cannot start with embed:. Groups the runs of one of your users: the token sees the runs of that end user. Without it, the token sees only the runs it started.

Example:

Terminal window
WF="wf_53fb2f6576fa4bc9a6d3c91a7e84de47"
curl -s -X POST "$BASE_URL/api/v1/embed/tokens" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workflow": "'"$WF"'",
"interface": "simple",
"allowed_origins": ["https://app.example.com"],
"expires_in_minutes": 60,
"max_executions": 25,
"end_user_id": "customer-10472"
}'

Response:

{
"token": "eyJhbGciOi...",
"jti": "emb_7f2e...",
"expires_at": "2026-06-18T15:30:00+00:00"
}

Store jti server-side if you may need to revoke the token before expiry.

Runtime embed route:

{APP_BASE_URL}/embed/{workflowGuid}/{interfaceId?}?token={embed_token}

Supported query parameters:

Parameter Values Notes
token JWT Required. Runtime embed token returned by POST /api/v1/embed/tokens.
interface string Optional fallback interface id when not present in the path.
theme light, dark Optional initial theme class.
primaryColor CSS color Optional initial primary/accent color, applied before iframe readiness.
locale locale code Optional, for example it or en. Unsupported locales fall back to English.
layout default, compact, inline, headless Optional UI layout.
compact true Shortcut for compact layout.
hideHeader true Hide the workflow/interface header.
hideExecuteButton true Reserved by init payload; not all interface renderers expose a separate button.
hidePoweredBy true Hide the powered-by label.
hideProgress true Hide default progress block when applicable.
hideOutput true Hide progress/output area.
showName false Hide the workflow/interface name while keeping the rest of the embed UI.
showBranding false Hide Madoo branding references, including the powered-by label.

Custom CSS should be sent with postMessage (madoo:init payload css or madoo:setCustomCss) after the iframe loads. Do not place full CSS in the URL.

Example:

<iframe
id="madoo-runtime"
src="https://testing-portal.madoo.ai/embed/53fb2f65-76fa-4bc9-a6d3-c91a7e84de47/simple?embed=true&token=EMBED_TOKEN&locale=it&layout=compact"
style="width:100%;border:0;"
allow="clipboard-read; clipboard-write"
></iframe>

The hosted SDK can create and manage the runtime iframe for you: It always appends embed=true to the iframe URL so Madoo runs in embed mode and does not trigger the normal refresh-token flow.

<div id="madoo-runtime"></div>
<script src="https://testing-portal.madoo.ai/embed/sdk/madoo-embed.js"></script>
<script>
const runtime = MadooEmbed.create({
container: "#madoo-runtime",
workflow: "53fb2f65-76fa-4bc9-a6d3-c91a7e84de47",
interface: "simple",
token: "EMBED_TOKEN",
locale: "it",
layout: "compact",
customCss: ".madoo-embed { font-family: Inter, sans-serif; }",
onExecutionCompleted: (data) => console.log(data.outputs),
});
</script>

If your backend already returns the clean runtime embed URL, pass it as iframeUrl and pass the token separately:

MadooEmbed.create({
container: "#madoo-runtime",
iframeUrl: "https://testing-portal.madoo.ai/embed/53fb2f65-76fa-4bc9-a6d3-c91a7e84de47",
token: "EMBED_TOKEN",
});

The runtime page bootstraps by calling:

Internal call Purpose
GET /api/embed/config Validates token, resolves workflow GUID to internal workflow id, returns metadata and token execution cap.
POST /api/embed/check-execution When max_executions is set, tells whether the token has runs left. It spends nothing: each run spends one when it is created, a request refused before its run starts gives it back, and a run past the cap answers 429 execution_cap_exceeded.
Standard workflow/execution APIs Load the definition/interface of the token’s workflow, create a run of it, poll its own runs. The embed token is used as bearer auth (see “What an embed token can call”).

If a design/template_render node has the opt-in latest_compatible policy, a new runtime embed execution can select a compatible published template revision. Its effective pin is stored in that execution’s immutable definition snapshot. The runtime embed does not save or version the workflow. A permanent pin change belongs to authoring in the embedded editor or another authorized authoring surface; a released App binding remains frozen.

EmbedAuthMiddleware validates runtime tokens on /embed/* and /api/embed/*, sets a scoped principal, stores EmbedClaims in HttpContext.Items, and adds Content-Security-Policy: frame-ancestors 'self' ... for embed page requests.

Parent pages send commands to the iframe with:

iframe.contentWindow?.postMessage(
{
type: "madoo:init",
payload: { locale: "it", values: { prompt: "Summer campaign" } },
},
"https://testing-portal.madoo.ai",
);

Supported parent-to-iframe commands:

Message type Payload Effect
madoo:init EmbedInitPayload Applies initial theme, locale, CSS, values, visibility flags, and layout.
madoo:setValues object Sets input values by field key. Values are JSON-encoded internally by the iframe.
madoo:execute none Starts the workflow execution with current inputs.
madoo:reset none Clears execution state and input values.
madoo:setTheme EmbedTheme Applies CSS variables and light/dark mode.
madoo:setLocale { "locale": "it" } Changes iframe language.
madoo:setCustomCss { "css": "..." } Injects sanitized custom CSS. Blocks url(...), @import, javascript:, and expression(...).

Values preserve their JSON scalar type. In particular, an input/number field must receive a finite JavaScript number such as 60, not the string "60"; an input/boolean field must receive true or false, not text. This is the same strict contract used by editor runs, REST v1, MCP, AI Agent and AI Assistant. It lets reusable workflows safely expose controls such as highlight duration and optional subtitle generation without surface-specific coercion.

An input/json_value field receives one complete JavaScript object, array or scalar and preserves it as one structured value, including when the runtime stores a large payload outside the execution row. It does not create iterations. Use input/json only when the configured array members must fan out into separate executions. The distinction is identical in an embedded runtime and in a direct REST v1 call.

EmbedInitPayload shape:

type EmbedInitPayload = {
theme?: EmbedTheme;
locale?: string;
values?: Record<string, unknown>;
css?: string;
lockedFields?: string[];
hiddenFields?: string[];
hideHeader?: boolean;
hideExecuteButton?: boolean;
hidePoweredBy?: boolean;
hideProgress?: boolean;
hideOutput?: boolean;
showName?: boolean;
showBranding?: boolean;
compact?: boolean;
layout?: "default" | "compact" | "inline" | "headless";
};

EmbedTheme shape:

type EmbedTheme = {
mode?: "light" | "dark" | "auto";
primaryColor?: string;
backgroundColor?: string;
surfaceColor?: string;
textColor?: string;
mutedColor?: string;
borderColor?: string;
successColor?: string;
errorColor?: string;
borderRadius?: string;
fontFamily?: string;
};

The iframe posts events to the parent with { type, payload }, addressed only to the allowed origin of the token that hosts it (never '*').

Always validate event.origin in the parent page before trusting the payload.

The runtime and editor iframes accept a command (madoo:init, madoo:setValues, madoo:execute, madoo:editor:save, …) only when both of these hold:

  • it comes from the window that contains the iframe (event.source === window.parent);
  • its origin is one of the token’s allowed_origins.

Messages from any other window or origin are ignored silently, including a page that opened your site in a popup and reached the iframe through popup.frames[0]. Send commands from the page that embeds the iframe, on an origin listed in allowed_origins, with Madoo’s exact target origin. A token without allowed origins accepts no command at all.

When the browser exposes the hosting page (location.ancestorOrigins: Chrome, Edge, Safari), an iframe framed by a site that is not in allowed_origins does not render (“This content cannot be shown on this site.”). Where the browser does not tell (Firefox), the command checks above still apply.

window.addEventListener("message", (event) => {
if (event.origin !== "https://testing-portal.madoo.ai") return;
const { type, payload } = event.data ?? {};
if (type === "madoo:execution:completed") {
console.log(payload.outputs);
}
});

Runtime iframe-to-parent events:

Message type Payload When emitted
madoo:ready { version, fields } Config and workflow definition are loaded.
madoo:resize { width, height } Content size changes. Use it to adjust iframe height.
madoo:values:changed { fieldKey, value, allValues } A field value changes.
madoo:validation { valid, errors } Validation event. Reserved for validation-capable UI paths.
madoo:execution:started { executionId } Execution was created.
madoo:execution:progress { executionId, progress, status } Execution status/progress changed while non-terminal.
madoo:execution:completed { executionId, outputs } Execution completed or partially succeeded.
madoo:execution:failed { executionId, error, code } Execution failed or was cancelled.
madoo:error { code, message } Runtime command or execution startup error.

madoo:error codes. message is a human, end-user-facing string, safe to display as-is; where Madoo produces a localized message (e.g. missing_required_inputs, invalid_execution_inputs) it follows the embed locale. Known code values: missing_required_inputs (a required input was left empty; message names the field(s) to fill in), invalid_execution_inputs (a provided input is not accepted — an unrecognized key, a value that is not JSON-encoded, or text sent to a number or boolean input; message names the offending input(s)), upload_pending (an asset upload is still in progress — wait, then retry), execution_cap_exceeded (the embed’s execution limit was reached), credit_cap_reached (a daily or monthly usage limit of the embedding workspace or organization is reached; message says which, the usage and when runs resume, in local time — adding credits does not help), insufficient_credits (the organization’s balance cannot cover the run), ai_spend_paused (an AI safety limit was reached — the message says when AI resumes — or Madoo has temporarily paused AI generation for the platform, the plan, the organization or the workspace: nothing ran and nothing was charged — retry later; a run whose AI step meets a pause later sends madoo:execution:failed with code AI_SPEND_PAUSED), and execution_failed (any other startup error). A run stopped by a usage limit while running sends madoo:execution:failed with code CREDIT_CAP_REACHED. Branch on code for custom handling; new codes may be added over time, so treat unknown codes as a generic error.

The hosted runtime and embedded editor present a concise error summary in their primary UI. Low-level provider bodies, FFmpeg diagnostics, memory addresses and local worker paths are never rendered as the headline; in the editor they remain available only inside an explicit Technical details disclosure for authorized troubleshooting. Integrators should apply the same summary-first pattern to any error payload they render themselves.

Video previews and seeking. Madoo-provided video URLs support browser byte-range requests, so an embedded native <video> can start from metadata and seek without downloading the whole asset first. If an integrator proxies an output URL through its own server or CDN, that layer must preserve the request Range header and the 206 Partial Content, Content-Range and Accept-Ranges: bytes response semantics. A custom player cannot compensate for a proxy that collapses range requests into full 200 OK responses.

Skipped outputs. Each entry in payload.outputs carries a presence of "present" or "absent". An "absent" output was skipped because an optional input was not provided (or an upstream node was skipped); it has no url/value. The run still completes successfully — check presence before rendering, and skip the absent ones. See Reading outputs › Optional inputs & skipping.

Iterative runs. A run whose output nodes iterate (fan-out over a list) delivers one entry per iteration in payload.outputs — potentially dozens or hundreds. Entries carry a logical_name (the author-facing output name) alongside the unique name ({logical}_{iteration}_{nodeId}): group by logical_name when rendering. Runs executed before this capability shipped return an empty list for iterative outputs.

Auto-resize example:

const iframe = document.querySelector<HTMLIFrameElement>("#madoo-runtime");
window.addEventListener("message", (event) => {
if (event.origin !== "https://testing-portal.madoo.ai") return;
if (event.data?.type !== "madoo:resize") return;
iframe!.style.height = `${event.data.payload.height}px`;
});

Use an editor embed when an external application should let a workspace user author workflows inside your own UI.

POST /api/v1/embed/editor/tokens
Authorization: Bearer <public_api_access_token>
Content-Type: application/json

Request body:

Field Type Required Notes
allowed_origins string[] yes Origins allowed to host the editor iframe.
expires_in_minutes integer or null no >= 1. Null means practical never-expiry.
end_user_id string or null no Max 128 chars, cannot start with embed:. Audit/correlation id for the external user.

Example:

Terminal window
curl -s -X POST "$BASE_URL/api/v1/embed/editor/tokens" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"allowed_origins": ["https://admin.example.com"],
"expires_in_minutes": 60,
"end_user_id": "seller-user-42"
}'

Response:

{
"token": "eyJhbGciOi...",
"jti": "emb_ed_9c1a...",
"expires_at": "2026-06-18T15:30:00+00:00"
}

The editor token includes:

Claim group Purpose
uid, org_id, ws_id, aki Normal identity, workspace context, and parent API-key traceability.
email_verified Lets email verification middleware see the embedded owner as verified when the API-key owner is verified.
org_role, ws_role, permissions Lets normal authorization checks work inside the embedded editor.
allowed_origins The only sites that may host the editor iframe and command it (see §3.5); events go only to them.
end_user_id External audit/correlation identifier.

Editor route:

{APP_BASE_URL}/embed/editor?token={editor_token}

The token is mandatory: without it the editor shows “Missing editor token.” and never falls back to a Madoo portal session open in the same browser.

Supported query parameters:

Parameter Aliases Notes
token editorToken Editor embed token.
workflowExternalId workflow_external_id, workflow External workflow id to open, for example wf_....
name workflowName, workflow_name Initial workflow name for create flows when workflowExternalId is omitted.
theme - Optional initial theme class.
primaryColor - Optional initial primary/accent color, applied before iframe readiness.
locale - Initial locale. Unsupported locales fall back to English.
showName - Pass false to hide the workflow name in the embedded editor toolbar.
showBranding - Pass false to hide Madoo branding references in the embedded editor shell.

Example:

<iframe
id="madoo-editor"
src="https://testing-portal.madoo.ai/embed/editor?embed=true&token=EDITOR_TOKEN&workflowExternalId=wf_abc123&locale=it"
style="width:100%;height:900px;border:0;"
allow="clipboard-read; clipboard-write"
></iframe>

The hosted SDK exposes an editor bridge with the same iframe + postMessage model as the runtime: It always appends embed=true to the iframe URL so Madoo runs in embed mode and does not trigger the normal refresh-token flow.

<div id="madoo-editor"></div>
<button id="save-workflow">Save</button>
<script src="https://testing-portal.madoo.ai/embed/sdk/madoo-embed.js"></script>
<script>
const editor = MadooEmbed.editor.create({
container: "#madoo-editor",
token: "EDITOR_TOKEN",
workflow: "wf_abc123", // for create flows, omit workflow and pass name instead
// name: "Campaign workflow",
locale: "it",
customCss: ".madoo-editor-embed { font-family: Inter, sans-serif; }",
onSaved: (workflow) => console.log(workflow.workflowExternalId),
onSaveFailed: (error) => console.error(error.message),
});
document.querySelector("#save-workflow").addEventListener("click", () => {
editor.save();
});
</script>

If your backend returns a clean editor URL and workflow identity separately, pass those to the bridge instead of adding query parameters yourself:

MadooEmbed.editor.create({
container: "#madoo-editor",
editorUrl: "https://testing-portal.madoo.ai/embed/editor",
token: "EDITOR_TOKEN",
workflowExternalId: "wf_abc123",
showName: false,
showBranding: false,
});

The bridge builds the iframe URL for token/workflow initialization, listens for madoo:editor:ready, then applies theme/CSS through postMessage. If constructing the iframe manually, token and workflow initialization are query-string based. Theme can be passed initially with theme or updated after load with madoo:editor:setTheme. Custom CSS should be sent after madoo:editor:ready with madoo:editor:setCustomCss. Do not place full CSS in the URL.

When workflowExternalId is omitted, the parent must provide name. The workflow is only created when the parent sends madoo:editor:save or the user runs the workflow. The embedded editor does not show a Save button; hosts that need an explicit save action should render it outside the iframe and send madoo:editor:save. Missing name is treated as an initialization error.

For a design/template_render node, an embedded editor save or run that selects the current template revision requires a Published DesignDocument. An already pinned immutable revision remains usable if its source template later returns to draft or is archived. A rejected current selection returns through the existing madoo:editor:saveFailed path; the iframe message contract is unchanged. When the author chooses another published revision in the embedded editor, the same review dialog as the full editor shows added, removed and changed placeholder fields, new required fields, connected-field risks, page count and whether visual content changed. The author confirms the manual pin change; opening the workflow never switches revisions implicitly. This review is informational. The editor saves against the definition ETag it read on load. If another authoring surface changes the workflow first, the save returns a 412 precondition failure and the iframe emits madoo:editor:saveFailed; the author must reload and review the newer definition before trying again. An execution request does not silently write the template pin. The publisher also rejects a bare numeric version ID on a template outside Published status unless the saved workflow pin carries the matching document/version identity and hashes.

After an editor run completes or partially succeeds, the embedded editor opens its final-results gallery when at least one workflow output exists (unless the user disabled automatic opening in the editor’s execution menu). Closing the gallery returns to the executed canvas without clearing its node states. Failed and cancelled runs do not open the gallery automatically. This is an iframe-local presentation behavior and does not add or change any postMessage event.

The editor iframe posts madoo:editor:ready after installing its message listener. Wait for this event before sending commands.

Accepted parent-to-editor commands:

Message type Payload Effect
madoo:editor:save { "requestId"?: string } Saves current workflow and publishes it before emitting madoo:editor:saved. If no workflow exists yet, uses the name provided during initialization. Echoes requestId in the response event.
madoo:editor:run none Saves when needed and runs the current workflow.
madoo:editor:setTheme EditorEmbedTheme Applies CSS variables and light/dark mode.
madoo:editor:setCustomCss { "css": "..." } Injects sanitized custom CSS.

Example:

const editor = document.querySelector<HTMLIFrameElement>("#madoo-editor");
window.addEventListener("message", (event) => {
if (event.origin !== "https://testing-portal.madoo.ai") return;
if (event.data?.type !== "madoo:editor:ready") return;
editor!.contentWindow?.postMessage(
{
type: "madoo:editor:setTheme",
payload: {
mode: "light",
primaryColor: "#0f766e",
borderRadius: "6px",
fontFamily: "Inter, sans-serif",
},
},
"https://testing-portal.madoo.ai",
);
});

Editor iframe-to-parent events:

Message type Payload When emitted
madoo:editor:ready { version, acceptedMessages } Message listener is ready.
madoo:editor:saved WorkflowEditorSavedPayload Workflow save succeeds.
madoo:editor:saveFailed { message, requestId? } Workflow save fails.

The saved payload is produced by WorkflowEditorSurface and includes the saved workflow identity and name. When save was triggered by madoo:editor:save, the payload also includes the request id supplied by the parent. Treat the exact shape as part of the frontend contract and update this document whenever WorkflowEditorSavedPayload changes.


The editor’s Embed code panel (Interfaces -> an interface -> Embed) makes a runtime code without an API key. It acts as the user who made it, and follows these rules:

  • Sites are required. At least one site address (https://www.example.com, http://localhost:3000 for tests); each is reduced to scheme, host and port. The widget is shown only there.
  • Expiry. 30 days, 90 days, 1 year (default), or never — only when chosen.
  • It lives with its creator’s access. The code works while the user who made it can still manage the workspace’s workflows. Removing that person from the workspace or the organization, or giving them a role that cannot manage workflows, stops their codes at once; the members pages say how many codes are affected before a removal.
  • Active codes. The panel lists the workflow’s active codes (made in the portal or with an API key), with who made each, its sites and expiry, and revokes any of them at once.

The same rules hold for the internal API the panel uses (GET/POST /api/embed-tokens, DELETE /api/embed-tokens/{jti}); servers keep minting their tokens with an API key (§3.1).


Runtime and editor embed tokens use the same revocation endpoint:

DELETE /api/v1/embed/tokens/{jti}?expires_at={iso8601}
Authorization: Bearer <public_api_access_token>

Examples:

Terminal window
curl -s -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/v1/embed/tokens/emb_7f2e...?expires_at=2026-06-18T15:30:00Z"
curl -s -X DELETE \
-H "Authorization: Bearer $TOKEN" \
"$BASE_URL/api/v1/embed/tokens/emb_ed_9c1a..."

Notes:

  • jti must start with emb_, so both emb_... and emb_ed_... are valid.
  • Only a token of the caller’s workspace can be revoked; another workspace’s token answers 404 embed_token_not_found.
  • A revocation is recorded for good, whatever the token’s expiry (“never expires” included). expires_at only bounds a fast-path flag; if omitted, the flag lasts 30 days. Tokens minted before the token registry (October 2026) rely on that flag only: revoke the parent API key to retire them for good.
  • A revoked token is refused on every route, not only on the embed pages: every request with an embed token runs the same validation (revocation, parent API key).
  • Revoking the parent API key also invalidates child embed tokens once the API-key active cache says the key is inactive.

Follow these rules in every external integration:

Rule Reason
Mint embed tokens only on your backend. API keys and OAuth client secrets must never reach the browser.
Use narrow allowed_origins. Only these sites can host the iframe and command it (§3.5); the runtime middleware also uses them for frame-ancestors.
Prefer short-lived per-session tokens. Limits impact if a browser token is copied.
Use max_executions for runtime tokens where possible. Bounds cost and abuse for end-user widgets.
Use end_user_id for customer-facing embeds. Groups runs by external user across tokens; without it each token sees only its own runs.
Validate event.origin in every parent message listener. The iframe addresses only allowed origins, but another frame on your page could post too: check it.
Send postMessage with Madoo’s exact target origin. Avoid broadcasting commands to an arbitrary child window.
Revoke tokens when an external session, installation, or user is disabled. Revocation is immediate and checked on every embed request.

These are important because they affect how integrations should be built today:

  • Every embed token (runtime and editor) is validated on every route — revocation and parent key included — by the bearer handler (EmbedBearerValidation); EmbedAuthMiddleware additionally sets the embed principal and the CSP header on /embed/* and /api/embed/*.
  • EmbedTokenSurfaceMiddleware keeps each token type on its routes (deny by default, 403 EMBED_TOKEN_SURFACE); the services decide which workflow (the token’s) and which runs (its end user’s, or embed:{jti}) it may touch.
  • Runtime iframe initialization is split: token/workflow/interface come from URL and backend config; theme, locale, dynamic values, visibility, and custom CSS can come from postMessage.
  • Editor iframe initialization is query-string based for token/workflow/theme/locale. Theme and CSS can be updated with postMessage.
  • The runtime and editor iframes enforce the token’s allowed_origins on every command and post events only to the allowed origin hosting them (§3.5). The HTML of the embed pages is served by the web host, which does not send the token’s frame-ancestors; the iframe refuses to render under a non-allowed site where the browser exposes location.ancestorOrigins.
  • prefilled_inputs exists in the public token request and application request model. The current runtime page does not consume it directly from /api/embed/config; use madoo:init or madoo:setValues for browser-side prefilling until that changes.

Area File
Public token endpoints backend/src/Presentation/Madoo.Api/V1/EmbedTokensV1Controller.cs
Public token DTOs backend/src/Presentation/Madoo.Api.Contracts/V1/Embed/EmbedV1Contracts.cs
Token creation/validation backend/src/Core/Madoo.Application/Services/Identity/EmbedTokenService.cs
Token service contract/claims backend/src/Core/Madoo.Application/Services/Identity/IEmbedTokenService.cs
Runtime embed middleware backend/src/Presentation/Madoo.Api/Middleware/EmbedAuthMiddleware.cs
Embed token surface (routes) backend/src/Presentation/Madoo.Api/Middleware/EmbedTokenSurfaceMiddleware.cs
Validation on every route backend/src/Presentation/Madoo.Api/Authorization/EmbedBearerValidation.cs
Own runs / token scope backend/src/Core/Madoo.Application/Identity/EmbedEndUser.cs
Runtime bootstrap API backend/src/Presentation/Madoo.Api/Controllers/EmbedController.cs
Runtime iframe page frontend/src/app/routes/embed/index.tsx
Editor iframe page frontend/src/app/routes/embed/editor.tsx
Hosted runtime/editor SDK frontend/src/embed/host/madoo-embed-host-sdk.ts
Shared embed message protocol frontend/src/embed/protocol/embed-message-protocol.ts
Runtime iframe bridge frontend/src/embed/internal/runtime-iframe-bridge.ts
API client embed methods/types frontend/src/shared/lib/api-client.ts

Any change to the files listed in the code map, or to embed token claims, iframe URLs, query parameters, supported postMessage message types, auth requirements, execution-cap behavior, or revocation behavior must update this document in the same change.

If the implementation and this document disagree, treat the implementation as the source of truth, then update this document immediately.


Next: 08-reference.md - reference appendix.