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.
1. Integration model
Section titled “1. Integration model”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 | vPOST /api/v1/embed/.../tokens -> returns embed token | vRender iframe with token -> /embed/... or /embed/editor | vCommunicate 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
minimumRendererVersioninternally to reject unsupported layout capabilities safely; it does not change the URLs, tokens, orpostMessagemessages 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.
2. Server-side authentication
Section titled “2. Server-side authentication”First obtain a normal Public API bearer token with client credentials:
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 403EMBED_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.
3. Runtime workflow embed
Section titled “3. Runtime workflow embed”Use a runtime embed when an external end user should fill workflow inputs and generate outputs, without seeing the Madoo editor.
3.1 Create a runtime token
Section titled “3.1 Create a runtime token”POST /api/v1/embed/tokensAuthorization: Bearer <public_api_access_token>Content-Type: application/jsonRequest 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:
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.
3.2 Render the runtime iframe
Section titled “3.2 Render the runtime iframe”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.
3.3 Runtime postMessage commands
Section titled “3.3 Runtime postMessage commands”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;};3.4 Runtime postMessage events
Section titled “3.4 Runtime postMessage events”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.
3.5 Who can command the iframe
Section titled “3.5 Who can command the iframe”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:errorcodes.messageis 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 embedlocale. Knowncodevalues:missing_required_inputs(a required input was left empty;messagenames 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;messagenames 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;messagesays 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 sendsmadoo:execution:failedwithcodeAI_SPEND_PAUSED), andexecution_failed(any other startup error). A run stopped by a usage limit while running sendsmadoo:execution:failedwithcodeCREDIT_CAP_REACHED. Branch oncodefor 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 requestRangeheader and the206 Partial Content,Content-RangeandAccept-Ranges: bytesresponse semantics. A custom player cannot compensate for a proxy that collapses range requests into full200 OKresponses.
Skipped outputs. Each entry in
payload.outputscarries apresenceof"present"or"absent". An"absent"output was skipped because an optional input was not provided (or an upstream node was skipped); it has nourl/value. The run still completes successfully — checkpresencebefore 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 alogical_name(the author-facing output name) alongside the uniquename({logical}_{iteration}_{nodeId}): group bylogical_namewhen 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`;});4. Editor embed
Section titled “4. Editor embed”Use an editor embed when an external application should let a workspace user author workflows inside your own UI.
4.1 Create an editor token
Section titled “4.1 Create an editor token”POST /api/v1/embed/editor/tokensAuthorization: Bearer <public_api_access_token>Content-Type: application/jsonRequest 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:
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. |
4.2 Render the editor iframe
Section titled “4.2 Render the editor iframe”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.
4.3 Editor postMessage commands
Section titled “4.3 Editor postMessage commands”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", );});4.4 Editor postMessage events
Section titled “4.4 Editor postMessage events”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.
4b. Embed codes made in the portal
Section titled “4b. Embed codes made in the portal”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:3000for 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).
5. Revocation
Section titled “5. Revocation”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:
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:
jtimust start withemb_, so bothemb_...andemb_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_atonly 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.
6. Security rules for host applications
Section titled “6. Security rules for host applications”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. |
7. Current implementation notes
Section titled “7. Current implementation notes”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);EmbedAuthMiddlewareadditionally sets the embed principal and the CSP header on/embed/*and/api/embed/*. EmbedTokenSurfaceMiddlewarekeeps each token type on its routes (deny by default, 403EMBED_TOKEN_SURFACE); the services decide which workflow (the token’s) and which runs (its end user’s, orembed:{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_originson 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’sframe-ancestors; the iframe refuses to render under a non-allowed site where the browser exposeslocation.ancestorOrigins. prefilled_inputsexists in the public token request and application request model. The current runtime page does not consume it directly from/api/embed/config; usemadoo:initormadoo:setValuesfor browser-side prefilling until that changes.
8. Code map
Section titled “8. Code map”| 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 |
9. Maintenance rule
Section titled “9. Maintenance rule”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.