Embed a published app
Embed an app in an iframe on another site, in another app, or in a classic app.
By default, Retool blocks other pages from framing your apps as one of many mechanisms that keep your apps safe. To place a published app in an iframe, an admin adds Content Security Policy directive rules that name the pages allowed to frame it. This guide covers the three places you can embed an app: an external website, another app, and a classic app.
Retool does not currently support public links for apps, which make your app available to anyone without authentication.
Requirements
You must be an admin or have the delegated Manage advanced settings permission to change these rules. Refer to Customize the Content Security Policy for apps for how the policy works and which source expressions are accepted.
Each app you embed must be published. Refer to Publish an app for the URL of a published app.
Required directives
To embed your apps while keeping them secure, set one or both of the following directives in the Content Security Policy settings.
| Directive | Applies to | Default | Effect | Required for |
|---|---|---|---|---|
frame-ancestors | All apps in your organization. | 'none' | Names the pages allowed to frame the app. | All parent types (apps, classic apps, and external websites). |
frame-src | The page holding the iframe. | 'self' | Names what that page can load in an iframe. | When embedding an app in another app. |
Rules take up to 30 minutes to apply across all of your apps.
Embed an app in an iframe
- External website or app
- Retool app
- Classic Retool app
Set frame-ancestors to allow a parent website or app to frame your app:
- Go to Settings > App security > Content Security Policy.
- Select Add rule, and choose the
frame-ancestorsdirective. Enter the origin of the page that holds the iframe (which is the scheme, host, and optional port), such ashttps://portal.example.com. To allow several subdomains of one site, use a single-level wildcard such ashttps://*.example.com. - Add an iframe to that page with the published app URL as its
src.
<iframe
src="https://retool.example.com/rr/app/support-tool"
width="100%"
height="800"
></iframe>
Set frame-ancestors and frame-src to allow another Retool app to frame your app:
- Go to Settings > App security > Content Security Policy.
- Add a
frame-ancestorsrule with the origin of the parent app. - Add a
frame-srcrule with the origin of the child app that you want to embed. - Navigate to the parent app. Ask the agent to add an iframe with the published URL of the child app as its
src.
Avoid a wildcard such as https://*.retool.example.com to cover every pair at once. Published app URLs from other
organizations can sit beneath the same domain, so a wildcard can allow apps outside your organization to frame yours. List each app origin you embed instead.
Embedding apps in classic apps is a helpful strategy for organizations that are in the process of converting their classic apps to apps, but haven't fully made the switch.
A classic app embeds content with the IFrame component. Set frame-ancestors to allow a classic app to frame your app, and configure the IFrame component to allow storage and cookies:
- Self-hosted
- Cloud
- Follow the instructions in the Configure same-origin and sandbox for iframes guide to set the
ALLOW_SAME_ORIGIN_OPTIONenvironment variable to use theallow-same-originattribute. - Go to Settings > App security > Content Security Policy.
- Add a
frame-ancestorsrule with the origin of your Retool organization, such ashttps://retool.example.com. - Add the IFrame component to the classic app and set URL to the published app URL.
- In the component settings, enable Storage and cookies.
- Go to Settings > App security > Content Security Policy.
- Add a
frame-ancestorsrule with the origin of your Retool organization, such ashttps://retool.example.com. - Add the IFrame component to the classic app and set URL to the published app URL.
- In the Inspector, enable Storage and cookies in the Interaction section.
Pass data between embedded and parent apps
To pass data between the parent app and the embedded app, use the browser's postMessage API.
To align your parent app and embedded app's postMessage events for interactivity, Retool suggests using Retool's CLI or MCP server with external source control to build the communication path for both apps at once.
Configure custom authentication
If the parent app is not a Retool app, you can use custom authentication with the Retool API to automatically authenticate the user.
This flow is available for apps published on a cloud instance that use the standard Retool domain (example.retool.com). It is not available to self-hosted instances or organizations that use a custom domain, because their apps are published on a different URL.
When parent app authenticates a user using your preferred authentication method, your backend must make a request to Retool to generate an embed URL. This embed URL is a secure, single-use link on the domain of your published app. Loading it in the iframe exchanges the link for a session, sets the cookies the app needs, and then displays the app. The following diagram illustrates the authentication flow.
The app must already be published, and the origin of the page that holds the iframe must have a frame-ancestors rule, as described in Embed an app in an iframe. Custom authentication does not replace that rule.
Generate an access token
First, an admin must create an access token with the Retool Embed scope, listed under Apps. This token allows you to create sessions for embedding apps.
- Go to Settings > Retool API and click Create new to generate an access token.
- Enter a name and description, then select the required scopes.
- Copy and save your token, as you can only access it once.
Create permission groups for your users
An admin must create a permission group that determines the apps users can access.
To create a permission group:
- Navigate to Settings > Groups.
- Click Create new in the top right and complete the form to create a group.
- On the group's Apps tab, enable Use access to the app you want users to have access to. Refer to Share apps for more information.
Make sure to note the Group ID for the group you want to give access to. You can find a group's ID by hovering over the group, clicking the menu, and then selecting Copy group ID. Retool adds the external user to the groups you specify when it creates the session.
Create an embed URL
The embed URL is a single-use link to an app for an authenticated user. Create this URL by sending an API request from the backend of your parent app to to POST /api/embed-url/external-user with the required parameters. Retool returns the URL, which you can use in the frontend of your parent app to display the embedded app.
Refer to Create an Embed URL for the full API specification.
The following examples send the request to Retool and return the embed URL to your frontend. Replace APP_UUID, GROUP_IDS, and USER_ID with your own values.
- cURL
- Python
- JavaScript
curl -X POST "https://exampleorg.retool.com/api/embed-url/external-user" \
-H "Authorization: Bearer retool_01hn417x8zsaqfye58r995re18" \
-H "Content-Type: application/json" \
-d '{ "landingPageUuid": "APP_UUID", "groupIds": GROUP_IDS, "externalIdentifier": "USER_ID" }'
This example accepts a request from the client, builds and sends a POST request to /api/embed-url/external-user, receives the embed URL, and returns the URL to the client.
@app.route('/embedUrl', methods=['POST'])
def embed_url():
data = request.get_json()
app_uuid = app_name_to_uuid[data['retoolAppName']]
# userJwt is an example variable that the frontend could pass to your backend and is not required.
# Verify it with PyJWT so a browser cannot choose the identity of the embed session.
try:
claims = jwt.decode(data['userJwt'], secret_key, algorithms=['HS256'])
except jwt.InvalidTokenError:
return {'error': 'Invalid token'}, 401
headers = {
# The RETOOL_API_KEY is the token generated in the first step
'Authorization': f"Bearer {os.environ['RETOOL_API_KEY']}",
'Content-type': 'application/json',
}
body = {
'landingPageUuid': app_uuid,
'groupIds': [12, 13],
'externalIdentifier': claims['sub'],
'userInfo': {
'firstName': claims['firstName'],
'lastName': claims['lastName'],
'email': claims['email'],
},
'metadata': {
'storeId': 5
}
}
resp = requests.post(
f"https://{os.environ['RETOOL_URL']}/api/embed-url/external-user",
headers=headers,
json=body,
)
if resp.ok:
return resp.json()
else:
# Handle error
pass
router.post("/embedUrl", async (req, res) => {
const data = req.body;
const app_uuid = app_name_to_uuid[data.retoolAppName];
// userJwt is an example variable that the frontend could pass to your backend and is not required.
// Use verify() rather than decode(), which does not check the signature, so a browser
// cannot choose the identity of the embed session.
let claims;
try {
claims = jwt.verify(data.userJwt, process.env.SECRET_KEY);
} catch (err) {
return res.status(401).json({ error: "Invalid token" });
}
const { sub, firstName, lastName, email } = claims;
const response = await fetch(
`https://${process.env.RETOOL_URL}/api/embed-url/external-user`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RETOOL_API_KEY}`,
"Content-type": "application/json",
},
body: JSON.stringify({
landingPageUuid: app_uuid,
groupIds: [12, 13],
externalIdentifier: sub,
userInfo: { firstName, lastName, email },
metadata: { storeId: 5 },
}),
}
);
res.json(await response.json());
});
Retool responds with a URL on the domain of your published app.
{
"embedUrl": "https://exampleorg--support-tool.retool.app/embed-redirect?nonce=NONCE&destination=%2F"
}
Use the embed URL to display the app
On your frontend, request the embed URL from your backend and set it as the src of an iframe.
- React
- JavaScript
const RetoolWrapper = ({ retoolAppName, userJwt }) => {
const [retoolEmbedUrl, setRetoolEmbedUrl] = useState("");
useEffect(() => {
// Each embed URL works once, so request a new one whenever the app or the
// signed-in user changes, and ignore a response that arrives after a switch.
let current = true;
setRetoolEmbedUrl("");
const options = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userJwt, retoolAppName }),
};
fetch("/api/embedUrl", options)
.then((res) => res.json())
.then((data) => {
if (current) {
setRetoolEmbedUrl(data.embedUrl);
}
});
return () => {
current = false;
};
}, [retoolAppName, userJwt]);
return (
retoolEmbedUrl && (
<iframe
src={retoolEmbedUrl}
style={{ width: "100%", height: "800px", border: "none" }}
/>
)
);
};
const container = document.createElement('div')
app.appendChild(container)
const getEmbedUrl = async () => {...}
getEmbedUrl().then((retoolEmbedUrl) => {
const frame = document.createElement('iframe')
frame.src = retoolEmbedUrl
frame.style = "height: 100%; width: 100%; border: none;"
container.appendChild(frame)
});
Each embed URL works only once. Request a new URL from your backend each time the page that holds the iframe loads.
Control data access with metadata
You can pass information in the metadata object to dynamically control the data users see and the behavior within an app. Retool stores these values on the user and returns them in the Current User object as current_user.metadata when the app loads.
The values come from the request your backend makes, not from the page that holds the iframe, so a user cannot change them in the browser.
Retool merges the values you send with any metadata the user already has. A key you stop sending remains set from an earlier request, so send an explicit value for every key the app reads.
Testing
When generating the embed URL, Retool may create an additional user depending on the data that is passed into your POST request.
To avoid creating an extra user while testing, pass in the email associated with your Retool account in the email property of the userInfo object, as well as the IDs of the permission groups you're in.
If an email is specified and it matches that of an existing Retool user, Retool updates the user, its metadata, and permission groups. It creates a new user if the email does not exist. If no email is specified, Retool updates the user if the externalIdentifier matches, or creates a new user if the externalIdentifier does not exist. If the externalIdentifier and the email belong to two different users that already exist in your organization, the request fails.
Troubleshooting
Use this section to diagnose and resolve common issues with apps in an iframe.
Why is the iframe blank?
Open your browser's developer console, and find the Content Security Policy violation. The violation names the directive that blocked it.
- A
frame-ancestorsviolation means the parent page origin is missing from that directive. The following error appears in your developer console:Content Security Policy of your site blocks some resources. - A
frame-srcviolation means the parent page is a Retool app and the origin of the app in the iframe is missing from that directive. The following error appears in your developer console:Ensure CORS response header values are valid.
Add the missing origin, then reload the parent page.
Why does a request for an embed URL fail?
Retool rejects the request to /api/embed-url/external-user in the following cases. The response body names the reason.
| Failure reason | Resolution |
|---|---|
| The app has no published version. | Publish the app, then request the URL again. |
The request includes environment, branch, or releaseVersion. | Remove these parameters since they apply to classic apps only. |
The API token is missing the Retool Embed scope, which returns a 401 response. | Create a token with that scope. |
The externalIdentifier and the email in userInfo belong to two different users that already exist in your organization. | Pass values that refer to the same user. |
Why does the app ask the user to reload the page?
The embed URL could not be exchanged for a session, because each URL works only once and the URL was loaded a second time or has expired. A reload of the parent page requests a new URL, which succeeds. If the message persists, confirm your backend requests a new embed URL for each load rather than reusing one.
Why does the camera or microphone not work?
A browser grants an iframe only the features the page holding it delegates, so an app that works on its own can lose these features once embedded. Video and audio capture, fullscreen, and DRM playback are the usual ones.
- In an external website or another app, list the features in the
allowattribute of the iframe element, such as<iframe src="EMBED_URL" allow="camera; microphone; fullscreen"></iframe>. - In a classic app, enable the matching options on the IFrame component, such as Camera, Microphone, Fullscreen, and Encrypted media.
If the app instead loads media or an embedded player from another site, allow that origin in your organization's Content Security Policy. Refer to Customize the Content Security Policy for apps.
Why has a rule not taken effect?
Rules take up to 30 minutes to apply across all of your apps. Reload the parent page after that time. Confirm the rule records an origin with a scheme, such as https://portal.example.com, and not a bare host name.