# How we write docs Source: https://docs.embeddables.com/about/how-we-write-docs Our process for doc-writing as a team. ## Who writes these docs? Our CTO, Jeremy, writes most of the docs, especially the initial ones. This ensures that we have not only a consistent styles but a coherent strategy for how we structure and write our documentation. ## How does everyone else contribute? Everyone else on the Embeddables team contributes in the following ways: ### 1. Feedback + Suggestions * Giving feedback on docs. * Suggesting what docs are needed / need expanding. * Suggesting changes to the structure. ### 2. What they're working on * Notifying when they update or add a feature and the relevant docs need to be updated. * This means that while product builders are not responsible for writing those docs, they *are* responsible for making sure that things they build always come with up-to-date docs - by asking Jeremy to write/update them. ### 3. Creating authored docs * Writing an "authored doc", which means we include the name and photo of who wrote the doc at the very top! * This could be a text-based doc, or a Loom video, or (ideally) both. * On something that they have a connection to (they built the feature / pioneered this use-case / built a great example / love this use-case or feature). * The purpose of these is to reflect and celebrate the that fact that different authors will naturally write/record with their own voice. We have a wonderful range of backgrounds and personalities on the Embeddables team and are proud to give everyone (who wants it) a platform. ❤️ # Who are these docs for? Source: https://docs.embeddables.com/about/who-are-these-docs-for Why we write docs, our reader personas, and key use-cases. ## Why we write docs * To help our users learn how to achieve what they want to in our product. * To help our users get the most our of our platform (e.g. things they weren’t necessarily looking for but could benefit them). * To communicate to our users, and align our team on, the current state of the platform - a single source of truth. * To advise users more generally on good strategies and tactics for building great web experiences. ## Use-cases we cater for * "I’m a brand new user and I want to get started in under 10 mins." * "I’m a new/fairly new user and I want to take 30-60 mins to properly learn the platform in a structured way." * "I’m an existing user and I want to achieve a certain thing." * "I’m an existing user and I want to upgrade my skills on the platform to get more out of it." * "I’m a new user, or soon to be one, and I want to know what is/isn't possible to build with Embeddables." * "I’m a new or existing user, or even part of the Embeddables team, and I want a clear answer about the current state of the platform." * "I'm a new or existing user, and I want advice on best practices for builder amazing experiences online." # Get Embeddable Source: https://docs.embeddables.com/api-reference/embeddables/get-embeddable GET /projects/{projectId}/embeddables/{embeddableId} Retrieve an embeddable from the database based on the ID # Get Embeddable Version Source: https://docs.embeddables.com/api-reference/embeddables/get-embeddable-version GET /projects/{projectId}/embeddables/{embeddableId}/versions/{versionNumber} Retrieve a specific version of an embeddable from the database based on the ID and version number # Get Embeddable Versions Source: https://docs.embeddables.com/api-reference/embeddables/get-embeddable-versions GET /projects/{projectId}/embeddables/{embeddableId}/versions Retrieve all versions of an embeddable from the database based on the ID # Get Embeddables Source: https://docs.embeddables.com/api-reference/embeddables/get-embeddables GET /projects/{projectId}/embeddables Retrieve all embeddables in this project # Get Entries Source: https://docs.embeddables.com/api-reference/entries/get-entries GET /projects/{projectId}/entries Retrieve all entries from the database # Get Entries' Page Views Source: https://docs.embeddables.com/api-reference/entries/get-entries-page-views GET /projects/{projectId}/entries-page-views Retrieve all pages viewed, with timestamps, for all entries in the database # Get Entry Source: https://docs.embeddables.com/api-reference/entries/get-entry GET /projects/{projectId}/entries/{entryId} Retrieve an entry from the database based on the ID # Get Entry Events Source: https://docs.embeddables.com/api-reference/entries/get-entry-events GET /projects/{projectId}/entries/{entryId}/events Retrieve all events for entry from the database based on the ID # Create Event Source: https://docs.embeddables.com/api-reference/events/create-event POST /projects/{projectId}/events Submit a custom event for an entry # Get Experiment Source: https://docs.embeddables.com/api-reference/experiments/get-experiment GET /projects/{projectId}/experiments/{experimentId} Retrieve an experiment from the database based on the experiment ID # Get Experiments Source: https://docs.embeddables.com/api-reference/experiments/get-experiments GET /projects/{projectId}/experiments Retrieve all experiments in a project with optional status filtering # Embeddables API - Introduction Source: https://docs.embeddables.com/api-reference/introduction Get programmatic access to your Embeddables and your data For access to the Embeddables API, please contact the Embeddables team to discuss pricing. ## Welcome The Embeddables API is brand new and still being built. Please contact us to get access and let us know what you want us to prioritize shipping first! Use our interactive docs to generate your first API request. ## Authentication All private API endpoints are authenticated using API Keys. Each API Key gives full access to a particular [Project](/reference/glossary/glossary#project). Head to **Settings** > **Credentials and Endpoints** in the [Embeddables Dashboard](https://app.embeddables.com/) to create an API Key. You should provide the API Key as the `X-Api-Key` header in your request: ```sh Sample CURL Request theme={null} curl --request GET \ --url https://api.embeddables.com/projects/{projectId}/entries \ --header 'X-Api-Key: ' ``` # Embeddables Builder Source: https://docs.embeddables.com/builder/overview Build and edit Embeddables visually in the Builder, with a 5-minute onboarding tour The **Builder** is where you design pages, add logic, style components, and preview your Embeddable before publishing. This tour covers: * Overview of the Builder * Sidebars and Top Bar * Adding content, and navigating components and pages * Styling your Embeddable * Adding logic: show/hide conditions + split tests * Custom code, analytics, and sending data to other services * Saving, versions, drafts, pushing to staging + going live ## The Onboarding Tour ## Key Builder topics Style components, layers, and layouts. Save drafts, manage versions, and publish changes. ## Dive deeper into those features Now that you've watched the Onboarding Tour, these pages dive deeper into each of the features you just learned about: How data is stored Show/hide content Test different versions Compute new values from users' data Transform and send data to other services Track your users and measure insights Drafts, saves & versions Pass data from the URL Publish to your website # Using the CLI with AI Source: https://docs.embeddables.com/cli/ai Set up Cursor, Claude, and Codex for AI-assisted Embeddable editing The CLI is built for **AI-assisted editing**: the file layout is predictable, and `embeddables init` injects **Cursor rules**, **Claude project context**, **Codex context (AGENTS.md)**, **Gemini CLI context (GEMINI.md)**, and **Antigravity rules** so the assistant understands Embeddable components, keys, and conventions. Use the sections below to get the most out of each tool. ## How the context files work `embeddables init` creates a shared **`AI-README.md`** at the project root containing the full Embeddables CLI development guide (file structure, component types, `id`/`key` rules, action values, etc.). Each AI editor's rule file is a **short pointer** to that shared file rather than duplicating the full content. This keeps the rule files small and easy for AI tools to load, while `AI-README.md` remains the single source of truth. To keep all files up to date after a CLI upgrade, run `embeddables upgrade` — it automatically refreshes `AI-README.md` and all rule files to match the installed version. ## Cursor When you run `embeddables init`, the CLI can add a **Cursor rule** so Cursor AI has full context when you edit Embeddables: * **Where it goes**: `.cursor/rules/embeddables-cli.md` * **What it does**: A short pointer file marked `alwaysApply: true` that references `AI-README.md`. Cursor loads the shared guide automatically for every conversation, so you don't need to mention it each time. * **Referencing it in chat**: If you want to be explicit, you can @-mention the file in the composer, e.g. `@AI-README.md`, so the model is sure to use it for the current task. * **Re-injecting**: If you delete `.cursor/rules/embeddables-cli.md` or want the latest version, run `embeddables init` or `embeddables upgrade`; both will overwrite or recreate the rule file. Use Cursor for quick edits to a single page, adding a component, or fixing a prop — the rule keeps the assistant from suggesting invalid `action` values or keys that start with a number. ## Claude When you run `embeddables init`, the CLI can also add **Claude (Claude Code / project) context**: * **Where it goes**: `.claude/CLAUDE.md` (a short pointer to `AI-README.md`). * **What it does**: Claude uses this file as project context when you work in the repo. The pointer in `.claude/CLAUDE.md` directs Claude to `AI-README.md`, which covers pages, global components, styles, computed fields, actions, and component primitives. * **Referencing it in prompts**: In a Claude Code or project prompt, you can say: "Always read the Embeddables CLI context before starting (e.g. `AI-README.md`)." For complex tasks (e.g. building multiple pages from designs), the CLI repo also includes example prompts in `.prompts/custom/` (e.g. build-funnel style) that you can adapt and paste into Claude. * **Re-injecting**: Run `embeddables init` or `embeddables upgrade` to recreate or refresh `.claude/` with the latest content. Claude works well for multi-step tasks (e.g. "add a new page and wire up the buttons") or when you paste in the context file path at the start of a long conversation. ## Codex When you run `embeddables init`, the CLI generates an **`AGENTS.md`** file at the project root: * **Where it goes**: `AGENTS.md` (project root) * **What it does**: Follows the OpenAI Codex convention. A short pointer to `AI-README.md`, giving Codex full awareness of the project's file structure, component types, and conventions. * **Re-injecting**: Run `embeddables init` or `embeddables upgrade` to regenerate or update `AGENTS.md` with the latest content. ## Gemini CLI When you run `embeddables init`, the CLI generates a **`GEMINI.md`** file at the project root: * **Where it goes**: `GEMINI.md` (project root). * **What it does**: Follows the Gemini CLI convention. A short pointer to `AI-README.md`, giving Gemini full awareness of the project's file structure, component types, and conventions. * **Re-injecting**: Run `embeddables init` or `embeddables upgrade` to regenerate or update `GEMINI.md` with the latest content. ## Antigravity When you run `embeddables init`, the CLI generates an **Antigravity rules file** at `.agent/rules/embeddables-cli.md`: * **Where it goes**: `.agent/rules/embeddables-cli.md` * **What it does**: Follows the [Antigravity](https://antigravity.dev) convention. Includes Antigravity-specific frontmatter (`globs`, `alwaysOn: true`) and points to `AI-README.md` so the context is always active for relevant files. * **Re-injecting**: Run `embeddables init` or `embeddables upgrade` to regenerate or update `.agent/rules/embeddables-cli.md` with the latest content. ## Tips and best practices for prompt writing Getting the best results from Cursor or Claude when editing Embeddables: 1. **Be specific about files and components.** Name the file you want changed (e.g. `embeddables//pages/welcome.page.tsx`) and, when relevant, the component key or id (e.g. "the CustomButton with key `submit_btn`"). That keeps edits scoped and avoids accidental changes elsewhere. 2. **For big tasks (e.g. building from screenshots or docs), use a plan-first style.** Ask for a short plan and list of decisions/uncertainties before editing. Example: "Create a plan and highlight any decisions or things you're unsure about, especially around which components to use. Then implement." The CLI's example prompt in `.prompts/custom/build-funnel.md` follows this pattern (plan, then build, then summary of decisions/assumptions/uncertainties). 3. **Attach images when possible.** If you have a screenshot of what you want the result to look like (for example, "build a page like this example layout"), include it with your prompt. Likewise, if something's wrong with the page as rendered, adding a screenshot of the issue can help Cursor or Claude diagnose and fix the problem much more quickly. Screenshots provide visual clarity that's sometimes hard to convey with just words or code snippets. 4. **Model choice.** For best results, use **Opus 4.6** when available (e.g. in Claude). It thinks a little longer but produces very accurate results for Embeddables edits. # Command Reference Source: https://docs.embeddables.com/cli/commands Complete reference for all Embeddables CLI commands and their options ## Core Commands ### Setup & auth | Command | Description | | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `embeddables login` | Log in to your Embeddables account (global). | | `embeddables logout` | Clear stored authentication. | | `embeddables init` | Initialize from inside your project folder: creates `embeddables.json`, `.types/`, `embeddables/`, optional Cursor/Claude/Codex/Gemini/Antigravity prompts; creates `tsconfig.json` if absent, or patches it to add the required type alias paths if it already exists. | | `embeddables upgrade` | | ### Daily workflow | Command | Description | | ----------------------------- | ------------------------------------------------------------------------------------- | | `embeddables pull` | Fetch an Embeddable from the cloud and reverse-compile to TSX. | | `embeddables dev` | Start dev server with hot reload; proxies to the Engine. | | `embeddables save` | Build and upload the Embeddable to the cloud. | | `embeddables diff` | Compare two versions of an Embeddable (default: latest vs local). | | `embeddables branch` | Switch to a different branch (interactive list or `--branch`), then pull that branch. | | `embeddables branches create` | Create a new branch from the current Embeddable state. | | `embeddables builder open` | Open the Embeddables Builder in your default browser for an Embeddable. | | `embeddables feedback` | Send feedback about the CLI directly from your terminal. | You can `cd embeddables/` and run Embeddable-specific commands (`dev`, `pull`, `save`, `branch`, `builder open`) from there. The CLI will automatically use that Embeddable instead of prompting you to choose. ### Tasks | Command | Description | | --------------------------------- | --------------------------------------- | | `embeddables tasks list` | List all tasks for the current project. | | `embeddables tasks get` | Fetch full details for a task by ID. | | `embeddables tasks update-status` | Change a task's status. | | `embeddables tasks assign` | Change a task's assignee. | | `embeddables tasks comment` | Add a comment to a task. | | `embeddables tasks add-branch` | Link an Embeddables branch to a task. | ### Assets | Command | Description | | --------------------------- | ----------------------------------------------------------------------- | | `embeddables assets upload` | Upload a local asset folder to the Embeddables asset store. | | `embeddables assets sync` | Sync uploaded asset metadata from the cloud into a local `assets.json`. | ### Branches | Command | Description | | ----------------------------- | ------------------------------------------------------------ | | `embeddables branches create` | Create a new Embeddable branch from the current local state. | ### Experiments | Command | Description | | --------------------------------- | ------------------------------- | | `embeddables experiments create` | Create a new experiment. | | `embeddables experiments connect` | Connect an existing experiment. | ### Build (advanced) | Command | Description | | ------------------- | ------------------------------------------------------------------------------------------------- | | `embeddables build` | Compile TSX → JSON only (no upload). Used automatically by `save` unless you pass `--skip-build`. | ### Publishing (advanced) | Command | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------- | | `embeddables dangerously-publish` | Promote an existing saved version to staging or production without a full rebuild. Use with care. | ### Debugging | Command | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------ | | `embeddables inspect` | Fetch an Embeddable from the engine, reverse-compile to React files, rebuild to JSON, and compare the two outputs. | *** ## Command Options
Option Description
-t, --token \ Use a token for non-interactive login. Pass a JWT directly instead of going through the OTP email flow — ideal for CI pipelines and cloud containers. The token's expiry is parsed from the JWT payload and stored alongside the session.
Option Description
`-p, --project-id ` Set project ID (if logged in, you can pick from a list instead).
`-y, --yes` Skip prompts and use defaults.
Option Description
`-i, --id ` Embeddable ID to pull (skips interactive selection).
`--version [number]` Version to pull. Omit a value (`--version`) to open an interactive version selector. Pass a number (`--version 47`) to pull a specific version. Without this flag, `pull` defaults to the latest version.
`-o, --out ` Output path for compiled JSON (default: `embeddables//.generated/embeddable.json`).
`-b, --branch ` Pull a specific branch.
`-f, --fix` Remove components with missing required props instead of erroring.
`-p, --preserve` Preserve component order in config (see note below).
--project-id \ Project ID for embeddable selection (skips project selection prompt).
--bypass-auth Skip the login check. Intended for CI environments and automated tooling where no auth token is stored.
Option Description
`-i, --id ` Embeddable ID (prompt if not provided).
`-p, --pages ` Custom pages glob (default: `embeddables//pages/**/*.page.tsx`).
`-o, --out ` Output path for compiled JSON.
`-L, --local` Use local engine (`http://localhost:8787`) instead of production.
`-e, --engine ` Engine origin (default: `https://engine.embeddables.com`; overridden by `--local`).
`--port ` Dev proxy port (default: `3000`). If in use, CLI tries the next available port.
`--overrideRoute ` Route to override in proxy (default: `/init`).
`--pageKeyFrom ` How to derive page keys: `filename` or `export` (default: `filename`).
`--fix` Apply lint fixes (e.g. duplicate IDs, keys starting with a number).
--bypass-auth Skip the login check. Intended for CI environments and automated tooling where no auth token is stored.
Option Description
`-i, --id ` Embeddable ID (required).
`-p, --pages ` Pages glob.
`-o, --out ` Output path for JSON.
`--pageKeyFrom ` `filename` or `export` (default: `filename`).
`--fix` Apply lint fixes.
--bypass-auth Skip the login check. Intended for CI environments and automated tooling where no auth token is stored.
Option Description
`-i, --id ` Embeddable ID (prompt if not provided).
`-l, --label Human-readable version label.
`-b, --branch ` Branch ID to save to.
`-s, --skip-build` Use existing `.generated/embeddable.json` (no build step).
`--from-version ` Base version (auto-detected from local config if not set).
-p, --project-id \ Project ID (skips project selection prompt).
--force Skip confirmation prompts (version conflicts and other users' drafts).
Option Description
`-i, --id ` Embeddable ID (prompt from local Embeddables if not provided).
-b, --branch \ Branch ID or name to switch to (skips the interactive branch selection prompt).
Option Description
-i, --id \ Embeddable ID. Inferred from the current working directory or automatically when run inside embeddables/\/; otherwise prompts to choose from local Embeddables if not provided.
-n, --name \ Branch name (required). The origin version and branch are read automatically from config.json. The success output includes the new branch ID returned by the server.
Option Description
-i, --id \ Embeddable ID (inferred from cwd or interactive prompt if not provided).
-n, --name \ Experiment name (required).
-k, --experiment-key \ Experiment key (required). Used to reference the experiment in code.
-p, --project-id \ Project ID (skips project selection prompt).
--variants \ Comma-separated key:weight pairs (e.g. control:50,treatment:50). Defaults to control:50,treatment:50. Weights must sum to 100; if they don't, the CLI will prompt to auto-adjust (or pass --auto-adjust to skip the prompt).
--status \ Initial experiment status. Valid values: draft, active, paused, completed. Defaults to draft.
--auto-adjust Automatically scale variant weights to 100% without an interactive confirmation prompt. Useful for non-TTY/CI environments.
Option Description
`-i, --id ` Embeddable ID.
`--experiment-id ` Experiment ID (prompt to choose if not provided).
`--experiment-key ` Experiment key (required if `--experiment-id` is set).
-p, --project-id \ Project ID (skips project selection prompt).
Option Description
`-i, --id ` Embeddable ID (prompt from local Embeddables if not provided; inferred automatically if run from inside `embeddables//`).
-p, --project-id \ Project ID (skips project selection prompt).
Option Description
`-i, --id ` Embeddable ID to inspect (required).
`--version ` Version to inspect — a version number or `"latest"` (default: `latest`).
`-b, --branch ` Embeddable branch ID.
`-f, --fix` Fix by removing components missing required props (default: on).
`-p, --preserve` Preserve component order during forward compilation (default: off). Use when you want to keep existing component order for comparison purposes.
`-e, --engine ` Engine origin (default: `https://engine.embeddables.com`).
Option Description
`-i, --id ` Embeddable ID (prompt from local Embeddables if not provided; inferred automatically if run from inside `embeddables//`).
`--from ` Version to compare from (default: `latest`). Accepts a version number, `latest`, `staging`, `prod`, `local`, or `branch@version`.
`--to ` Version to compare to (default: `local`). Same format as `--from`.
`--depth ` Detail level: `pages`, `components` (default), or `props`.
`--page ` Filter output to specific page keys or IDs (comma-separated, no spaces).
`--component ` Filter output to specific component keys or IDs (comma-separated, no spaces).
`-b, --branch ` Branch to use when fetching cloud versions (auto-detected from `config.json` if not set).
`-e, --engine ` Engine origin (default: `https://engine.embeddables.com`).
`--no-color` Disable colored output.
Option Description
`-i, --id ` Embeddable ID (inferred from cwd or interactive prompt if not provided).
`--dir ` Local directory to upload from (default: `assets/`).
`--group-id ` Target asset group ID. Resolved from project config if not provided.
`--recursive` Traverse subdirectories (default: top-level only).
`--include ` Glob pattern to include only matching files.
`--exclude ` Glob pattern to exclude matching files.
`--dry-run` Preview what would be uploaded without actually uploading.
`--workers ` Number of concurrent upload workers (default: `5`).
Option Description
`-i, --id ` Embeddable ID (inferred from cwd or interactive prompt if not provided).
`--group-id ` Asset group ID to sync. Resolved from project config if not provided.
`-o, --out ` Output path for the asset manifest (default: `assets.json`).
Option Description
`[message]` Optional feedback message as a positional argument. If omitted, you will be prompted interactively.
`--positive` Mark the feedback as positive (👍).
`--negative` Mark the feedback as negative (👎). Default when a message is passed non-interactively.
`--category ` Feedback area: `cli`, `ai`, `compiler`, or `other`.
Option Description
-p, --project-id \ Project ID to list tasks for. Defaults to the project configured in embeddables.json; prompts interactively if not set.
Option Description
-i, --id \ Task ID (required). Displays status, priority, type, assignee, dates, body, and effort.
Option Description
-i, --id \ Task ID (required).
-s, --status \ New status (required). Valid values: to\_do, scoping, in\_progress, feedback\_cycle, qa, blocked, completed, cancelled. Hyphens are accepted in place of underscores.
Option Description
-i, --id \ Task ID (required).
--assignee-id \ User ID to assign the task to. Mutually exclusive with --assign-to-owner.
--assign-to-owner Assign the task to its current owner. Mutually exclusive with --assignee-id.
Option Description
-i, --id \ Task ID (required).
-m, --message \ Comment content (required).
Option Description
-i, --id \ Task ID (required).
-b, --branch-id \ Branch ID to link to the task (required).
Option Description
-i, --id \ Embeddable ID (prompt if not provided).
--staging Publish to the staging environment. Mutually exclusive with --prod.
--prod Publish to production only (the version must already be on staging server-side). Mutually exclusive with --staging.
--via-staging With --prod: push to staging first, then to production. Use when the version is not yet on staging.
--save Build and upload a new saved version before publishing it.
--publish-version \ Promote this specific version number instead of reading the version from config.json.
-l, --label \ Human-readable label for the new version (only applies with --save).
--skip-build Skip the build step (only applies with --save).
-p, --project-id \ Project ID (skips project selection prompt).
--force Skip confirmation prompts (version conflicts when using --save, other users' drafts).
**`pull --preserve`:** If you've held back on using the CLI because the first pull changes the JSON a lot, try **`embeddables pull --preserve`**. It keeps the same order of components in the config. The biggest cause of "changes" on pull is usually component reordering (React sorts them within containers), which doesn't affect the rendered Embeddable. You may still see smaller changes (e.g. `parent_key` fixes), but it's easier to verify what actually changed when component order stays the same. # Development Workflow Source: https://docs.embeddables.com/cli/development Dev server, Workbench, TypeScript authoring, and workflows for technical developers ## Development server **`embeddables dev`** compiles your TSX (and styles, config, computed fields, actions) and runs a **local proxy** that: * Serves the compiled Embeddable to the Engine at the **Preview URL**. * Watches files under `embeddables//` and recompiles on change (hot reload). * By default uses the **production** Engine (`https://engine.embeddables.com`); use `--local` to point to a local engine (e.g. `http://localhost:8787`). The terminal prints a URL like: `http://localhost:3000?id=&version=latest` Open it in a browser to see your Embeddable. The dev server needs **internet** when using the default (remote) engine. ### Workbench (debugging) The **Workbench** is a debugging panel for easily changing pages, inspecting user data, computed fields, and actions. * On **localhost** previews (from `embeddables dev`), the Workbench is **on by default**. * To hide it, add `?workbench=false` to the URL. * On **non-localhost** preview links (e.g. shared or deployed URLs), add `?workbench=true` to show the Workbench. * It is injected by the dev proxy so you can debug without deploying. *** ## Authoring with TypeScript Page and global-component files are React (TSX). Import primitives from `@embeddables/cli/components` for autocomplete and correct props: ```tsx theme={null} import { Container, InputBox, PlainText, CustomButton, } from "@embeddables/cli/components"; export default function MyPage() { return ( <InputBox id="email" key="email" input_type="email" label="Email" placeholder="you@example.com" isRequired /> <CustomButton id="submit" key="submit" text="Continue" action="next-page" /> </Container> ); } ``` * **IDs and keys**: Each component needs a unique `id` and `key`. Keys must not start with a digit (use a prefix like `option_` or `range_` if needed). * **Button actions**: Use `"next-page"` and `"prev-page"` (with `-page`), not `"next"` or `"prev"`. * Types and stubs live in `.types/` (created by `init`) so your editor can resolve `@embeddables/cli/components` and `@embeddables/cli/types` without installing the CLI as a project dependency. *** ## For technical developers * **Comparing changes**: Use `embeddables diff` to compare your local working files against the last saved cloud version (`embeddables diff` with no extra arguments defaults to `--from latest --to local`). For a deeper look, pass `--depth props` to see individual prop changes, or `--page <key>` / `--component <key>` to filter to specific pages or components. You can also compare two cloud versions directly (e.g. `embeddables diff --from 46 --to 47`). For a raw JSON diff, open `embeddable.json` and the latest `embeddable-<branch>@<version>.json` in `.generated/` (e.g. `embeddable-main@47.json`) side by side in VS Code or Cursor: **Shift+click** both files, then **right-click → Compare Selected**. * **Branches**: Use `embeddables branch` to switch branches; branch/version are stored in `config.json`. Pass `--branch <id|name>` to skip the interactive prompt (useful in CI). CLI-only branches can exist that aren't editable in the Builder; the Builder remains the merge point for production. * **Save conflicts**: `embeddables save` uses the version in `config.json` (or `.generated/`) for optimistic concurrency. If the server has a newer version, you'll be prompted; resolve by pulling the latest or force-saving when appropriate. In non-interactive environments (CI, scripts), pass `--force` to skip confirmation prompts for version conflicts and other users' drafts. * **Custom engine**: Use `embeddables dev --local` or `--engine <url>` for local or custom Engine origins. * **Build only**: Run `embeddables build -i <id>` to compile to `.generated/embeddable.json` without saving; then optionally `embeddables save --skip-build`. *** ## Best practices 1. **Save frequently** (e.g. every 10 minutes or after significant edits) to avoid large conflicts. 2. **Use the Builder** to diff and review after saving from the CLI; helps catch translation or styling differences. 3. **Test in both** local preview (`embeddables dev`) and (when applicable) live/staging to catch environment-specific issues. 4. **Use Workbench** (on by default for localhost previews from `embeddables dev`; use `?workbench=true` on non-localhost links) to inspect user data, computed fields, and actions while debugging. 5. **Use `--fix`** on pull or build if you need to recover from missing required props (removes invalid components with a warning). # Embeddables CLI Source: https://docs.embeddables.com/cli/overview Build and manage Embeddables locally with code — for developers and AI-assisted builders <Warning> **The Embeddables CLI is currently in Beta.** Use it with caution, update the package frequently, and always check the JSON diff before saving, merging or pushing to production. </Warning> <Info> **Current version: 0.16.7** — See the [CLI Changelog](/reference/changelog/cli-changelog) for what's new. </Info> The Embeddables CLI lets you build, edit, and manage Embeddables using **code** instead of (or alongside) the web Builder. It turns Embeddables into **TypeScript/TSX** you can edit in any editor, with **hot reload** and strong support for **AI assistants** (Cursor, Claude, etc.). <Info> **Who is this for?** Both **technical developers** (full control, scripts, CI) and **non-technical builders** who "vibe-code" with AI: the CLI gives you a clear file structure and prompts so AI can edit pages, styles, and logic reliably. </Info> ## Requirements * **Node.js** >= 20.0.0 <AccordionGroup> <Accordion title="Installing Node.js and NPM on macOS"> The Embeddables CLI requires Node.js (which includes NPM). If you don't have Node.js installed: **Option 1: Using Homebrew (Recommended)** If you have Homebrew installed: ```bash theme={null} brew install node ``` **Option 2: Download from Node.js website** 1. Visit [nodejs.org](https://nodejs.org/) 2. Download the LTS (Long Term Support) version for macOS 3. Run the installer and follow the prompts **Troubleshooting on macOS:** If you see errors about missing command line tools or Xcode, you may need to install Xcode Command Line Tools first: ```bash theme={null} xcode-select --install ``` Click "Install" in the dialog that appears, then try installing Node.js again. **Verifying installation:** After installation, open a new terminal window and verify: ```bash theme={null} node --version npm --version ``` Both commands should print version numbers. </Accordion> <Accordion title="Installing Node.js and NPM on Windows"> The Embeddables CLI requires Node.js (which includes NPM). If you don't have Node.js installed: **Option 1: Using Node.js installer (Recommended)** 1. Visit [nodejs.org](https://nodejs.org/) 2. Download the LTS (Long Term Support) version for Windows 3. Run the `.msi` installer 4. Follow the installation wizard (keep all default options) 5. Restart your terminal or Command Prompt **Option 2: Using Chocolatey** If you have Chocolatey installed: ```bash theme={null} choco install nodejs ``` **Verifying installation:** After installation, open a new terminal window and verify: ```bash theme={null} node --version npm --version ``` Both commands should print version numbers. If you see "command not found" errors, restart your terminal or computer. </Accordion> </AccordionGroup> ## Installation Install the CLI globally: ```bash theme={null} npm install -g @embeddables/cli ``` Check your version: ```bash theme={null} embeddables -v ``` ## Upgrading When a new CLI version is released: ```bash theme={null} embeddables upgrade ``` This installs the latest stable version from npm. ## Quick Start ```bash theme={null} # 1. Log in (applies globally) embeddables login # 2. Create a project folder for your Embeddables project and go into it mkdir my-embeddable-project cd my-embeddable-project # 3. Initialize from inside that folder (creates config, types, embeddables/ with subfolders per Embeddable) embeddables init # 4. Pull an Embeddable (interactive list if logged in) embeddables pull # 5. Start the dev server with hot reload embeddables dev ``` After `embeddables pull`, you'll have TSX pages, styles, computed fields, and actions on disk. After `embeddables dev`, open the **Preview URL** in your browser; edits to files will recompile and refresh automatically. ## Project Structure Run **`embeddables init`** from inside a project folder you've created for your Embeddables project. That folder will hold **all** Embeddables for that project—the CLI creates an `embeddables/` directory and, as you pull, one subfolder per Embeddable (e.g. `embeddables/<embeddable-id>/`). `embeddables init` creates (or updates) project-level files. `embeddables pull` adds one folder per Embeddable under `embeddables/`: ``` my-project/ ├── embeddables.json # Project ID and org (from init) ├── tsconfig.json # TypeScript/JSX config (created by init, or patched to add type paths if it already exists) ├── default-styles.css # Baseline styles for standard HTML tags (created by init) ├── assets.json # Uploaded asset URLs (synced by `embeddables assets sync`) ├── .types/ # Type stubs for editor autocomplete (no npm install needed) ├── .gitignore # Updated to ignore .generated/ and node_modules/ ├── .cursor/rules/ # Cursor AI rules (optional, from init) ├── .claude/ # Claude project context (optional, from init) ├── AGENTS.md # Codex context (optional, from init) ├── GEMINI.md # Gemini CLI context (optional, from init) ├── .agent/rules/ # Antigravity IDE context (optional, from init) └── embeddables/ ├── tsconfig.json # TypeScript config for embeddables/ (auto-generated by pull) └── <embeddable-id>/ ├── pages/ # One .page.tsx file per page ├── global-components/ # Shared components (e.g. before_page.location.tsx) ├── styles/ # CSS (e.g. index.css) ├── computed-fields/ # Custom computed field logic (.js) ├── actions/ # Data output / action handlers (.js) ├── config.json # Embeddable metadata, page list, version ├── metadata.json # Title and basic info └── .generated/ # Compiled JSON (used by dev/save; avoid editing) ├── embeddable.json # Current compiled output └── embeddable-<branch>@<version>.json # Saved versions (for comparison) ``` * **Pages**: `pages/<pageKey>.page.tsx` — React components built from Embeddable primitives. * **Global components**: One file per location (e.g. `before_page.location.tsx`, `after_components.location.tsx`). * **config.json**: Holds page keys, version, branch info; the compiler merges this with the compiled pages/styles/actions/computed fields. ## Commands The CLI provides commands for setup, daily workflow, building, and debugging. Here's a quick overview: | Command | Description | | --------------------------------- | ------------------------------------------------------------------------------------- | | `embeddables login` | Log in to your Embeddables account. | | `embeddables init` | Initialize a project folder with config, types, and AI context. | | `embeddables pull` | Fetch an Embeddable from the cloud and reverse-compile to TSX. | | `embeddables dev` | Start the dev server with hot reload. | | `embeddables save` | Build and upload the Embeddable to the cloud. | | `embeddables diff` | Compare two versions of an Embeddable (default: latest vs local). | | `embeddables branch` | Switch to a different branch (interactive list or `--branch`), then pull that branch. | | `embeddables branches create` | Create a new branch from the current Embeddable state. | | `embeddables experiments create` | Create a new experiment. | | `embeddables experiments connect` | Connect an existing experiment. | | `embeddables assets upload` | Upload a local asset folder to the Embeddables asset store. | | `embeddables assets sync` | Sync uploaded asset metadata into a local `assets.json` file. | | `embeddables tasks list` | List all tasks for the current project. | | `embeddables tasks get` | Fetch full details for a task by ID. | | `embeddables tasks update-status` | Change a task's status. | | `embeddables tasks assign` | Change a task's assignee. | | `embeddables tasks comment` | Add a comment to a task. | | `embeddables tasks add-branch` | Link an Embeddables branch to a task. | | `embeddables build` | Compile TSX to JSON without uploading. | | `embeddables inspect` | Round-trip compile and compare outputs for debugging. | | `embeddables feedback` | Send feedback about the CLI from your terminal. | <Card title="Full command reference" icon="rectangle-terminal" href="/cli/commands"> See all commands, flags, and options. </Card> ## Using the CLI with AI The CLI is built for **AI-assisted editing**. Running `embeddables init` injects context files for **Cursor**, **Claude**, **Codex**, **Gemini**, and **Antigravity** so AI assistants understand Embeddable components, keys, and conventions out of the box. <Card title="AI assistant setup" icon="wand-magic-sparkles" href="/cli/ai"> Learn how to set up Cursor, Claude, Codex, Gemini, and Antigravity for the best editing experience. </Card> ## Next steps <CardGroup> <Card title="Development workflow" icon="code" href="/cli/development"> Dev server, Workbench, TypeScript authoring, and best practices. </Card> <Card title="FAQs" icon="circle-question" href="/faqs#cli"> Common questions about the CLI, components, and workflows. </Card> <Card title="Troubleshooting" icon="wrench" href="/cli/troubleshooting"> Fix common errors and learn about current limitations. </Card> <Card title="CLI Changelog" icon="list" href="/reference/changelog/cli-changelog"> See what's new in each CLI release. </Card> </CardGroup> # Troubleshooting Source: https://docs.embeddables.com/cli/troubleshooting Fix common CLI errors and learn about current limitations ## Current limitations * **You can't create Embeddables** from the CLI; you can only pull and edit existing Embeddables (create new ones in the Builder). * **Asset browsing and deletion** are not yet available from the CLI. Use the Builder to browse the full asset library or delete assets. To upload local assets and reference their URLs in code, use `embeddables assets upload` and then `embeddables assets sync` to pull the URLs into `assets.json`. * **No creating new experiments** from the CLI; use the Builder to create experiments, then connect with `embeddables experiments connect`. You can create and switch branches with `embeddables branches create` and `embeddables branch`. * **`embeddables dev` doesn't yet work** for Embeddables that use the CMS feature; use the Builder preview or a deployed embed for those. * **Multi-language Embeddables** are supported in round-trip compilation; some edge cases with complex language configurations may still arise — report them if you encounter them. * **Internet required** for the dev server when using the default (remote) Engine. *** ## Common issues <AccordionGroup> <Accordion title="I don't have Node.js or NPM installed"> The Embeddables CLI requires Node.js version 20.0.0 or higher (which includes NPM). See the [Installation instructions in the Overview](/cli/overview#requirements) for step-by-step setup guides for macOS and Windows. </Accordion> <Accordion title="'npm' is not recognized as an internal or external command (Windows)"> This error means Node.js and NPM are not installed, or not in your system PATH. See the [Installation instructions in the Overview](/cli/overview#requirements) for how to install Node.js and NPM on Windows. After installing, restart your terminal or Command Prompt. </Accordion> <Accordion title="npm: command not found (macOS/Linux)"> This error means Node.js and NPM are not installed, or not in your PATH. See the [Installation instructions in the Overview](/cli/overview#requirements) for how to install Node.js and NPM on macOS. After installing, open a new terminal window and try again. </Accordion> <Accordion title="'embeddables' is not recognized or embeddables: command not found after installation"> After running `npm install -g @embeddables/cli`, if you get "command not found" when running `embeddables`: 1. Close and reopen your terminal (or Command Prompt on Windows) 2. Verify the global npm bin directory is in your PATH: ```bash theme={null} npm config get prefix ``` The bin folder inside that directory should be in your PATH. 3. On macOS/Linux, you may need to add npm's global bin to your PATH in `~/.zshrc` or `~/.bashrc`: ```bash theme={null} export PATH="$(npm config get prefix)/bin:$PATH" ``` Then restart your terminal or run `source ~/.zshrc` (or `~/.bashrc`). 4. On Windows, the npm global folder should be added automatically, but you may need to restart your computer. If Node.js and NPM aren't installed yet, see the [Installation instructions in the Overview](/cli/overview#requirements). </Accordion> <Accordion title="'No Embeddables found' or 'No Embeddable selected' when running dev or save"> Run from the **project root** (where `embeddables.json` and the `embeddables/` folder are), or from inside `embeddables/<id>/` so the CLI infers the Embeddable, or pass `-i <embeddable-id>`. If you haven't pulled yet, run `embeddables pull` first so that `embeddables/<id>/` exists. </Accordion> <Accordion title="Port 3000 is already in use"> The CLI will try the next available port and print the actual URL. To force a port: `embeddables dev --port 3001`. </Accordion> <Accordion title="Preview URL loads but shows a blank or wrong Embeddable"> Confirm the URL includes `?id=<embeddable-id>&version=latest`. If you have multiple Embeddables, make sure the ID in the URL matches the one you're editing and the one that is running in the dev server. Restart `embeddables dev` and do a hard refresh (e.g. Cmd+Shift+R). </Accordion> <Accordion title="Build fails: “duplicate id” or “duplicate key”"> Every component must have a unique `id` and `key` in the Embeddable. Search your pages and global components for the repeated value and rename one. You can run `embeddables build -i <id> --fix` (or pull with `--fix`) to let the CLI fix some issues automatically; check the output for warnings. </Accordion> <Accordion title="Build fails: “key must not start with a number”"> Keys (and option button keys) must not start with a digit. Use a prefix, e.g. `option_1_to_2_weeks` or `range_1_to_2_weeks` instead of `1_to_2_weeks`. </Accordion> <Accordion title="Save fails with “version conflict” or “newer version on server”"> Pull the latest: `embeddables pull -i <id>`. Merge or re-apply your local changes, then run `embeddables save` again. Only force-save if you intend to overwrite the server version. </Accordion> <Accordion title="After pull, I see duplicate or wrong global components"> Rarely, global component files can get out of sync. You can delete the `global-components/` folder for that Embeddable and run `embeddables pull -i <id>` again to regenerate them. Back up any local edits first. </Accordion> <Accordion title="Styles look different in preview vs my site"> The dev preview uses the same Engine as production, but your site might have different CSS (e.g. Tailwind preflight) that affects embedded content. Test on a page that matches your production environment, or use the Builder's "Preview on your site" if available. Some Tailwind resets can leak into the embed; scope or override them in your embed wrapper if needed. </Accordion> <Accordion title="“Cannot find module ‘@embeddables/cli/components’” in editor"> Run `embeddables init` (or re-run it) so that `.types/` is created. The CLI also ensures `tsconfig.json` has the required `baseUrl` and `paths` entries — if you already had a `tsconfig.json`, `init` will patch it rather than replacing it. If you removed `.types/`, run `embeddables init` again to regenerate type stubs and repair the path mappings. </Accordion> <Accordion title="Login or pull fails with a network error"> Check your internet connection. The CLI talks to Embeddables APIs and (for dev) the production Engine. If you're behind a strict firewall or proxy, you may need to allow access to Embeddables endpoints. </Accordion> <Accordion title="Dev server says “using port X instead”"> The default port (3000) was in use, so the CLI picked the next free port. Use the URL printed in the terminal. To avoid this, run `embeddables dev --port <port>` with a port you know is free. </Accordion> <Accordion title="I get a compile error in a .page.tsx file (e.g. missing prop, wrong type)"> Open the file and fix the reported prop or type. The compiler uses the same types as in `.types/`. For CustomButton, ensure `action` is one of: `next-page`, `prev-page`, `no-action`, `open-url`, `open-info-box`, `close-popup`, `reset`, or `page-<pageKey>`. For OptionSelector buttons, ensure each button has a valid `key` (no leading digit). </Accordion> <Accordion title="Workbench doesn't appear or doesn't load"> On localhost (`embeddables dev`), the Workbench is on by default—if you hid it with `?workbench=false`, remove that param or use `?workbench=true`. On non-localhost preview links, you must add `?workbench=true` to show it. Ensure the preview is from the dev proxy (localhost) or a URL that supports Workbench. If it still fails, try a different browser or clear cache. </Accordion> <Accordion title="Workbench is in the way or blocking something — how do I hide it?"> On localhost previews from `embeddables dev`, the Workbench is on by default. To hide it, add `?workbench=false` to the preview URL in your browser (e.g. `http://localhost:3000?id=<embeddable-id>&version=latest&workbench=false`). Reload the page and the Workbench panel will be hidden so you can see or click the content underneath. </Accordion> <Accordion title="I want to undo a save"> The CLI doesn't version for you. Use the Embeddables Builder to view version history and roll back to a previous version if your project has that feature. Going forward, save often in smaller steps so you can revert via the Builder if needed. </Accordion> <Accordion title="When I report an issue, what should I include?"> Include your **CLI version** (<code>embeddables -v</code>), the **Embeddable** you're on (ID and version), and the **commands or prompts** you used. Check the [latest version on npm](https://www.npmjs.com/package/@embeddables/cli)—if you're not on the latest, run <code>embeddables upgrade</code> and try again; many issues are fixed in newer releases. </Accordion> </AccordionGroup> *** ## Reporting issues When you report a problem or ask for help, it's very helpful to include: * **CLI version** — Run `embeddables -v` and share the number. * **Embeddable** you're working on (ID and version from `config.json` or the Builder). * **Commands or prompts** you ran (and any options). Also **compare your CLI version to the latest on npm** ([@embeddables/cli](https://www.npmjs.com/package/@embeddables/cli)). If you're not on the latest, try **`embeddables upgrade`** first—many issues are fixed in newer releases. # FAQs and Troubleshooting Source: https://docs.embeddables.com/faqs Common questions, solutions to issues, and troubleshooting tips ## The Embeddables Platform ### Compliance & Security <AccordionGroup> <Accordion title="Are you HIPAA compliant?"> Yes, we are HIPAA compliant. However, customers need to follow our HIPAA compliance checklists and requirements to maintain compliance. </Accordion> <Accordion title="Are you SOC2 compliant?"> No, we are not SOC2 compliant at this time. </Accordion> <Accordion title="Do you sign BAAs (Business Associate Agreements)?"> No, we do not sign BAAs. </Accordion> </AccordionGroup> ### Data Processing & Privacy <AccordionGroup> <Accordion title="Do you process or store personal data?"> Our platform is used to build and embed frontend components. These components are then embedded or hosted on your website. The amount of data we process depends on your preferences. If you want to use our platform's analytics features, then our platform collects data from your end-users such as IP addresses, browser information, and page views. If you don't want to use our platform's analytics features, then we don't need to collect, process, or store any personal end-user data. You can choose instead to collect and store data through your own backend. You can send directly from the user's browser to your designated backend without it passing through our infrastructure. </Accordion> <Accordion title="Do you provide DPAs (Data Processing Agreements)?"> Customers on our Pro and Enterprise plans can request us to review and sign a DPA. For customers on lower plans, if you require a DPA with any vendors who process personal data, then we recommend that you send data directly from the user's browser to your backend, without passing through our infrastructure (which would disable most of Embeddables' built-in analytics features). </Accordion> <Accordion title="Do you conduct DPIAs (Data Protection Impact Assessments)?"> We do not currently have a DPIA. </Accordion> </AccordionGroup> ### Infrastructure & Hosting <AccordionGroup> <Accordion title="Where is your code/platform hosted?"> Our platform is hosted on the following providers: * Cloudflare * Vercel * Heroku * Supabase * Firebase * Clickhouse Our codebase itself is hosted on GitHub. </Accordion> <Accordion title="Are Embeddables embedded using iframes?"> No, Embeddables are not embedded using iframes. They are embedded directly into your page as native HTML/CSS/JavaScript components. This approach provides several benefits: * Better performance and faster load times * Seamless integration with your page's styling * No cross-origin restrictions * Better SEO as content is part of your page * Easier data passing between your site and the Embeddable * Better support for Stripe payments, since Stripe uses its own iframes (avoiding nested iframes) </Accordion> </AccordionGroup> ## Building with Embeddables ### Troubleshooting <AccordionGroup> <Accordion title="Builder not loading or stuck in infinite loop"> If your Builder is not loading or appears to be stuck in an infinite loop, check if any Computed Field keys share the same name as other keys in your User Data (such as option button keys or other input field keys). **Problem**: Computed Fields with the same name as other keys can cause infinite loops that prevent the Builder from loading. **Solution**: Rename the Computed Field to have a unique key name. For example, if you have an option buttons field called `health_conditions_options`, rename your Computed Field to something like `health_conditions_options_cf` (adding a suffix like `_cf` for "Computed Field"). <Warning> Computed Field keys must be unique and cannot share the same name as any other key in your User Data JSON object. </Warning> <Tip> If you can't fix your changes because the Builder doesn't load, the problem is likely on the latest version. Switch to the previous version (if possible) and save from there. You'll need to redo your latest changes without causing the bug. </Tip> </Accordion> </AccordionGroup> ### Logic <AccordionGroup> <Accordion title="How do I find where a User Data key is being created/updated?"> If you need to find where a specific User Data key is being created or updated in your Embeddable, you can search through the entire Embeddable JSON. **How to search:** 1. **Open the Builder** for your Embeddable 2. **Press ESC** to deselect any components (so nothing is selected) 3. **In the Options sidebar**, click the **three dots menu** (⋮) in the top right corner 4. **Click "Edit JSON"** to open the JSON editor modal 5. **Use the search inside the code editor box** (Cmd+F or Ctrl+F with the code editor in focus) to search for the key name within the JSON This will show you all locations where that key appears in your Embeddable configuration, including: * Input fields and form components that create the key * Actions that update the key * Computed Fields that are set with their key as the key you're looking for, or that reference the key * Conditions that use the key <Tip> We're working on shipping a better global search feature that will make this process easier. For now, searching the Entire Embeddable JSON is the most reliable way to find where keys are being used. </Tip> </Accordion> </AccordionGroup> ### Design, Styling, and Layouts <AccordionGroup> <Accordion title="How do I place components side by side?"> To place components side by side (horizontally), first those components need to be inside a container. 1. **Add a container** to the page if it does not already exist. 2. For each component that you want to align horizontally, **move it into the container**. 3. Select the container, and in the Designer sidebar, set the **`display property to flex`**. 4. Ensure the **`flex direction is set to row`** (this is usually the default). <Tip> If components are still stacking vertically, check that the container does not have `flex-direction: column` set. </Tip> </Accordion> <Accordion title="How do I center a button in my design?"> When you want to center a button horizontally on your page, place the button inside a container and use flexbox to center it. 1. **Select the container** that includes ONLY the button (or create one if it doesn't exist). 2. In the Designer sidebar, **`set the container's width to 100%`**. 3. **`Set the display property`** of the container to `flex`. 4. Ensure the **`flex direction is set to row`** (this is usually the default). </Accordion> <Accordion title="How do I edit styles for specific screen sizes?"> To apply different styles for mobile, tablet, and desktop: 1. **Select the component** you want to style. 2. **In the Designer sidebar**, look for the "On" dropdown. 3. **Choose the specific screen size** you want to style for: * Mobile: up to 520px width * Tablet: 521px to 720px width * Desktop: 721px width and above 4. **Apply your styles** while that screen size is selected - this will set the styles for only that screen size and smaller screen sizes. </Accordion> <Accordion title="What do the device screen type options (Desktop/Mobile/Tablet) do?"> The device screen type options in the Builder allow you to preview how your content will appear on different screen sizes: * **Mobile**: Shows layout for devices with width up to 520px. * **Tablet**: Shows layout for devices with width between 521px and 720px. * **Desktop**: Shows layout for devices with width of 721px and above. These are preview modes that help you see how your design will look on different devices. To actually edit styles for specific screen sizes, use the "On" dropdown in the Designer sidebar. You can also use the "On" dropdown to preview how your design will look on different screen sizes. </Accordion> <Accordion title="Why do my desktop styles affect mobile too?"> In our Builder, styles cascade downward from larger screens to smaller screens, similar to how CSS works. This means: * **Desktop styles** (721px+) apply to desktop, tablet, and mobile * **Tablet styles** (521px-720px) apply to tablet and mobile * **Mobile styles** (up to 520px) apply only to mobile This design choice was made based on feedback because most Embeddables users design for desktop first, then adapt for smaller screens. **To fix unwanted style inheritance:** 1. **Select the component** that has unwanted styles on mobile 2. **Switch to Mobile view** using the "On" dropdown in the Designer sidebar 3. **Override the problematic styles** by setting different values for mobile **Example:** If you set a width of 300px on desktop but want full width on mobile: * Desktop: width = 300px * Mobile: width = 100% </Accordion> <Accordion title="What are the different button elements I can style?"> Button components contain several elements you can style individually: * **Whole thing**: Apply styles to the entire button (background, border, etc.). * **Text Wrapper**: Style the container that holds the button's text. * **Text**: Style the text itself (font, color, size, etc.). * **Description**: Style any description text (if present). * **Icon**: Style the icon (if present). * **Image**: Style any image within the button (if present). The available elements depend on what content your button contains. For a simple text button, you'll primarily use "Whole thing", "Text Wrapper", and "Text". </Accordion> <Accordion title="Why does my Embeddable look different on my website than in the preview?"> If your Embeddable appears different when embedded on your website compared to the builder preview, this is typically caused by CSS conflicts between your website's global styles and the Embeddable's styles. **Common causes:** * **Global CSS styles** on your page are overriding the Embeddable's styles * **Margin or padding** applied to elements inside the Embeddable (e.g., body margins preventing edge-to-edge display) * **Font styles** or other inherited CSS properties from your page * **CSS reset or normalize** stylesheets affecting the Embeddable's layout **How to fix:** 1. **Inspect the Embeddable** on your website using browser developer tools (right-click → Inspect) 2. **Look for conflicting styles** that are being applied from your website's CSS 3. **Override the conflicting styles** by adding more specific CSS rules to your page: ```css theme={null} /* Example: Remove body margin affecting the Embeddable */ .embeddables-container body { margin: 0; } /* Example: Reset specific styles for the Embeddable */ .embeddables-container * { box-sizing: border-box; } ``` 4. **Test the changes** to ensure the Embeddable displays correctly <Tip> The Embeddable is embedded as native HTML/CSS/JavaScript (not an iframe), which means it inherits styles from your page. This provides better performance and integration but requires careful CSS management. </Tip> </Accordion> </AccordionGroup> ### Form Elements <AccordionGroup> <Accordion title="How do I disable a button based on input validation?"> You can disable a button based on whether inputs on the page are completed or validated directly from the button settings. 1. **Select the button** you want to configure. 2. In the button settings, find the **Validation** section. 3. Look for the option **"React if page requirements not met"**. 4. **Set it to "Disable"**. The button will automatically be disabled if the inputs on the page aren't completed or validated. Once all required inputs are filled and validated, the button will become enabled. <Tip> This works with any input fields that have validation rules set up. The button will check all inputs on the page and disable itself if any requirements aren't met. </Tip> </Accordion> <Accordion title="How do I reference multiple form elements in Computed Fields?"> To access multiple form element values in a Computed Field, you can pull the keys of each input component from the first argument of the function. <CodeGroup> ```javascript Basic theme={null} // Computed Field: full_name function result(userData) { return `${userData.first_name} ${userData.last_name}`; } ``` ```javascript With Destructuring theme={null} // Computed Field: full_name function result({ first_name, last_name }) { return `${first_name} ${last_name}`; } ``` </CodeGroup> The Computed Field receives a single object containing all your form element results. Make sure the property names in the destructuring match exactly with your form element names. </Accordion> <Accordion title="How do I add a phone number input field?"> 1. Add a regular Input component. 2. In the Options tab, set the `Input Type` to `Phone`. Optional: To validate the phone number: 1. Under the `Validation` section, check `Validate value`. 2. Set `Formula` to `Phone Number (US)` or `Custom formula`. 3. If you choose `Phone Number (US)`, the phone number will be automatically formatted as `(xxx) xxx-xxxx`. 4. If you choose `Custom formula`, you can use the something like the following formula to validate the phone number: ```javascript theme={null} function validate(value) { return /^[\d\s\(\)\-]+$/.test(value); } ``` This formula will allow numbers, parentheses, spaces, and dashes. </Accordion> <Accordion title="Can I prevent users from entering text in a phone input field?"> Yes, there are several approaches to restrict phone input to numbers only: #### Option 1: Use the built-in US phone validation (Recommended) The simplest and most reliable approach is to use the built-in phone validation: 1. Set the Input component's `Input Type` to `Phone` 2. In the Validation section, check `Validate value` 3. Set the Formula to `Phone Number (US)` This automatically: * Formats the number as `(xxx) xxx-xxxx` as the user types * Validates the format is correct * Provides a consistent user experience * Handles edge cases and formatting automatically <Tip> The built-in validation is the most reliable option and handles all US phone number formats automatically. </Tip> #### Option 2: Custom validation with regex For more control over the validation rules, you can use a custom formula: 1. Set the Input component's `Input Type` to `Phone` 2. In the Validation section, check `Validate value` 3. Set the Formula to `Custom formula` 4. Write a custom function to validate the phone number, for example: ```javascript theme={null} function validate(value) { return /^[\d\s\(\)\-]+$/.test(value); } ``` <Warning> Custom validation gives you more control but requires more testing and maintenance. The built-in option is recommended for most use cases. </Warning> #### Option 3: Remove disallowed characters immediately on type, using a custom action Create an action that runs on page load to restrict input to numeric characters only: ```javascript theme={null} function output(_, { setUserData }) { const input = document.querySelector(".ComponentKey-phone input"); if (!input) return; input.setAttribute("inputmode", "numeric"); input.addEventListener("input", (e) => { let value = e.target.value; let digits = value.replace(/\D/g, ''); if (digits.length > 10) { digits = digits.slice(0, 10); } // Remove letters but keep formatting characters value = value.replace(/[A-Za-z]/g, ''); e.target.value = value; }); } ``` <Warning> Using custom actions to restrict input may interfere with the automatic phone number formatting. The built-in phone validation is simpler and more reliable. </Warning> </Accordion> <Accordion title="Does the phone input automatically format numbers?"> Yes! When you set an Input component's type to `Phone`, and choose `Phone Number (US)` for validation, it automatically formats the number as `(xxx) xxx-xxxx` as the user types. This provides a better user experience and ensures consistent formatting across all submissions. <Tip> The automatic validation won't prevent users from entering text, but it will prevent them from submitting the form if the phone number is invalid, showing them a validation error message. </Tip> </Accordion> </AccordionGroup> ### Versions & Publishing <AccordionGroup> <Accordion title="If I edit an old version, do those edits become the newest version?"> Yes. When you edit an old version and save it, those edits become the newest version. However, you'll receive a warning before saving that alerts you that you're about to skip/overwrite changes from other versions. This means your new version will include your changes but will **not** include changes made in the intervening versions. The Builder will show a "From version XXX" message in the Version Control card to indicate which version you're editing from. <Tip> To avoid accidentally overwriting recent changes, always check the Version Control card to see which version you're currently editing before making changes. </Tip> </Accordion> <Accordion title="How do I roll back to a previous version?"> To roll back to a previous version: 1. **Open the Version Control card** in the Builder sidebar. 2. **Find the version** you want to roll back to in the version history list. 3. **Click on that version** to load it in the Builder. 4. **Make a small edit** (the Save button won't be active until you make a change). 5. **Click Save** - this creates a new version based on the old one. When you save, you'll see a warning that you're about to skip/overwrite changes from intervening versions. This is expected when rolling back. <Warning> Rolling back creates a new version based on the old one, but does **not** include changes made in versions between the old version and the current version. Those changes will be skipped. </Warning> <Tip> The rolled-back version becomes your newest version. You can always access any previous version (including the ones you rolled back from) in the version history. </Tip> </Accordion> <Accordion title="Where can I see the audit trail of staging and production deployments?"> To view the audit trail of staging and production deployments: 1. **Click on Version Control** in the top bar of the Builder. 2. **Go to the Logs tab**. 3. This shows an audit trail of staging and production deployments. <Tip> The Logs tab provides a complete audit trail of staging and production deployments, including timestamps and version numbers. </Tip> </Accordion> <Accordion title="How do I compare changes between two versions?"> To see what changed between two versions: 1. **Open Version Control** in the Builder Top Bar. 2. **Click the `<>` icon** (compare icon) in Version Control. 3. **Select the two versions** you want to compare. This will show you the differences between the selected versions, helping you understand what edits were made. <Tip> This is especially useful when you need to understand what changed between an older version and the current one, or when investigating performance differences between versions. </Tip> </Accordion> </AccordionGroup> ## Data & Analytics <AccordionGroup> <Accordion title="How do I find my funnel submissions data?"> Your users' data can be found in two places: 1. **Users page** * Shows individual user sessions. * You can click on a user to see their activity and their submission data for each Embeddable. 2. **Embeddable --> Submissions tab** * Shows the User Data for completed form submissions. * Each user will have a separate row for each Embeddable they have interacted with, meaning there could be multiple rows for the same user. * All users from all stages of the funnel are included, so the majority of rows will likely correspond to users who did not reach the end of the funnel. </Accordion> <Accordion title="How do I access production data vs. test data?"> To switch between viewing production data vs viewing all data (including test data): 1. **Go to your funnel dashboard**. 2. **Look for the "Data collected in production version" button**. 3. **Click it to toggle** between: * **Production data only** * **All data (including both production and test data)** </Accordion> <Accordion title="How do I capture UTM parameters and campaign IDs from incoming links?"> You can capture UTM parameters and campaign IDs from incoming links in two ways: **Method 1: Using URL Keys (Recommended)** 1. In the Builder, select nothing (press ESC) to see the Options tab for the Entire Embeddable settings. 2. Go to the **Options tab** and find the **URL Keys** section. 3. Add the UTM parameters you want to capture (e.g., `utm_source`, `utm_campaign`, `utm_medium`) - add one on each line. 4. These values will automatically be stored in UserData when users visit your page. **Method 2: Using Custom Code** Create an Action with custom code, and set up a Trigger that executes the Action when the Embeddable is loaded: ```javascript theme={null} function output(userData) { // Get UTM parameters from the URL const params = new URLSearchParams(window.location.search); const utmSource = params.get("utm_source"); const utmCampaign = params.get("utm_campaign"); const utmMedium = params.get("utm_medium"); // Store in UserData or use as needed return { utm_source: utmSource, utm_campaign: utmCampaign, utm_medium: utmMedium }; } ``` <Tip> The URL Keys method is simpler and automatically handles the capture. Use custom code when you need more complex logic or want to process the UTM parameters before storing them. </Tip> </Accordion> <Accordion title="Why can't I see user details or search by email address?"> If you cannot see user details or search for users by email address, this is likely due to your data access permissions. This restriction is in place to maintain [HIPAA Compliance](/guides/hipaa-compliance) and protect sensitive user information. **Why this happens:** * Your workspace has HIPAA Compliance enabled, which restricts access to personally identifiable information (PII) and protected health information (PHI). * Your account has **Anonymized Data Access**, which means you can only view anonymized data—personally identifiable information about end-users is hidden. * Email addresses are classified as contact information and PII, so they are not visible to users with Anonymized Data Access. **How to get access:** To view user details and search by email address, you need **Full Data Access** permissions. Contact your workspace Admin or Data Privacy Officer to request an access level upgrade. Your Admin can update your permissions by: 1. Going to **Settings** → **Team** tab 2. Finding your user in the table 3. Changing your Data Access Level to **Full Data Access** <Warning> Full Data Access should only be granted to team members who have a clear, documented need to view personally identifiable or health information. This is a HIPAA Compliance requirement. </Warning> For more information, see: * [User Roles & Permissions](/guides/user-roles-permissions) * [HIPAA Compliance](/guides/hipaa-compliance) </Accordion> </AccordionGroup> ## CLI <AccordionGroup> <Accordion title="Do I need to install the CLI in my project as well as globally?"> No. Install globally with `npm install -g @embeddables/cli`. Your project only needs the files created by `embeddables init` (including `.types/` for editor support). You do not add `@embeddables/cli` as a dependency in `package.json` for normal use. </Accordion> <Accordion title="Where is my login stored?"> Login is stored globally on your machine (not per project). Use `embeddables logout` to clear it. </Accordion> <Accordion title="Can I use the CLI without ever using the Builder?"> You can build and save Embeddables entirely from the CLI. The Builder is still the recommended place to create experiments, upload assets, and for non-technical teammates to review and merge. Many teams use both: CLI for fast iteration and AI-assisted edits, Builder for final review and publishing. </Accordion> <Accordion title="What is the difference between id and key on components?"> Both must be unique in context. `id` is the stable internal identifier (e.g. `comp_12345`). `key` is used in React and for targeting (e.g. in conditions or user data). Keys must not start with a number; use a prefix like `option_` or `range_` if the label would otherwise slug to a number. </Accordion> <Accordion title="Can I have multiple Embeddables in one project?"> Yes. Run `embeddables pull` for each; they live in `embeddables/<embeddable-id>/`. When you run `embeddables dev`, `embeddables save`, or `embeddables branch` without `-i`, you'll be prompted to choose which Embeddable to use. To avoid the prompt, **go into that Embeddable's folder** (e.g. <code>cd embeddables/\<EMBEDDABLE\_ID></code>) and run the command from there—the CLI will automatically use that Embeddable. </Accordion> <Accordion title="First pull changes the JSON too much. How can I reduce noise?"> Use <strong><code>embeddables pull --preserve</code></strong>. It keeps the same order of components in the config. Most of the "changes" you see on a first pull are from React reordering components within containers—that doesn't affect how the Embeddable renders. With <code>--preserve</code>, you may still see smaller changes (e.g. <code>parent\_key</code> fixes), but it's easier to verify what actually changed. </Accordion> <Accordion title="How do I point the dev server at a local Engine?"> Use `embeddables dev --local` to use `http://localhost:8787`. Or use `embeddables dev --engine https://your-engine.example.com` for a custom URL. </Accordion> <Accordion title="What is the Workbench and how do I open it?"> The Workbench is a debugging panel that lets you inspect user data, computed fields, actions, and navigate pages. On localhost previews from `embeddables dev`, it is shown by default; add `?workbench=false` to hide it. On non-localhost preview links, add `?workbench=true` to show it. It is served by the dev proxy and does not require a separate deploy. </Accordion> <Accordion title="Does the CLI work with Cursor / Claude / Codex?"> Yes. The CLI is designed for AI-assisted editing. Running `embeddables init` injects Cursor rules (`.cursor/rules/`), Claude project context (`.claude/`), and a Codex-compatible `AGENTS.md` file so the assistant understands the file layout, component types, and conventions. You can also use the built-in prompts from the CLI repo in your own rules. </Accordion> <Accordion title="How do I get the latest CLI version?"> Run `embeddables upgrade`. You can check the current version with `embeddables -v`. </Accordion> <Accordion title="Can I create a new branch or experiment from the CLI?"> You can create new branches with `embeddables branches create` and switch between them with `embeddables branch`. Creating experiments is still done in the Builder; use `embeddables experiments connect` to link an existing experiment to your Embeddable. </Accordion> <Accordion title="Why does save say the server has a newer version?"> Someone (in the Builder or via the CLI) saved a newer version after you last pulled. You can pull the latest with `embeddables pull` and then re-apply your changes, or force-save when the prompt offers it (use with care to avoid overwriting others' work). </Accordion> <Accordion title="What are global components and where do they live?"> Global components are shared across pages (e.g. a nav bar or footer). They live in `embeddables/<id>/global-components/` with one file per location, e.g. `before_page.location.tsx`, `after_components.location.tsx`. The `_location` value controls where they render relative to the page. </Accordion> <Accordion title="Should I use Git or version control for my Embeddable project? How do I compare changes?"> We don't currently recommend using version control (e.g. Git) in your codebase for the Embeddable files, because it can easily get out of sync with Embeddables' own version and branch system. Instead, use `embeddables diff` to compare your local files against the last saved cloud version: ```bash theme={null} embeddables diff # latest (cloud) vs local — default embeddables diff --depth props # drill into individual prop changes embeddables diff --from 46 --to 47 # compare two saved versions ``` For a raw JSON diff, open **`embeddable.json`** and the latest **`embeddable-<branch>@<version>.json`** (e.g. `embeddable-main@47.json`) inside the **`.generated/`** folder. In VS Code or Cursor: **Shift+click** or **Command/Ctrl+Click** to select both files, then **right-click → Compare Selected**. <Tip> If you see a huge number of changes after pulling, try using the `--preserve` flag with the `pull` command to keep the same order of components in the config. </Tip> <Note> The Embeddables team is exploring internally various ways of integrating with version control systems like Git and GitHub. If you have any feedback or ideas on how you could envisage this working, please reach out, we'd love to bounce ideas off you! </Note> </Accordion> </AccordionGroup> # Using Actions in Embeddables Source: https://docs.embeddables.com/features/actions Running custom code when some kind of event occurs **Actions** are a core feature of Embeddables, allowing you to run custom JS code when some kind of event occurs. Actions are initiated by **Triggers**, which are events that occur in the Embeddable. For example, a Trigger might be when a user clicks a button, or when a page is loaded. Actions have access to the [User Data](/features/user-data) JSON object, a series of helper functions like `setUserData` and `triggerAction`, and extra context about what triggered the Action. <Steps> <Step title="Create a custom code Action"> * Go to the Logic sidebar and click on the Actions tab. * Click `+ Add New Action`. * Give it a name like "Send Data to Klaviyo". * Hit `Add`. </Step> <Step title="Write your custom code"> * Add whatever custom JS you like inside the `output()` function. * The first argument of `output()` is the User Data, and the other arguments are [described here](/guides/custom-code). * The code will execute on the frontend, in the main window environment, so has access to window functions and variables. </Step> </Steps> ## Action execution environment Actions are executed client-side, in the main window environment. This means that they have access to the main window's functions and variables, such as `window.location`, `document`, and `localStorage`. This can have useful benefits, such as: 1. The ability to manipulate the DOM 2. Access to read and write to `localStorage` 3. Accessing frontend functionality that you might want to expose to the Embeddable, e.g. `window.logInToApp()`. <Warning> **Be careful to avoid exposing secure information such as API Secrets in Actions.** One workaround for this (which can also solve CORS issues) is to use the Embeddables Proxy API - ask Embeddables for more information on this. </Warning> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> ## Example Actions ### Send Data to Backend Here's an example of an Action that sends a subset of User Data to a backend. ```js theme={null} // All Actions must contain a function called output() function output(userData) { // Create an object with the data we want to send const dataToSend = { email: userData.email, name: userData.name, favorite_color: userData.favorite_color, }; // Send this data to the backend fetch("https://your-backend-url.com/", { method: "POST", body: JSON.stringify(dataToSend), }); } ``` ## Common use-cases <CardGroup> <Card title="Embeddables and Klaviyo" href="/how-to/send-data-to-klaviyo"> Setting up an Action with custom code that sends user data to Klaviyo's API </Card> <Card title="Embeddables and Telegra MD" href="/how-to/send-data-to-telegra-md"> Setting up an Action with custom code that sends user data to Telegram's API </Card> <Card title="Embeddables and Tellescope" href="/how-to/send-data-to-tellescope"> Setting up an Action with custom code that sends user data to Tellescope's API </Card> <Card title="How To: Handle Partial Data Capture" href="/how-to/partial-data-capture"> Storing incomplete user answers, even when they haven't yet completed the entire funnel. </Card> <Card title="How To: Retrieve User Data on Different Device" href="/how-to/retrieve-user-data-on-different-device"> Resuming a user's progress after remarketing to them by email or SMS </Card> <Card title="How To: Add External Scripts" href="/how-to/add-external-scripts"> Two ways to add external scripts or style libraries to your Embeddable </Card> <Card title="How To: Run custom code on Embeddable Load" href="/how-to/run-custom-code-on-load"> Running JS code on first load, e.g. to run a function or add an event listener </Card> </CardGroup> # Tracking Analytics in Embeddables Source: https://docs.embeddables.com/features/analytics Capturing and analyzing user data **Analytics** is baked into Embeddables, and can be captured and analyzed in a variety of ways. There are various ways to capture analytics in Embeddables: * Automatically captured analytics * Custom client-side events * Custom server-side events * Conversion events ## Capturing analytics ### Automatically captured analytics The following analytics are automatically captured and available to analyze: * The User * The entire [User Data](/features/user-data) JSON object is available to be captured. * You can control which of the User Data fields are sent to the Embeddables database, vs which are stored locally in the user's browser. * Events * Page views * Button Clicks, if you turn the Capature Clicks setting on for that button ### Custom client-side events You can capture custom events using custom code - it's best to do this in an [Action](/features/actions): <Steps> <Step title="Create a custom code Action, with a Trigger on the relevant event"> * Go to the Logic -> Actions, and click `+ Add New Action`. * Give it a name like "Track X event", and hit `Add`. * Add a Trigger for the relevant event, such as a button click, attaching the Action to that Trigger. </Step> <Step title="Use the `trackCustomEvent()` function"> * Inside the `output()` function, use the `trackCustomEvent()` function inside the `helperFunctions` argument. * The first argument is the name of the event, and the second is a JSON object of properties. * `trackCustomEvent()` returns a `Promise<void>` — you can `await` it if you need the event to be delivered before navigating or taking a follow-up action. * For example: ```js theme={null} function output(userData, helperFunctions) { // Track a custom event, with extra metadata properties helperFunctions.trackCustomEvent('got_recommendation', { recommendation_id: properties.recommendation_id, recommendation_name: properties.recommendation_name }); } ``` </Step> </Steps> **Structured payload for custom event data** For custom events where you want to send a mix of numeric metrics and categorical dimensions, we recommend using a controlled schema so they can be analyzed consistently. Use two optional objects inside your props: * **payload\_metrics**: A key–value object where all values are numbers (for example `order_value`, `revenue`, `subtotal`, `tax`, `score`, `duration_ms`). * **payload\_dimensions**: A key–value object where all values are strings (for example `plan`, `bundle`, `purchase_type`, `currency`, `segment`, `experiment_variant`). This follows a metrics-vs-dimensions approach and gives you flexibility without locking into fixed columns. You can use your own keys as long as values match the types above. Example for a purchase completion event using this schema: ```js theme={null} function output(userData, helperFunctions) { helperFunctions.trackCustomEvent('purchase_completed', { payload_metrics: { order_value: 99.99, revenue: 89.99, subtotal: 79.99, tax: 10.0 }, payload_dimensions: { plan: 'premium', bundle: 'annual', purchase_type: 'subscription', currency: 'USD' } }); } ``` <Tip> Keep `payload_metrics` values as numbers and `payload_dimensions` values as strings. You can add any keys you need within those constraints. </Tip> ### Custom server-side events Some events will occur somewhere other than in the Embeddable, for example a user completing a doctor's appointment. To track these events in Embeddables, you can send a server-side event to the Embeddables API. This will then be available to analyze in the Embeddables Web App: <Card title="How to send server-side events to the Embeddables API" icon="code" href="/how-to/send-data-to-embeddables"> Read more about the Embeddables API, and how to send server-side events. </Card> ### Conversion events Conversion events are a special status given to certain events of the other types described above. Conversion events are defined in the Embeddables Web App, and consist of a name, a type, and specific properties. Types of conversion events: * Page View * Properties: `page_key` * Button Click * Properties: `button_key` * Custom Event * Properties: `event_name` Conversions that relate to a page or button in an Embeddable will be shown on the relevant pages in the Pages sidebar. # Embeddables CMS Source: https://docs.embeddables.com/features/cms Use the Embeddables CMS to manage dynamic content for your Embeddables without code changes The **Embeddables CMS** is a powerful content management system that lets you control dynamic content in your Embeddables from outside the Builder. It provides a user-friendly interface for managing data like **Stripe prices** or **programmatic SEM page content**, that can be used across all your Embeddables. ## Key Benefits * **No-Code Updates**: Make content changes without modifying or deploying code. * **User-Friendly Interface**: Simple table-based interface for managing data. * **Audit Trail**: Track changes to your content over time. * **Real-Time Updates**: Changes in the CMS are immediately reflected in your Embeddables. * **Non-Technical Users**: Enable team members to update content without coding knowledge. <Card title="Control Stripe Prices from the Embeddables CMS" icon="code" href="/how-to/control-stripe-prices-from-cms"> Learn how to use the Embeddables CMS to manage Stripe prices. </Card> ## How to Use the CMS ### Creating Tables 1. Navigate to the CMS section in the Embeddables Web App. 2. Click "Edit Tables" to begin. 3. Select "+ Add Table" and choose from available templates or start with a blank table. 4. Configure your columns with appropriate names, keys, and types. ### Managing Content * Add rows to your tables with the "+ Add Row" button. * Edit existing content by clicking on cells. * Each table has a unique ID that you'll use to connect it to your Embeddables. ### Connecting to an Embeddable To use CMS data in your Embeddable: <Steps> <Step title="Add a new Content Source"> * In the Embeddables Builder, go to the Embeddable Options (make sure to deselect any pages or components by pressing Escape). * Scroll down to the CMS section and click `+ Add Content Source`. </Step> <Step title="Choose a User Data key"> <Title>Enter a key under which to store the data in the User Data.</Title> </Step> <Step title="Select a table"> <Title>Select the table you want to connect to (if you don't see any tables, make sure that you've saved the new table as a version in the CMS).</Title> </Step> <Step title="Configure which records are fetched"> * Under "Which records should get shown?", choose the method by which you want to control which record(s) are included * `Show all records` - All records in the table will be fetched (or just the first record if `Multiple Records` is left unchecked) * `Records that match a URL pattern` - Only records that match the URL pattern will be fetched (or the first record that matches if `Multiple Records` is left unchecked) * If you choose `Records that match a URL pattern`, under "Controller Key", enter a column key that will be used to match the URL pattern. * Check the "Multiple Records" checkbox if you want to fetch multiple records - e.g. to display a list of items like FAQs or Prices. </Step> <Step title="Finish setting it up"> * Add your chosen User Data key to the list of Registered Keys in the Embeddable Options. * You can now reference the data in your Embeddable, just like you would with any other User Data property. </Step> </Steps> <Warning> When testing your Embeddable on a preview link, you must add <code>savvy\_test=false</code> to the URL to ensure CMS data loads as expected. </Warning> ### Utilizing CMS Version Control Just like the Embeddables Builder, the Embeddables CMS comes with a built-in version control system that allows you to track changes to your content over time. You can save a new version, switch versions, and push a version to staging or production, from the controls in the top right of the CMS page (when Editing Tables). To preview an Embeddable with the content from a specific CMS version, you can use the `savvy_cms` URL parameter, which works in the same way as the `version` parameter: * To preview a particular version, add `savvy_cms=123` to the URL. * To preview the latest version (useful if you're making lots of updates), just use `savvy_cms=latest`. * To preview the version currently on staging, use `savvy_cms=staging`. <Tip> This is a great way to preview what your CMS content updates will look like before pushing them live. </Tip> ## Example Use Cases ### 1. Managing Stripe Prices The CMS can manage your Stripe pricing configuration, making it easy to update prices without code changes. **Example Table Structure:** | Column Name | Column Key | Purpose | | ------------------- | ----------------- | ---------------------------------------- | | **Plan Name** | `plan_name` | Identifies the pricing plan | | **Stripe Price ID** | `stripe_price_id` | Links to Stripe's price ID | | **Price Amount** | `price_amount` | Displays the actual price | | **Size** | `size` | Distinguishes between plan sizes | | **Discount Type** | `discount` | Identifies if this is a discounted price | This setup allows you to: * Maintain multiple pricing tiers. * Update prices without writing code or saving new versions. * Give non-technical team members ability to manage pricing. * Keep a clear audit trail of price changes. For a full guide on how to use the CMS to manage Stripe prices, go here: <Card title="Control Stripe Prices from the Embeddables CMS" icon="code" href="/how-to/control-stripe-prices-from-cms"> Learn how to use the Embeddables CMS to manage Stripe prices. </Card> ### 2. Programmatic SEM Pages Use the CMS to control content displayed on templated pages based on URL parameters. This is a great way to create a large number of pages from a single template, so you can utilize **Search Engine Marketing (SEM)** to target different audiences. **Example Table Structure:** | Column Name | Column Key | Purpose | | --------------- | ------------- | ------------------------------------ | | **Page URL** | `url_slug` | Defines the URL path for the content | | **Title** | `title` | Page title to display | | **Description** | `description` | Page meta description | | **Content** | `content` | Main page content | | **Image URL** | `image_url` | Featured image for the page | This enables you to: * Create hundreds of pages from a single template. * Target different audiences or SEO keywords. * Update content through the CMS interface. ## Best Practices 1. **Plan Your Schema**: Carefully design your table structure before creating it. 2. **Use Clear Names**: Choose descriptive column names and `snake_case` keys. 3. **Test Updates**: Verify changes on the latest CMS version before pushing to production. ## Available Templates The CMS comes with several pre-built templates for common use cases: * **Blank Table**: Start from scratch. * **Prices**: Manage product/service pricing. * **URLs & Links**: Handle URL routing and redirects. * **FAQs**: Organize frequently asked questions. * **Programmatic Pages**: Control dynamic page content. * **Coupons**: Manage promotional codes and discounts. Each template comes with recommended columns and structure, which you can customize to fit your needs. # Using Computed Fields in Embeddables Source: https://docs.embeddables.com/features/computed-fields Writing custom code to calculate new values based on existing data **Computed Fields** are a core feature of Embeddables, allowing you to calculate new values based on existing data. They take in inputs, from the [User Data](/features/user-data) JSON object, and return an output. This output is then part of the User Data JSON object, and can be used elsewhere in the Embeddable. For example, it can be displayed in a text component, used in a condition, or even used as an input for another Computed Field. Computed Fields have access to the User Data JSON object, a series of helper functions like `setUserData` and `triggerAction`, and extra context about how the Computed Field was triggered. ## How to create a Computed Field <Steps> <Step title="Create a Computed Field"> * Go to the Logic sidebar and click on the Computed Fields tab. * Click `+ Add New Computed Field`. * Give it a key to be used for the output value in the User Data JSON object, like `full_name`. * Hit `Add`. </Step> <Step title="Write your custom code"> * Computed Fields must start with a function called `result()`. * You can write any custom JS code inside the `result()` function. * The first argument of `result()` is the User Data, and the other arguments are [described here](/guides/custom-code). * The code will execute on the frontend, in the main window environment, so has access to window functions and variables. </Step> </Steps> <Tip> Computed Fields are executed client-side, in the main window environment. This means that they have access to the main window's functions and variables, such as `window.location`, `document`, and `localStorage`. </Tip> ## Computed Field Options Computed Fields have several configuration options that control how they behave: <AccordionGroup> <Accordion title="Storage"> Controls where and how the computed field result is stored: * **Save to User Data** (default): The result is saved to the User Data JSON object and persisted in cookies/localStorage * **Do Not Save**: The result is calculated but not saved to storage. It's still available in the User Data during the session * **Save to Cloud Only**: The result is saved to the Embeddables database but not stored locally in cookies/localStorage </Accordion> <Accordion title="Inputs"> Specifies which User Data keys the computed field depends on. When any of these keys change, the computed field will have access to their values in the `userData` parameter. **Example:** If you add `first_name` and `last_name` as inputs, your computed field will receive these values: ```js theme={null} function result(userData) { // userData.first_name and userData.last_name are available return `${userData.first_name} ${userData.last_name}`; } ``` </Accordion> <Accordion title="Triggers"> Specifies which User Data keys will trigger the computed field to re-run. This is useful when you want the field to recalculate only when specific values change, even if it depends on other inputs. **Use case:** You might have a computed field that uses many inputs but should only recalculate when a specific key changes (like `recalculate_total`). </Accordion> <Accordion title="Include and Watch Full User Data"> When enabled, the computed field: * Receives the entire User Data object (excluding its own key to prevent circular references) * Re-runs whenever **any** User Data value changes * Useful for computed fields that need access to all data or should update frequently **Example:** ```js theme={null} function result(userData) { // All User Data keys are available // This will re-run whenever ANY User Data changes return Object.keys(userData).length; } ``` </Accordion> <Accordion title="Include, but Not Watch Full User Data"> When enabled, the computed field: * Receives the entire User Data object (excluding its own key) * Only re-runs when the keys specified in **Inputs** or **Triggers** change * Useful when you need access to all data but want to control when recalculation happens **Example:** ```js theme={null} function result(userData) { // All User Data keys are available // But this only re-runs when specified inputs/triggers change return userData.first_name + userData.last_name + userData.email; } ``` </Accordion> <Accordion title="Async"> Marks the computed field as asynchronous, allowing you to use `await` and promises inside the function. **Example:** ```js theme={null} async function result(userData, helperFunctions) { const response = await fetch('https://api.example.com/data'); const data = await response.json(); return data.value; } ``` <Note> Async computed fields are evaluated separately from synchronous ones. Sync fields are computed first for faster updates, then async fields are computed afterward. </Note> </Accordion> <Accordion title="Wait Before Updating [ms]"> Only available when **Async** is enabled. Adds a debounce delay (in milliseconds) before the computed field recalculates. This prevents excessive recalculations when inputs change rapidly. **Use case:** If a user is typing in an input field and you're making an API call based on that input, you might set this to 500ms to wait until they stop typing. </Accordion> <Accordion title="Include Loading State"> When enabled, the computed field key will be set to `"_loading"` while the async computation is in progress. This allows you to show loading indicators in your UI. **Example:** ```js theme={null} // In your computed field async function result(userData) { const data = await fetch('https://api.example.com/data'); return data.value; } // In your UI, you can check: // if (userData.my_field === "_loading") { show loading spinner } ``` </Accordion> <Accordion title="Set Value as Class on Flow"> When enabled, the computed field's value is added as a CSS class to the flow container element in the format: `field-value-{key}-{value}` **Example:** If your computed field key is `user_type` and it returns `"premium"`, the class `field-value-user_type-premium` will be added to the flow container. **Use case:** This allows you to style your flow differently based on computed values using CSS: ```css theme={null} .field-value-user_type-premium .pricing-section { background-color: gold; } ``` </Accordion> </AccordionGroup> ## Example Computed Fields ### Full Name Here's an example of a Computed Field called `full_name`, that takes the user's first and last name and concatenates them together. ```js theme={null} // All Computed Fields must contain a function called result() function result(userData, helperFunctions, triggerContext) { // Get the user's first and last name from the User Data JSON object, // and join them together with a space in the middle return `${userData.first_name} ${userData.last_name}`; } ``` ### BMI Here's an example of a Computed Field called `bmi`, that takes the user's weight and height and calculates their BMI. ```js theme={null} // All Computed Fields must contain a function called result() function result(userData, helperFunctions, triggerContext) { // Get the user's weight and height from the User Data JSON object // In this example, we're assuming the weight is in kilograms and the height is in centimeters const weight = userData.weight; const height = userData.height; const bmi = weight / (height * height); // Return the BMI value return bmi; } ``` ### User is/is not over 18 - from a single date input Here's an example of a Computed Field called `is_over_18`, that takes the user's date of birth and returns a boolean value indicating whether they are over 18. ```js theme={null} // All Computed Fields must contain a function called result() function result(userData, helperFunctions, triggerContext) { // Get the user's date of birth from the User Data JSON object // Assumes the date of birth is stored as a 'YYYY-MM-DD' string, e.g. '1990-01-01' const dob = userData.date_of_birth; // Calculate the date exactly 18 years ago const eighteenYearsAgo = new Date(); eighteenYearsAgo.setFullYear(eighteenYearsAgo.getFullYear() - 18); // Return true if the user's date of birth is before 18 years ago, false otherwise return new Date(dob) < eighteenYearsAgo; } ``` ### User is/is not over 18 - from three dropdowns Here's an example of a Computed Field called `is_over_18`, that takes the user's date of birth, entered by the user into month, day and year dropdowns, and returns a boolean value indicating whether they are over 18. ```js theme={null} // All Computed Fields must contain a function called result() function result(userData, helperFunctions, triggerContext) { // Get the user's date of birth from the User Data JSON object const day = userData.dob_day; const month = userData.dob_month; const year = userData.dob_year; // Calculate the date of birth from the day, month and year const dob = new Date(year, month - 1, day); // Calculate the date exactly 18 years ago const eighteenYearsAgo = new Date(); eighteenYearsAgo.setFullYear(eighteenYearsAgo.getFullYear() - 18); // Return true if the user's date of birth is before 18 years ago, false otherwise return dob < eighteenYearsAgo; } ``` <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> # Using Conditions in Embeddables Source: https://docs.embeddables.com/features/conditions Hiding/showing content based on the user's answers and other logic **Conditions** are one of Embeddable's powerful logic features, and have a variety of use-cases. A Condition decides whether a page or component is shown or hidden, based on the value of a property in [User Data](/features/user-data). To set up a Condition, you define which values are acceptable for a User Data property. If the property has those values, then the page or component is shown - if not, then it is hidden. ## Showing/Hiding a component or page with Conditions <Steps> <Step title="Go to the Page/Component Options"> * Select the page or component, in the Layers sidebar or (in the case of a component) in the central preview pane. * Make sure you're on the Content tab at the top-left. * Click on Options in the tabs at the top of the right-hand sidebar. </Step> <Step title="Add a Condition"> * Click the `+ Add condition` button. * Choose the **key** in User Data that you want to impact whether this page/component is shown or hidden. * Choose the type of **operator** for the Condition - usually this is `Is or includes`. * Choose the **value** that should make this page/component be shown. <Tip> The way to think about key, operators and values in a Condition is **like an equation**. For example, you might want to display a some warning text to users under 18, so the condition is effectively `age_range = 'under_18'`. Here, `age_range` is the **key**, `Is or includes` is the **operator**, and `under_18` is the **value**. </Tip> </Step> </Steps> ## Available Condition Operators Below is a detailed explanation of each operator type you can use in Conditions, and exactly how they work: <AccordionGroup> <Accordion title="Is or includes (==)"> This operator checks if the user data value matches any of the specified values. For single values: * Returns true if the user data exactly matches any of the specified values * Returns true if `_no_value` is specified and the user data is undefined/null * Returns false otherwise For arrays: * Returns true if any of the specified values are included in the user data array * Returns true if `_no_value` is specified and any value in the user data array is undefined/null * Returns false otherwise <Tip> Special values like `_true` and `_false` will be automatically converted to their boolean equivalents. </Tip> </Accordion> <Accordion title="Is not or excludes (!=)"> This operator checks if the user data value does NOT match any of the specified values. For single values: * Returns true if the user data does not match any of the specified values * Returns true if `_no_value` is specified and the user data exists * Returns false otherwise For arrays: * Returns true if none of the specified values are included in the user data array * Returns true if `_no_value` is specified and all values in the user data array exist * Returns false otherwise <Tip> Special values like `_true` and `_false` will be automatically converted to their boolean equivalents. </Tip> </Accordion> <Accordion title="Exists"> Checks if the user data value exists (is not undefined or null). For single values: * Returns true if the value is defined and not null * Returns false otherwise For arrays: * Returns true if the array contains any defined, non-null values * Returns false if the array is empty or contains only undefined/null values </Accordion> <Accordion title="Does not exist (!exists)"> Checks if the user data value does not exist (is undefined or null). For single values: * Returns true if the value is undefined or null * Returns false otherwise For arrays: * Returns true if the array is empty or contains only undefined/null values * Returns false if the array contains any defined, non-null values </Accordion> <Accordion title="Is true (is-true)"> Checks if the user data value is exactly the value `_true`. * Returns true only if the value is strictly equal to `_true` * Returns false for any other value </Accordion> <Accordion title="Is false (is-false)"> Checks if the user data value is exactly the value `_false`. * Returns true only if the value is strictly equal to `_false` * Returns false for any other value </Accordion> <Accordion title="Is not true (is-not-true)"> Checks if the user data value is not the value `_true`. * Returns true for any value that is not strictly equal to `_true` * Returns false only if the value is strictly equal to `_true` </Accordion> <Accordion title="Is not false (is-not-false)"> Checks if the user data value is not the value `_false`. * Returns true for any value that is not strictly equal to `_false` * Returns false only if the value is strictly equal to `_false` </Accordion> <Accordion title="Is empty (is-empty)"> Checks if the user data value is empty. A value is considered empty if it is: * undefined * null * An empty array (length = 0) * An empty string (or string containing only whitespace) </Accordion> <Accordion title="Is not empty (is-not-empty)"> Checks if the user data value is not empty. A value is considered not empty if it is: * defined and not null * A non-empty array (length > 0) * A non-empty string (containing at least one non-whitespace character) </Accordion> </AccordionGroup> <Tip> When working with arrays in conditions, remember that the behavior can be different from single values. For example, the "Is or includes" operator will match if ANY of the specified values are found in the array, while "Is not or excludes" will only match if NONE of the specified values are found. </Tip> <Tip> Special values like `_true`, `_false`, and `_no_value` are automatically converted to their appropriate types when used in conditions. For example, `_true` becomes the boolean value `true` when evaluated. </Tip> # Design in Embeddables Source: https://docs.embeddables.com/features/design Styling your Embeddables with tags, selectors, and breakpoints The Design sidebar in the Builder allows you to style your Embeddables using a powerful CSS-based system. You can apply styles to components, pages, and the entire Embeddable using tags, element selectors, and breakpoints. ## How styling works in Embeddables Embeddables uses a tag-based styling system that generates CSS selectors. When you apply styles in the Design sidebar, you're creating CSS rules that target specific elements in your Embeddable. ### The selector constructor The Design sidebar includes a selector constructor that helps you build precise CSS selectors. The selector is built from several components: 1. **Styling tag** - The primary identifier for what you're styling 2. **Breakpoint** - The screen size at which the styles apply 3. **Element** - The specific part of a component you're styling 4. **State** - The interaction state (hover, focus, etc.) 5. **Scope** - Where the styles apply (entire Embeddable, specific pages, etc.) ## Styling tags Tags are the foundation of the styling system in Embeddables. They allow you to apply styles to multiple components at once or create reusable style patterns. ### Adding tags to components <Steps> <Step title="Select a component or page"> Click on the component or page you want to style in the Layers sidebar or preview pane. </Step> <Step title="Open the Design sidebar"> Click on the Design tab in the left sidebar. </Step> <Step title="Add a styling tag"> * Click on the "Add a tag" dropdown in the Design sidebar - Either select an existing tag or type a new tag name - Press Enter or click to add the tag </Step> </Steps> ### Managing tags on components Once a tag is added to a component: * You can see all tags applied to the selected component in the Design sidebar * Click on a tag to select it and view/edit its styles * You can add multiple tags to a single component * Tags can be removed by clicking the X next to the tag name ### Conditional tags You can also apply tags conditionally based on [User Data](/features/user-data) using [Conditions](/features/conditions). This allows you to change the styling of components dynamically based on user input or other logic. To add a conditional tag: 1. In the component Designer sidebar, find the "Styling tag" section at the top and open the dropdown. 2. Click the "Convert to conditional tag" button inside the tag, to convert it to a conditional tag (it will move down into a new "Conditional tags" section). 3. Click the "Conditions" or "Experiment conditions" button in the tag. 4. In the popup, specify the condition for when the tag should apply based on the User Data or Experiment variants. ## Tags best practices Use a structured approach to organizing tags for maintainability and reusability. ### Tag naming conventions * Tags should be descriptive and indicate their purpose (e.g., `primary_button`, `section_header`) * Use `snake_case` for tag names (lowercase with underscores) ### Primary tags * Each component should have **one main ("primary") tag** that defines its default appearance and behavior, e.g. `standard_button` or `pricing_card`. * The primary tag typically includes all styles for a component, except any styles that come from secondary tags or single-style overrides. ### Secondary tags * **Secondary tags** are commonly used to build on primary tags to create a more specific version of a component type, without needing to repeat all/most of the styles in the primary tag. * Secondary tags can overwrite styles in primary tags, as long as it's not overwriting the vast majority of the primary tag's styles. * **Examples:** * `standard_button` + `primary_button` (adds color change) * `standard_option_buttons` + `option_buttons_2_cols` (adds 2-column layout, modifies padding/margins, centers text) ### Spacing and Tailwind-style tags * When applying a **single style** (like a margin or padding), use a **Tailwind-style tag in snake\_case** (e.g. `mt_10`, `mb_0`, `pt_8`). * These tags should usually be used instead of creating a new secondary tag for one-off overrides. * If a spacing style (e.g. `margin: 10px;`) is **universally useful across all instances**, it can be included in the **primary tag**. * For layout spacing between components, see whether you can instead use the `gap` property in the parent container. ## Element selectors Element selectors allow you to style specific parts of a component. Different component types have different elements you can target. ### Common elements Many component types include basic elements like these: * **Whole thing** - The entire component container * **Contents** - The inner content area * **Text** - Text elements within the component * **Icon** - Icon elements ### Component-specific elements Different component types have specialized elements: <AccordionGroup> <Accordion title="Input Box elements"> * **Input** - The input field itself - **Label** - The label text - **Placeholder** - The placeholder text - **Invalid message** - Error message text - **Text area** - For multi-line inputs </Accordion> <Accordion title="Button elements"> * **Button** - The button element - **Text** - Button text - **Icon** - Button icon - **Description** - Secondary text </Accordion> <Accordion title="Image elements"> * **Image element** - The image itself - **Alt text** - Alternative text for accessibility </Accordion> <Accordion title="Option Buttons elements"> * **Button list** - Container for all buttons - **Button** - Individual option button - **Text** - Button text - **Checkbox** - Checkbox element (for multi-select) - **Icon** - Button icon - **Description** - Button description text </Accordion> <Accordion title="Dropdown elements"> * **Dropdown main** - The main dropdown container - **Dropdown list** - The list of options - **List option** - Individual dropdown option </Accordion> <Accordion title="File Upload elements"> * **Uploader** - The upload area - **Label** - Upload label text - **Sub label** - Secondary label text - **File list** - List of uploaded files - **File item** - Individual file in the list </Accordion> <Accordion title="Progress Bar elements"> * **Whole thing** - The entire progress bar container - **Bar filled** - The filled portion of the bar - **Progress text** - The progress label or percentage text - **Reset button** - The reset control inside the progress bar </Accordion> <Accordion title="Stripe Checkout elements"> * **Payment box** - Container for payment form - **Payment form** - The form element - **Stripe checkout button** - Submit button - **Message list** - Container for messages - **Error message** - Error text - **Success message** - Success text </Accordion> </AccordionGroup> ### Selecting elements In the Design sidebar, use the "Element" dropdown to choose which part of the component you want to style. The available options will change based on the type of component you have selected. ## Breakpoints Breakpoints allow you to create responsive designs that adapt to different screen sizes. Embeddables includes default breakpoints and supports custom breakpoints. ### Default breakpoints Embeddables comes with three default breakpoints: * **Desktop** - Styles that apply to all screen sizes (no max-width) * **Tablet** - Styles for screens 720px and below * **Mobile** - Styles for screens 520px and below ### How breakpoints work Breakpoints in Embeddables use CSS media queries with `max-width`. When you apply styles at a specific breakpoint: 1. The styles are wrapped in a media query targeting that maximum width 2. Styles cascade from larger to smaller breakpoints 3. Desktop styles apply to all sizes unless overridden 4. Tablet styles override desktop styles on screens 720px and below 5. Mobile styles override both desktop and tablet styles on screens 520px and below ### Creating custom breakpoints Custom breakpoints allow you to target specific screen sizes beyond the default options. <Note> Custom breakpoints are currently configured through the Embeddable JSON. Contact Embeddables support for assistance setting up custom breakpoints. </Note> To add a custom breakpoint, you need to add it to the `breakpoints` array in your Embeddable configuration: ```json theme={null} { "breakpoints": [ { "name": "large_tablet", "max_width": 1024 }, { "name": "small_mobile", "max_width": 375 } ] } ``` Each breakpoint requires: * **name** - A descriptive name for the breakpoint (use `snake_case`) * **max\_width** - The maximum screen width in pixels where these styles apply ### Breakpoint ordering In the generated CSS, breakpoints are automatically ordered from highest to lowest max-width. This ensures that styles cascade correctly and more specific (smaller) breakpoints override broader (larger) ones. ### Using breakpoints in the Builder 1. Select a component or page 2. Open the Design sidebar 3. Choose your breakpoint from the "On" dropdown 4. Apply your styles - they will only affect screens at or below that breakpoint's max-width <Tip> Start by styling for desktop, then add breakpoint-specific overrides for tablet and mobile. This approach (desktop-first) is more efficient than styling each breakpoint from scratch. </Tip> ## Advanced selector options The Design sidebar includes additional options for creating more specific selectors: ### State selectors State selectors allow you to style components based on user interaction: * **Always** - Default state (no state selector) * **Hover** - When the user hovers over the element * **Focus** - When the element has focus * **Active** - When the element is being clicked * **Disabled** - When the element is disabled <Note> Hover states are automatically wrapped in a `@media (hover:hover)` query to prevent issues on touch devices where hover doesn't work as expected. </Note> ### Scope selectors Scope selectors determine where your styles apply: * **Entire Embeddable** - Styles apply everywhere the tag is used * **This page only** - Styles only apply on the current page * **Pages with tag** - Styles apply only on pages that have a specific tag. For example, you might want to apply certain styles to all buttons on interstitial pages ### By key selector For Option Selector and Advanced Dropdown components, you can target specific buttons by their key. This is useful when you want to style individual options differently. ## Custom CSS selectors For advanced users, Embeddables also supports custom CSS selectors. This allows you to write your own CSS selectors directly. <Note> Custom CSS selectors are an advanced feature. Contact Embeddables support for guidance on using custom selectors. </Note> ## Best practices ### Tag organization * Use descriptive tag names that indicate purpose (e.g., `cta_button`, `form_section`) * Create reusable tags for common patterns (e.g., `card`, `highlight`) * Avoid overly specific tags that will only be used once ### Responsive design * Start with desktop styles, then add breakpoint overrides * Test your Embeddable at different screen sizes * Use the breakpoint toggle in the Builder to preview different sizes * Consider touch targets on mobile (buttons should be at least 44x44px) ### Performance * Avoid overly complex selectors when possible * Reuse tags instead of creating many unique ones * Group related styles under the same tag ### Styling workflow 1. Select the component or page you want to style 2. Add or select a styling tag 3. Choose the appropriate element selector 4. Select your breakpoint (start with desktop) 5. Apply your styles in the style panels 6. Add breakpoint-specific overrides as needed 7. Test across different screen sizes ## Related features <CardGroup> <Card title="Conditions" icon="split" href="/features/conditions"> Use conditions to show/hide components based on user data </Card> <Card title="User Data" icon="database" href="/features/user-data"> Learn about the data system that powers conditional styling </Card> </CardGroup> # PayPal Source: https://docs.embeddables.com/features/payments/paypal How our PayPal integration works, and how to use it <Note>This page is queued up to be written soon - watch this space!</Note> # Accepting Stripe Payments in Embeddables Source: https://docs.embeddables.com/features/payments/stripe/overview How to accept payments with our deep Stripe integration Payments in Embeddables are handled via [Stripe](https://stripe.com), with a dedicated Stripe component that is integrated deeply into our platform. <Frame label="Stripe Checkout"> <img /> </Frame> ## Checkout types The Stripe Checkout component supports two checkout types: <AccordionGroup> <Accordion title="Payment"> The standard checkout experience where customers enter their payment details (credit card, bank account, etc.) directly in the embedded form. This is the default and most common type. <Frame> <img alt="Stripe Payment element showing card and payment method input fields" /> </Frame> </Accordion> <Accordion title="Express Checkout"> A streamlined checkout experience that displays quick-pay buttons such as Apple Pay, Google Pay, and Link. Express Checkout lets returning customers complete purchases faster using saved payment methods in their wallets. <Frame> <img alt="Stripe Express Checkout element showing Apple Pay, Google Pay, and Link buttons" /> </Frame> </Accordion> </AccordionGroup> You select the checkout type when configuring the Stripe component in the Builder. You can enable both types at the same time — for example, offering Express Checkout buttons alongside the standard payment form. The checkout type(s) you choose determine which configuration options and triggers are available. <Note> Some configuration fields and triggers only apply to Express Checkout. These are clearly marked throughout this documentation. </Note> ## Keys and IDs that you'll need 1. Your Stripe **Publishable Key** - grab this from the [API Keys](https://dashboard.stripe.com/apikeys) section of your Stripe Dashboard. 2. A **Restricted Key** generated in Stripe (also from the [API Keys](https://dashboard.stripe.com/apikeys) page in Stripe), with the following scopes/permissions: <Note> Restricted Key - required scopes: * `Customers`: `Write` * `Payment Intents`: `Write` * `Setup Intents`: `Write` * `Checkout Session`: `Write` * `Credit notes`: `Read` * `Subscriptions`: `Write` </Note> 3. Any **Price IDs** for specific products you want to offer - grab these from the [Products](https://dashboard.stripe.com/products) page in Stripe. ## Setting up Stripe The following are the minimum steps required to add a Stripe component to your Embeddable: <Steps> <Step title="Add your Stripe credentials"> * Go to **Settings** → **Credentials & Endpoints** in your Embeddables dashboard. * In the **Stripe Credentials** section, click **+ New Stripe Credential**. * Choose the **Key Type** (Publishable Key or Restricted Key). * Select the Live **Environment**. * Add a **Label** to help identify the credential (e.g., "Stripe Live Publishable Key"). * Enter your **Value** (the actual Stripe key). (See above for details on the keys to grab/generate). * Click **Create** to save the credential. <Tip> You can also add credentials for the `Test` environment if you want to - if so, these will be used when in test mode instead of the default Embeddables-provided test keys. </Tip> </Step> <Step title="Add a Stripe component to your Embeddable"> * In the Builder, switch to the checkout page that should contain the Stripe component. * Select a Container to insert the Stripe component into, or a copmonent that it should be added next to. * Open the `+` left-hand sidebar, and select `Stripe` from the list of components. </Step> <Step title="Choose the type of payment you want to accept"> * Under Checkout mode, select one of the following options: * **Payment**: If you're accepting only one-time payments, or want to place a hold on a user's card. * **Setup**: For setting up a user's payment method to take payments in the future. * **Subscription**: If your line items will include at least one recurring payment. <Note> Please note that Stripe only supports placing a hold on one-time payments, not recurring payments. </Note> </Step> <Step title="Choose which payment methods you want to accept"> * Under Payment methods, select the payment methods you want to accept, from the following options: * **Credit card** * **Link** (Stripe's own quick purchase login system) * **US Bank Account** * **Buy now, pay later services**: Afterpay/Clearpay, Klarna, Affirm <Note> You need to have the relevant payment methods enabled in your Stripe account in order for them to appear for users. </Note> </Step> <Step title="Add line items to the checkout"> * There are two different types of line item that you can add: 1. **Price ID**: This corresponds to a product and price that you've already created in Stripe. <Warning> Make sure to grab the **Price ID** from Stripe, rather than the Product ID. </Warning> <Accordion title="How to find the Price ID"> * Go to the [Product Catalog](https://dashboard.stripe.com/products) section of your Stripe Dashboard. * Select the product you want to use. * Click on the more options (three dots) icon button in the Price that you want to use. * Select `Copy price ID`. <Frame> <img alt="How to find the Price ID" /> </Frame> </Accordion> 2. **Custom line item**: This lets you manually enter an amount, currency and name for the line item in Embeddables. * Both types of line item require you to enter a quantity. <Tip> If you need your line items to be dynamic (i.e. change based on the user's answers, like what plan they select), see the [Add dynamic line items](#add-dynamic-line-items) section below. </Tip> </Step> <Step title="Include the customer email address (and, optionally, phone number)"> * To insert the user's given email address into the Stripe checkout, add the key name of the component you use to collect the user's email, using **Templating** (e.g. `{{email}}`). * You can optionally collect a user's phone number, using the same method. <Tip> By default, Stripe's credit card fields will also include Country and Zipcode / Postal Code fields. These are all Stripe needs for a customer billing address, so you don't need to provide any billing address fields. </Tip> </Step> <Step title="Review any other settings you need to configure"> * Most brands want to [make sure a Customer is created in Stripe](#make-sure-a-customer-is-created-in-stripe) for new purchases, and [automatically set default payment methods in Stripe](#automatically-set-default-payment-methods-in-stripe) for those customers. * It's worth deciding on [which payment method tabs](#control-which-payment-method-tabs-are-shown) you want to show to customers, including setting up [Apple Pay and Google Pay](#set-up-apple-pay-and-google-pay) if desired, and deciding whether to enable [Stripe Link](#set-up-stripe-link). * You may want to [collect shipping information](#collect-shipping-information) if you're sending a physical product (billing information is usually collected automatically by Stripe using the zipcode/postal code field). * See below for a full list of optional settings you might want to configure. </Step> </Steps> ## Optional: Additional Stripe configurations These features are not required in order to successfully accept payments, but they can be useful in different scenarios. <AccordionGroup> <Accordion title="Collect shipping information"> To collect shipping information from the user, you need to make sure you have the relevant input components in your Embeddable: * Address Line 1 * Address Line 2 * City * State * Zipcode / Postal Code * Country Then, enter the keys of these components in the Shipping Address fields in the Stripe component using Templating, e.g. `{{address_line_1}}`. <Info> Shipping information is optional, depending on whether you're sending the customer a physical product. Billing information is required, but only country and postal code. This is handled automatically by the Stripe component, as described above, so you don't need to add your own billing address fields. </Info> </Accordion> <Accordion title="Add dynamic line items"> Sometimes you need your line items to be dynamic - i.e. change based on the user's answers, such as the plan that they select. To do this: 1. Use a [Computed Field](/features/computed-fields)\` to generate the JSON for the line items. 2. Refernce the key of that Computed Field in the Line Items section of the Stripe component. <Note> In order for the Stripe component to still work when the Embeddable is in test mode, you need to ensure that the Computed Field handles test mode correctly by returning a test price\_id. </Note> **Example Computed Field code:** ```javascript theme={null} // @TODO: Delete one of these based on whether you're charging a recurring or one-time payment const TEST_PRICE_ID = 'price_1JMwNRIQ3JVMlttKsvCn3lK3' // This is a recurring Embeddables-provided test price ID const TEST_PRICE_ID = 'price_1KGqjRIQ3JVMlttKzJ059opF' // This is a one-time Embeddables-provided test price ID // @TODO: Replace these constants const PRICE_ID_PLAN_1 = 'price_aaaaaaaaaaaaaaaaaaaaaaaa' const PRICE_ID_PLAN_2 = 'price_bbbbbbbbbbbbbbbbbbbbbbbb' // All Computed Fields must contain a function called result() function result(userData) { // Determine whether we're in test mode const url = new URL(window.location) const isTestMode = url.searchParams.get('savvy_test') === 'true' || (!url.searchParams.get('savvy_test') && url.searchParams.get('savvy_env')) || (!url.searchParams.get('savvy_test') && url.host.endsWith('embeddables.com')) const testPriceId = TEST_PRICE_ID // Determine which price_id the user has selected const selectedPriceId = userData.selected_plan === 'advanced' ? PRICE_ID_PLAN_2 : PRICE_ID_PLAN_1 // If we're in test mode, use the test price_id, otherwise use the selected product's price_id const priceId = isTestMode ? testPriceId : selectedPriceId // Return the line items return [ { price: priceId, quantity: 1, }, ]; } ``` </Accordion> <Accordion title="Set values based on dynamic data"> You can set pretty much any value in the Stripe component based on dynamic data, such as the user's answers to a question (e.g. their email address), or a value that's computed from an answer (e.g. the post-purchase success message). <Tabs> <Tab title="Simple Templating"> For values that come directly from user answers (like email, phone number, or shipping address): 1. Add an input component (like Email Input, Phone Input, etc.) to your Embeddable where you want to collect the user's information. 2. In the Stripe component settings, locate the field you want to set dynamically. 3. Use templating syntax to reference the input component's key, e.g. `{{email}}` or `{{phone}}`. <Note> This works for any field in the Stripe component that accepts templating, including: * Customer email * Customer phone * Shipping address fields * Pay button text * Success message </Note> </Tab> <Tab title="Computed Fields"> For values that need to be calculated or transformed (like a dynamic success message): <Steps> <Step title="Create a Computed Field"> Create a [Computed Field](/features/computed-fields) that will generate the value you want. </Step> <Step title="Write the computation logic"> In the Computed Field, write code that returns the final value. For example: ```javascript theme={null} function result(userData) { const name = userData.first_name || 'there'; return `Thanks ${name}! Your payment has been processed successfully.`; } ``` </Step> <Step title="Reference in Stripe component"> In the Stripe component settings, use templating to reference the Computed Field's key, e.g. `{{success_message}}`. </Step> </Steps> </Tab> </Tabs> <Tip> You can mix and match both approaches - some fields might use simple templating while others use computed fields, depending on your needs. </Tip> </Accordion> <Accordion title="Control Stripe prices from the Embeddables CMS"> If you handle multiple Stripe products or prices in your Embeddable, we recommend adding them to a table in the [Embeddables CMS](/features/cms), and pulling those records into your Embeddable to use in your Stripe component. See the [Control Stripe Prices from the Embeddables CMS](/how-to/control-stripe-prices-from-cms) guide for more information. </Accordion> <Accordion title="Apply coupons and promo codes"> You can offer discounts to your customers using Stripe's coupon and promo code system. This lets you create both customer-facing promo codes (like "BLACKFRIDAY50") and automatic discounts. See the [Use Stripe Coupons and Promo Codes](/how-to/use-stripe-coupons-and-promo-codes) guide for more information. </Accordion> <Accordion title="Share a single Stripe session across multiple Stripe Checkout components"> If you have more than one Stripe Checkout component on the same page (for example, a Payment element and an Express Checkout element side-by-side), you can make them share a single underlying Stripe Checkout session by setting `use_shared_stripe_session: true` on each component. **Why use this?** * Ensures coupons and promo codes applied to the session are immediately reflected on every component's "Pay" button. * Avoids creating multiple independent checkout sessions for the same purchase, which could lead to duplicate charges. **How to enable it:** 1. In the Builder, select each Stripe Checkout component that should share the session. 2. Open the component's JSON editor and add `"use_shared_stripe_session": true` to the component's options. 3. Repeat for every Stripe Checkout component on that page that should participate. <Note> All components in the shared session must use the same **Checkout mode** (e.g. all Payment, or all Subscription) and the same **currency**. Mismatched values will produce an error. </Note> <Warning> Shared sessions are scoped to a single page. Components on different pages each create their own independent session. </Warning> </Accordion> <Accordion title="Disable the Pay button until the page is valid"> When `disable_button_until_page_valid` is set to `true` on a Stripe Checkout 2 component, the Pay button is disabled reactively whenever any required field on the current page is incomplete or invalid. The button re-enables as soon as all required fields are satisfied. This is different from `enforce_validation`, which only blocks the payment attempt at the moment the user clicks Pay. With `disable_button_until_page_valid` the button itself is visually disabled throughout, giving users an immediate cue that they still need to fill something in. **How to enable it:** 1. In the Builder, select the Stripe Checkout 2 component. 2. Open the component's JSON editor and add `"disable_button_until_page_valid": true`. <Tip> You can style the disabled state however you like using CSS targeting `[disabled]` on the Pay button element. No default disabled style is applied — full control is yours. </Tip> <Note> This setting works in conjunction with the existing `enforce_validation` option. If you only want to block the payment attempt (without visually disabling the button beforehand), use `enforce_validation: true` without setting `disable_button_until_page_valid`. </Note> </Accordion> <Accordion title="Place a hold on the customer's card"> See the [Place a Hold on a Payment](/how-to/place-a-hold-on-payment) guide for more information on this feature. <Warning> You will need to manually release the hold on the user's card at a later date to complete the payment. </Warning> <Note> Please note that Stripe only supports placing a hold on one-time payments, not recurring payments. </Note> </Accordion> <Accordion title="Automatically set default payment methods in Stripe"> The method for automatically setting default payment methods depends on your checkout mode: * **Payment mode**: Simply set "Setup Future Usage" to "Off Session" in the Stripe component options. * **Setup mode**: Does not apply - payment methods are automatically set as default in this mode. * **Subscription mode**: Set up a webhook from Stripe to Embeddables to automatically set the first payment method as default, [using this step-by-step guide](/how-to/assign-default-payment-methods). </Accordion> <Accordion title="Make sure a Customer is created in Stripe"> When using **Setup** or **Payment** checkout modes, you can control whether a customer is created in Stripe. Use the **Create a Customer in Stripe?** option in the Stripe component options, below the **Checkout Mode** setting. <Note> This setting is not available for **Subscription** mode since customers are always created for recurring payments. </Note> **Available options:** * **If Required** (Default): A customer is only created if Stripe requires it for the specific payment method or transaction type. * **Always**: A customer is always created in Stripe, regardless of whether it's required for the transaction. <Tip> If you choose to create customers, you can collect the Customer ID using the [Collect Stripe IDs](#collect-stripe-ids-to-send-to-your-backend) feature. </Tip> </Accordion> <Accordion title="Customise the Pay button text and Success message"> You can customise both the label on the Pay button and the message shown to users after a successful payment. * **Pay button text** – Sets the label on the payment submit button (e.g. "Pay now", "Subscribe", "Complete purchase"). Supports templating, e.g. `{{cta_label}}`. * **Success message** – Sets the confirmation message shown to the user after payment is completed. If the payment fails (e.g. card declined), the error message from Stripe is shown instead. Supports templating, e.g. `{{success_message}}`. Both fields support **translations** — switch the content language in the Builder top bar to enter translated values for each language you have configured. If the payment fails, for example if the credit card is declined, the error message returned from Stripe will be displayed to the user. </Accordion> <Accordion title="Disable the checkout button until the page is valid"> When **Disable button until page is valid** is enabled, the Stripe checkout button stays disabled until every required field on the page passes validation. As soon as all fields are valid the button becomes clickable. This gives users a clear visual signal that they still have required fields to fill in before they can proceed to payment. To enable this: 1. In the Builder, select the Stripe Checkout component. 2. In the right sidebar under **Options**, toggle **Disable button until page is valid** on. <Tip> You can style the button's disabled appearance independently in the Designer — select the Stripe checkout button element and switch to the **disabled** state to customise its look when locked. </Tip> </Accordion> <Accordion title="Choose a layout for the Stripe component"> This lets you choose a layout for the Stripe component, which affects how different payment method options are displayed to the user. The following options are available: * **Tabs** (default): This displays different payment methods in separate tabs, in a horizontal row across the top of the component. * **Accordion**: This displays different payment methods in an accordion of expanded and collapsed sections, with one payment method option open at a time. </Accordion> <Accordion title="Set up Apple Pay and Google Pay"> Apple Pay and Google Pay can be enabled as payment methods in your Stripe checkout. Here's how to set them up: ### Google Pay * Enable Google Pay in your [Stripe Dashboard](https://dashboard.stripe.com/settings/payment_methods) under Payment Methods * Once enabled, Google Pay will automatically appear as a payment option for users who: * Are using Chrome browser * Are logged into a Google account * Have Google Pay set up on their account * No additional configuration is required in Embeddables ### Apple Pay * Enable Apple Pay in your [Stripe Dashboard](https://dashboard.stripe.com/settings/payment_methods) under Payment Methods * You must verify your domain with Apple through the Stripe Dashboard: 1. Go to the Apple Pay Settings in your Stripe Dashboard 2. Add and verify your domain(s) where you'll be accepting Apple Pay 3. Follow Stripe's verification process to complete domain verification * Once enabled and verified, Apple Pay will automatically appear as a payment option for users who: * Are using Safari browser * Are logged into an Apple account * Have Apple Pay set up on their device * No additional configuration is required in Embeddables <Note> Both Apple Pay and Google Pay will only appear as payment options when the user's browser and account settings support them. If a user doesn't meet the requirements, they won't see these payment options. </Note> </Accordion> <Accordion title="Set up Stripe Link"> Stripe Link is a fast-checkout payment method that lets users save and reuse their payment info across Stripe merchants. 1. **Enable Stripe Link in your [Stripe Dashboard](https://dashboard.stripe.com/settings/payment_methods)** under Payment Methods. 2. **Enable Stripe Link in Embeddables:** In the Stripe component Options sidebar, select "Link" under the list of available Payment method types. 3. Once enabled in both places, Stripe Link will automatically appear as a payment option for users who: * Have a Link account (or choose to create one at checkout) * Are using a supported browser and device </Accordion> <Accordion title="Control which payment method tabs are shown"> The logic for which payment methods appear to users in your Stripe component (e.g. in the tabs at the top of the component) is controlled by two different systems: 1. **Embeddables Builder:** the settings your choose in the Stripe component Options sidebar. 2. **Stripe Dashboard Settings:** the settings you choose when logged into your Stripe Dashboard. <Tip> If you're seeing payment methods that you didn't expect, check both your Embeddables builder settings and your Stripe Dashboard configuration to identify where the setting is controlled. </Tip> <Note> The payment methods currently supported in Embeddables are: * Credit/debit cards * Apple Pay * Google Pay * Bank Account * Link * Afterpay/Clearpay * Klarna * PayPal (please note PayPal is NOT supported by Stripe in the US - for US PayPal payments, please use our dedicated PayPal component) </Note> In general, a payment method will appear in the Stripe component if it is enabled in both the Embeddables builder and the Stripe Dashboard. However, there are various exceptions to this rule, including: **Bank Account** * When Stripe Link is enabled in your Stripe Dashboard, the Bank Account payment method will automatically appear. * This happens even if you haven't enabled bank transfers in Stripe's payment methods settings. * To completely remove the Bank Account tab, you must disable Stripe Link entirely in your Stripe Dashboard. <Warning> Disabling Stripe Link will remove all Link-related payment options, not just bank transfers. </Warning> </Accordion> <Accordion title="Trigger actions on payment and lifecycle events"> You can trigger [Actions](/features/actions) on the following Stripe Checkout events: | Event | Triggered when | Checkout type | | ---------------------- | ------------------------------------------------------------------ | ------------- | | **Payment attempted** | The user clicks the Pay button (regardless of success or failure) | All | | **Payment successful** | The payment is completed | All | | **Payment failed** | The payment is declined | All | | **Mounted** | The Stripe Checkout component is mounted (loaded onto the page) | All | | **Unmounted** | The Stripe Checkout component is unmounted (removed from the page) | All | To do this: 1. Create a new Action and type the JavaScript code you want to run. 2. Add a Trigger to the Action that fires on one of the above events. <Tip> The **Mounted** and **Unmounted** lifecycle triggers are useful for running setup or cleanup logic — for example, initialising analytics when the checkout appears, or tearing it down when the user navigates away. </Tip> </Accordion> <Accordion title="Prevent default click action on Express Checkout buttons"> <Warning> This setting only appears and applies when the Stripe component is configured as **Express Checkout**. It has no effect on the standard Payment checkout type. </Warning> When **Prevent default click action** is enabled, the Express Checkout buttons (e.g. Apple Pay or Google Pay) become purely visual — they display the correct button(s) for the user's device but do **not** trigger any payment functionality when clicked. The payment flow is completely disabled on this component. This turns the Express Checkout into a UI element you can use to capture the user's click intent without starting a transaction. The [**Payment button click**](#trigger-actions-on-express-checkout-button-click) trigger fires instead, allowing you to run custom logic in response to the click. <Tip> A typical workflow is to use the intercepted click to trigger a page change or popup that contains a **separate** Stripe Checkout component (one that does **not** have **Prevent default click action** enabled). This second component handles the actual payment. Without it, the user will not be able to complete a purchase. </Tip> To enable this: 1. In the Stripe component settings, locate the **Prevent default click action** toggle. 2. Enable it to prevent the default click behavior on Express Checkout buttons. <Warning> This component will not process payments on its own. You must provide another Stripe Checkout component (without this setting enabled) for the user to complete the payment. </Warning> </Accordion> <Accordion title="Trigger actions on Express Checkout button click"> <Warning> This trigger only applies when the Stripe component is configured as **Express Checkout** and **Prevent default click action** is enabled. It does **not** fire for the standard Payment checkout type. </Warning> The **Payment button click** trigger fires when a user clicks an Express Checkout button (e.g. Apple Pay, Google Pay) whose default click behavior has been disabled via the [**Prevent default click action**](#prevent-default-click-action-on-express-checkout-buttons) setting. Since the button itself does not initiate any payment, this trigger is your entry point for deciding what happens next. A common pattern is to use this trigger to navigate the user to a different page or open a popup that contains a **separate** Stripe Checkout component — one that is fully functional and can process the payment. To use this trigger: 1. Enable **Prevent default click action** in the Stripe component settings. 2. Create a new Action with the logic you want to run when the Express Checkout button is clicked (e.g. navigate to a checkout page or open a popup). 3. Add a Trigger to the Action that fires on **Payment button click**. <Note> If **Prevent default click action** is not enabled, this trigger will not fire, and Express Checkout buttons will behave as usual — clicking them will start the payment flow directly. </Note> </Accordion> <Accordion title="Collect Stripe IDs to send to your backend"> You can collect the following IDs that Stripe provides when a payment is completed: | Stripe ID | Provided for | | --------------------- | -------------------------------------------------------------------------------------------- | | **Customer ID** | All payment types (for Payment and Setup modes, `Customer Creation` must be set up `Always`) | | **Invoice ID** | All payments except on-hold payments and collecting card details | | **Payment Intent ID** | One-time payments only | <Note> Stripe used to provide a Subscription ID for recurring payments, but in their newer API version this is no longer the case. You can access this separately via the Stripe API. </Note> #### To collect these IDs: 1. Specify a User Data key to store the ID in, in the relevant field in the Stripe component options. 2. Add that key to the Registered Keys in the Embeddable-wide settings. #### To send them to your backend: 1. Set up an Action that's triggered when the payment is completed, as specified above in [Trigger actions on payment and lifecycle events](#trigger-actions-on-payment-and-lifecycle-events). 2. Use the relevant keys in `userData`, provided in the Action function's first argument. **Example Action code:** ```javascript theme={null} // All Actions must contain a function called output() // Trigger this Action when the payment is completed function output(userData) { // Extract the relevant IDs from the userData object const { customer_id, invoice_id, subscription_id } = userData; console.log(customer_id, invoice_id, subscription_id); // Send these IDs to your backend fetch('https://your-backend-url.com/', { method: 'POST', body: JSON.stringify({ customer_id, invoice_id, subscription_id }), }); } ``` </Accordion> <Accordion title="Store the selected plan/product and payment status in User Data"> Storing the plan or product that the user selected, and the status of the payment, can be useful when looking at your users' data later, or sending that data to your own backend or CRM. The good news is that this information is almost certainly already being stored! ### Storing the plan or product If you're offering multiple plans or products to the user, then that probably means you're either: 1. Using an Option Selector component to let the user select the plan or product they want to purchase, or 2. Using a Computed Field to calculate the appropriate plan or product based on the user's answers. In both cases, just make sure that the key you're using to store the plan or product is set to be [stored in the 'Embeddables Cloud Context'](https://docs.embeddables.com/features/user-data#where-is-user-data-stored%3F) - i.e. stored in Embeddables' database. <Tip> If you're offering multiple plans or products, you're also probably using a Computed Field to generate the **line items** for the Stripe component, including the price\_id(s). You can then opt to store this in the 'Embeddables Cloud Context' as well. Alternatively, you can store a simplified version with just the price\_id(s), by computing with with another Computed Field. </Tip> ### Storing the payment status The payment status is automatically stored in User Data, using the key of the Stripe component. For this reason, we recommend choosing a straightforward key name for the Stripe component, e.g. `stripe_payment`. The status will then appear in the User Data as one of the following: * `stripe_payment: 'fresh'` - The Stripe checkout has been reset and is ready for another attempt * `stripe_payment: 'pending'` - The payment has just been attempted, and is awaiting a success or failure response from Stripe * `stripe_payment: 'error'` - The payment failed * `stripe_payment: 'completed'` - The payment was successful <Note> Just make sure that the key of the Stripe component is set to be [stored in the 'Embeddables Cloud Context'](https://docs.embeddables.com/features/user-data#where-is-user-data-stored%3F) - i.e. stored in Embeddables' database. </Note> </Accordion> <Accordion title="Configure a Return url for the Checkout Session"> You can specify a **Return url** in the Stripe component's Checkout Session configuration. This URL is passed to Stripe when creating the Checkout Session and determines where the customer is redirected after completing an external payment flow (e.g. a bank redirect or 3D Secure authentication). <Note> **Return url** is used by Stripe for payment methods that redirect the customer away from your page during the checkout process. If you are only accepting credit card payments without redirects, this field is optional. </Note> To configure a return URL: 1. In the Stripe component settings, locate the **Checkout Session** configuration section. 2. Enter the desired URL in the **Return url** field. <Tip> You can use templating in the **Return url** field to include dynamic values, e.g. `https://example.com/confirmation?email={{email}}`. </Tip> </Accordion> <Accordion title="Send custom metadata to Stripe"> You can send custom metadata to Stripe with each payment, which is useful for tracking additional information about the customer or transaction. In the Stripe component settings, look for the <b>Metadata</b> section. Here, you can add key-value pairs that will be sent to Stripe as metadata on the Checkout Session or Payment Intent. * Enter a key (e.g., `user_id`, `order_source`, etc.) and a value. * You can use templating in the value field, such as `{{user_id}}` or `{{email}}`, to dynamically insert user data. You can even compute a new value in a Computed Field and use that in the metadata. * Click the `+` button to add more metadata fields. <Info> Metadata is visible in the Stripe Dashboard and can be used for reporting, searching, or integrating with your backend. </Info> </Accordion> </AccordionGroup> ## Troubleshooting <AccordionGroup> <Accordion title="I'm getting an error about a missing publishable key"> This likely means that you need to add your Stripe Publishable Key to your Embeddables project, for the appropriate environment. **How to fix:** * Go to **Settings** → **Credentials & Endpoints** in your Embeddables dashboard. * In the **Stripe Credentials** section, click **+ New Stripe Credential**. * Choose **Publishable Key** as the Key Type. * Select the appropriate **Environment** (Test or Live). * Add a descriptive **Label** and enter your Stripe publishable key. * Click **Create** to save the credential. <Note> If you are testing, ensure you are using the correct environment (test or live) and that the corresponding key is set up. See: [I'm seeing test mode errors on a live link](#im-seeing-test-mode-errors-on-a-live-link). </Note> <Tip> To use test keys, add <code>?savvy\_test=true</code> to your URL. </Tip> </Accordion> <Accordion title="I'm getting an error about a missing restricted key"> This means that you need to add a Stripe Restricted Key with the [scopes/permissions listed above](#keys-and-ids-that-you'll-need) to your Embeddables project. **How to fix:** * Go to **Settings** → **Credentials & Endpoints** in your Embeddables dashboard. * In the **Stripe Credentials** section, click **+ New Stripe Credential**. * Choose **Restricted Key** as the Key Type. * Select the appropriate **Environment** (Test or Live). * Add a descriptive **Label** and enter your Stripe restricted key. * Make sure the key has the correct permissions, listed [above](#keys-and-ids-that-you’ll-need). * Click **Create** to save the credential. </Accordion> <Accordion title="Error: You must provide one of `price` or `price_data` for each line item when using prices."> This means that you have at least one line item that is not using a Price ID (a pre-created price in Stripe), nor is it using a Price Data object (a custom price created in the Embeddables Builder). If you're using a Computed Field to generate the line items, check the value that it's returning, and make sure it's being passed into the Stripe component. </Accordion> <Accordion title="Error: No such price: 'price_xxxxxx'; a similar object exists in test mode, but a live mode key was used to make this request."> This means that you're using a **test mode** Price ID, but are testing Stripe in **live mode**. There are two common causes of this: 1. You're using a test mode Price ID in your Computed Field, but have not set the Embeddable to test mode. <Tip> To make sure that you're in Test Mode, ensure that one of the following is true: * You're testing on a link with `?savvy_test=true` in the URL, OR * You're testing on a `engine.embeddables.com/preview` link </Tip> 2. You've set the Embeddable to test mode, but the Public and Restricted keys that you gave to the Embeddables Team to use for Test Mode were actually live mode keys. <Tip> To check which Stripe keys are being used: 1. Open the Developer Tools in your browser and go to the Elements tab. 2. Use `Cmd+F` / `Ctrl+F` to search for `data-flow-stripe-publishable-key-value`. 3. Check whether the value starts with `pk_live_` or `pk_test_`. </Tip> See: [Adding dynamic line items](#add-dynamic-line-items) for more information on how to handle this. </Accordion> <Accordion title="Error: No such price: 'price_xxxxxx'"> This likely means that you're testing with a Price ID that comes from a different Stripe account from the one that you're using to accept payments. For example, you might be using a live mode Price ID from your own Stripe account, but the Embeddable is currently in Test Mode and connected to the Embeddables-provided Stripe account. See: [Adding dynamic line items](#add-dynamic-line-items) for more information on how to handle this. </Accordion> <Accordion title="I'm seeing test mode errors on a live link"> If you see errors about test keys or test Price IDs while using a live link, it usually means that the keys you've added to your project are for test mode only. **Fixes to try:** * Check your project's Stripe credentials in **Settings** → **Credentials & Endpoints**. * If you want to test in live mode, you must add **live mode** keys to your project. * If both keys are test keys, you can only test in test mode (`?savvy_test=true`). </Accordion> <Accordion title="I'm seeing live mode errors on a test link"> If you see errors about live keys or live Price IDs while using a test link (or with `?savvy_test=true`), it usually means that the keys you've added to your project are for live mode only. **Fixes to try:** * Check your project's Stripe credentials in **Settings** → **Credentials & Endpoints**. * If you want to test in test mode, you must add **test mode** keys to your project. * If both keys are live keys, you can only test in live mode (remove `?savvy_test=true` from the URL). </Accordion> </AccordionGroup> # Saving and Version Control in Embeddables Source: https://docs.embeddables.com/features/saving-and-version-control Maintaining high quality through version visibility **Version Control** is baked into everything in Embeddables. Almost everything you do in the Builder is tied to a version, which is available to be previewed in Embeddables or on your site, before pushing anything live. This serves to minimize mistakes, confusion and opaqueness around what edits are getting pushed live to users at any one time. ## Unsaved edits and drafts Whenever you make any edit in the Builder (adding a page, changing text, setting up a [Split Test](/features/split-testing), updating logic, etc), that edit becomes part of your unsaved edits. * These unsaved edits are only visible to you, but they are backed up as a Draft (once the `Save` button stops spinning). <Note> Drafts are tied to a particular version that you're editing from (usually the last version to be saved). </Note> * When you come back to the Builder later, it will ask you if you want to resume editing from your Draft, or discard it and edit from the last saved version. * It will ask you the same thing if you switch to a version that has unsaved edits from you attached to it. ## Saving and switching versions Once you hit `Save`, in the top-right, your changes are saved to a **version**. * You can open the Version Control card and use it to switch between versions that you or other people have created, to view and test out the current state of the Embeddable at that time. * You can start making edits on an older version, and even save your changes if you want to - HOWEVER if you save then that will mean that the latest version now includes your changes but does **not** include changes made in the intervening versions. In other words, you have "skipped" all changes made since the version you were editing from. <Note> The Builder will warn you if you're about to skip/overwrite changes from other versions, and will show a "From version XXX" message in the Version Control card. </Note> ## Pushing to Staging and Production * Once you're happy with your updates, you can click the `Staging` button in the top-right to push to Staging (or do so in the Version Control Card). <Note> A version on Staging doesn't have any special properties, other than being a useful way of marking a version for other people to look at, and the fact that it can be access via the `staging` URL param (see below). </Note> * Then, when you've run all your final checks, you can push that version to Product. To do this you must open the Version Control card, select the version, and click the `Production` button at the bottom of the card. ## Previewing versions with URL params Embeddables comes with features for previewing and testing your updates before pushing them live to users, using [URL Params](/features/url-params). * To preview a particular version, add `version=123` to the URL of any Embeddables preview link (starts with `engine.embeddables.com/preview...`), or `savvy_flow_version=123` to any live URL on your site that contains an Embeddable. * To preview the latest version (useful if you're making lots of updates), just use `version=latest` or `savvy_flow_version=latest`. * To preview the version currently on staging, use `version=staging` or `savvy_flow_version=staging`. <Tip> For a full list of URL params that can be used with Embeddables, check out our doc on [URL Params in Embeddables](/features/url-params). </Tip> # Split Testing in Embeddables Source: https://docs.embeddables.com/features/split-testing Creating an experiment, setting variants and modifying content **Experiments** are deeply integrated into the Embeddables platform. Watch this video to learn how to manage Experiments in the Web App and/or Builder: <TellaVideo /> ## How Experiments work in Embeddables Experiments make use of two of the core features of Embeddables: [Conditions](/features/conditions) and [User Data](/features/user-data). <Card title="How Experiments work in Embeddables" icon="flask"> When an experiment is launched, it randomly assigns a variant to each user by setting it as a value in User Data. For example, an experiment with the key `split_hero_title_length` that has 2 variants (`control_no_change` and `variant_longer_title`) might set the following in User Data for a user: ``` split_hero_title_length: 'variant_longer_title' ``` Then, to change the experience for that user, you set up Conditions on various pages or components to hide/show them based on the current variant. For example, you could clone the title, change the length of the new title, and add a condition to each title component so that each one is only shown if its corresponding variant is active. </Card> ## How to set up an Experiment <Steps> <Step title="Create a new Experiment"> * Click on the Logic tab (top-left). * Click on the Experiments tab beneath it to open the Experiments sidebar. * Click `+ Add new experiment`. </Step> <Step title="Set the keys and variant percentages"> * Choose a key for the experiment. Experiment keys always start with `split_`. <Tip> A good key is unique to the experiment and describes what is being tested, e.g. `split_hero_title_length`. </Tip> * Choose keys for each variant. <Tip> We recommend that the control key starts with `control_` (e.g. `control_no_change`). Similarly, we recommend that the variant keys start with `variant_` (e.g. `variant_longer_title` or `variant_shorter_title`). </Tip> * Choose the percentage of traffic to be directed to each variant. </Step> <Step title="Change the content for each variant"> * Select the page or component that you want to modify based on this Experiment (if multiple pages/components, just repeat the tasks in this step for each one). * Clone that page or component, and modify it to match what you want users in your first variant to see (e.g. change the title text to be longer). * Go to the Options sidebar (the tab on the right-hand-side) and add an Experiment Condition to the page/component, matching the experiment key and the variant key that matches this content. <Tip> This will likely make the current page/component disappear, since you are not currently viewing that variant. </Tip> * Add a similar Experiment Condition onto the control version of your component/page. <Tip> A `no_value` value will be added to the Experiment Condition on the control variant, so that it will always be shown by default. </Tip> * Repeat for any other variants by cloning the page/component, changing the content, and updating the Experiment Condition. </Step> <Step title="Preview, test and push live for real users"> * You can now preview your changes by re-opening the Experiments sidebar, selecting the Experiment, and clicking the Preview (<Icon icon="eye" />) icon buttons next to each variant in the sidebar on the right-hand side. * Once you're happy, save your changes. * You can now test it out by using the [`savvy_flow_version=latest` URL params](/features/url-params). * If you want to force a particular variant, you can add the key and value as a URL param. (Don't forget that once you're assigned a variant, you will be kept in that variant unless you clear local storage or switch browsers). * To launch this experiment for real users, just push the version to Staging and then to Production. </Step> </Steps> ## Pausing an Experiment <Steps> <Step title="Select Pause on the Experiment"> * You can pause an Experiment by selecting the Experiment, and in the right-hand sidebar press <Icon icon="pause" /> `Pause`. </Step> <Step title="Choose a default variant"> * Select which variant to display to users while the Experiment is paused. </Step> </Steps> <Tip> Pausing an Experiment moves the `no_value` value to the Experiment Conditions in the pages/components associated with that variant. This means that the Experiment and variant keys can be no longer added into the User Data, and the chosen variant will become the default for all new users. Existing users with previously assigned variants will be unaffected and will keep those variants. </Tip> ## Ending an Experiment <Warning> Ending an Experiment is final and can't be reversed. The only way to undo this is to switch to a previous version where the Experiment was still active and save from that version. </Warning> <Steps> <Step title="Select End on the Experiment"> * To end an Experiment, select the Experiment, and in the right-hand sidebar press <Icon icon="stop" /> `End`. </Step> <Step title="Choose the winning variant"> * Select which variant to display to users moving forward. * This will delete all pages/components associated with the other variants, and remove all Experiment Conditions relating to this Experiment. </Step> </Steps> # Debugger Source: https://docs.embeddables.com/features/testing-and-debugging/debugger How to get the most out of our in-built Debugger tool <Note>This page is queued up to be written soon - watch this space!</Note> # Translations in Embeddables Source: https://docs.embeddables.com/features/translations Add and edit multiple languages for your Embeddable content in the Builder **Translations** let you offer your Embeddable in multiple languages. You configure which languages are available, then edit the content for each language in the Builder. End users see the language that matches their choice (for example via User Data or URL params). ## How translations work in the Builder Every Embeddable has a **default language** (English). You can add more languages at the Embeddable level. Once you have more than one language: * A **content language** dropdown appears in the Builder top bar when you're in editing mode. * The dropdown controls **which language you are currently editing**. All content you add or change is saved for that language. * Text and other translatable fields (labels, placeholders, button text, etc.) are stored per language. The default language uses the normal property keys; other languages use language-prefixed keys internally. So you switch the active language in the top bar, then edit normally: either in the **Options** panel (Content section) or by **clicking and editing text directly** in the preview. Both respect the selected content language. ## Setting languages Languages are configured at the **Embeddable** level, not per page or component. <Steps> <Step title="Deselect any selection"> Click in empty space in the preview or press Escape so that nothing (no page, no component) is selected. The Options panel will show Embeddable-level options. </Step> <Step title="Open the Translations section"> In the left sidebar, open the **Options** tab, then expand the **Translations** accordion section. </Step> <Step title="Add a language"> Click **Add** and fill in: * **Key** – A short code for the language (e.g. <code>es</code>, <code>en\_us</code>, <code>fr</code>). The key should follow Embeddable's key name conventions, see [Naming keys](/guides/naming_keys). * **Name** – The display name (e.g. Español, English (US), Français). This is only used inside the builder. Click to add. The new language appears in the list below the default language. </Step> </Steps> * **Default language** – English is always the default and cannot be removed. It appears first in the list and does not have an Edit/Delete action. * **Editing a language** – Use **Edit** on a language card to change its **Name** only. The **Key** cannot be changed after creation. * **Removing a language** – Use **Delete** on a language card to remove that language. Content already stored for that language will no longer be shown to end users. ## Editing translations After you have at least two languages, you can edit content per language in two ways. ### Switching the content language When in **editing mode** and more than one language is configured, the **content language dropdown** appears in the top bar (next to the breakpoint toggle). It shows a short label for the current language (e.g. En, Es). 1. Click the dropdown and select the language you want to edit. 2. All subsequent edits (in the Options panel or in the preview) apply to that language until you switch again. The selected language is remembered for your session in the Builder (stored in User Data for the Builder preview). ### Editing in the Options panel 1. Select the content language you want to edit from the top bar dropdown. 2. Select a component or page in the Layers sidebar or in the preview. 3. In the **Options** tab, open the **Content** section. Fields that support translations (text, label, placeholder, button labels, descriptions, etc.) will show and save values for the **currently selected language**. 4. Change any of those fields; they are stored for the active content language. This applies to components that expose translatable fields in the Content section: **Plain Text**, **Rich Text**, **Rich Text Markdown**, **Custom HTML**, **Input Box**, **Option Selector**, **Custom Button**, **File Upload**, **Stripe Checkout** and **Media Image**. ### Editing text in the preview You can also edit translatable text **in place** in the preview: 1. Select the content language from the top bar dropdown. 2. Click the text you want to change in the preview. The text becomes editable. 3. Type your changes. They are saved for the currently selected language. In-place editing uses the same language as the dropdown, so you can work in one language at a time without opening the Options panel. ### Empty translations and fallback to default When a translation field is empty for a given language, the default language (English) is shown instead. This applies to both the Builder preview and the published Embeddable. **In the Builder, when editing in place:** * If you switch to a language that has no translation yet, you see the default-language text. As soon as you **click into that text and edit it** (in the preview), you are now editing the translation for that language. * If you **leave the field without making any change**, or if you **edit but then return the text to its original state** (e.g. with Ctrl+Z) before blurring, the translation stays empty and the default language continues to be shown. * If you **make any edit**—even adding or removing a single space—and leave the field with the text changed, the translation is saved and the translated value is shown for that language. * If you **clear the field** (e.g. with Ctrl+Z or by deleting all text) and then deselect the element, the translation is effectively empty again, so the default language is shown when you leave the field. ## End-user language How an end user sees a given language depends on your setup. The runtime uses the value of <code>embeddable\_language</code> in [User Data](/features/user-data) (when present) to decide which language to show. You can: * Set a default or detect locale and call <code>setUserData</code> (or equivalent) with <code>embeddable\_language: 'es'</code> (or another key you configured). * Pass the language via a [URL param](/features/url-params) if you expose that key in your allowed URL keys. ### Letting users choose the language in the Embeddable A common approach is to add an **Option Selector** (dropdown) somewhere on the page—for example in one of the corners—so end users can switch language themselves: 1. Add an **Option Selector** component and position it where you want (e.g. top-right or bottom-left). 2. Set the Option Selector's **key** to <code>embeddable\_language</code>. The selected value is then written to User Data and the runtime will show that language. 3. Add one **option** per language you configured: set each option's **key** to the corresponding language key (e.g. <code>en</code>, <code>es</code>, <code>fr</code>). Use the option labels for the display names (e.g. English, Español, Français). Users can then pick their preferred language from the dropdown and the Embeddable content updates to that language. If no value is present for <code>embeddable\_language</code>, or the provided value does not match any configured languages, the default language will be loaded. ## Related features <CardGroup> <Card title="User Data" icon="database" href="/features/user-data"> Use User Data to drive which language the end user sees (e.g. embeddable\_language). </Card> <Card title="URL Params" icon="vial" href="/features/url-params"> Prefill or override User Data (including language) via URL params. </Card> </CardGroup> # Testing and URL Params Source: https://docs.embeddables.com/features/url-params What URL params are available to use when viewing Embeddables ## How to use URL params in Embeddables 1. The following URL params are available for use on both Embeddables preview links and live URLs on your own site. They aren't available for use on the Builder or Web App. 2. The key of the param can differ, depending on whether it's on a preview link (e.g. `https://engine.embeddables.com/preview/flow_abcdefg?version=latest`) or a live link (e.g. `https://mywebsite.com/?savvy_flow_version=latest`). ## List of available URL params <Info> Some of these params start with "savvy" or "savvy\_flow" - Savvy is the old name for Embeddables before we changed our brand. We'll be updating these soon! </Info> | Param on preview link | Param on live link | Possible values | Description | | --------------------- | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `version` | `savvy_flow_version` | `latest`, `staging`, `123` | Display a certain version of the Embeddable | | `savvy_test` | `savvy_test` | `true`, `false` | Force test mode on (if live) or off (if preview) | | `savvy_track` | `savvy_track` | `true`, `false` | Force analytics tracking on (if live) or off (if preview) | | `static_render` | `static_render` | `true` | Render only the HTML with no interactivity (useful for showing a preview of the Embeddable) | | `[USER_DATA_KEY]` | `[USER_DATA_KEY]` | `[USER_DATA_VALUE]` | Prefills/forces a certain value in user data (must be an experiment or listed in the allowed URL keys in settings) | <Note> The rest of this page is queued up to be completed soon - watch this space! </Note> # User Data in Embeddables Source: https://docs.embeddables.com/features/user-data Storing and using data about your users In Embeddables, **User Data** refers to the current data stored on a given user. User Data exists as a JSON object of properties. These properties could be: * Answers the user gave to questions (the key would be the input component's key) * A value calculated by a [Computed Field](/features/computed-fields) (the key would be the computed field's key) * A value set by an [Experiment](/features/split-testing) (the key would be the experiment's key) * Other types of properties (see full list below) ## Where is User Data stored? User Data is stored in three places, or 'contexts', and each one usually stored a different amount of data. <Tip> Contexts are cascading, so if a property is stored in #2, it will also be stored in #1 as well, and so on. </Tip> 1. **Embeddable Context** - This is the full User Data, and is stored within the Embeddable itself. It is not available after the user refreshes or closes the tab. 2. **Local Storage Context** - This is the subset of User Data that is stored in the user's browser, in Local Storage. It's used to remember a user's answers and position in the flow after they close the tab and return later. 3. **Embeddables Cloud Context** - This is the smallest subset of User Data that is stored in Embeddables' databases. It is used for displaying analytics insights in the Embeddables Web App. ## How to set whether/where a User Data property is stored <Note> This feature is coming soon to the New Builder. In the meantime, ask Embeddables support to show you how to set this in the component JSON. </Note> ## Types of User Data | Type | Example | Typical Use-Case | | ------------------- | -------------------- | ------------------------------------------------------- | | **Automatic Data** | `current_page_key` | Sending the user's current page to an API | | **Answers** | `weight` | Asking the user for their details | | **Computed Fields** | `bmi` | Calculating a value to decide if the user is eligible | | **Experiment Data** | `split_title_length` | Using the user's current variant to show or hide a page | | **URL Params** | `utm_source` | Setting a condition based on the user's traffic source | | **Registered Keys** | `access_token` | Storing the result of an API request | ## User Data keys and key naming You can read our full guide to best practices for naming keys in Embeddables here: <Card title="Naming keys correctly in Embeddables" icon="key" href="/guides/naming-keys"> Learn how to choose good names for component keys, Computed Fields and more. </Card> ### Input component keys Input components, i.e. components that collect data from the user, are special in that they store their data in the User Data. This means that **the key name you choose for an input component will also be the property key name for that field in the User Data**. For example, if you have an input component with the key `name`, the User Data will look like this: ```json theme={null} { ... "name": "John Doe" ... } ``` For this reason, it's especially important to choose good key names for input components. ## Choosing good key names * All keys must be in `snake_case`, meaning: * lowercase letters and underscores only * no numbers * no special characters * no spaces * no uppercase letters * All experiment keys must start with `split_`. ### Input component keys, and other keys that end up in the User Data: * Should be short and descriptive (e.g. `full_name`). * Should describe the data it's collecting, not the component (e.g. `full_name` not `full_name_input`). * Should be unique within the Embeddable, except in very special cases (see below). ## Keeping keys unique All keys, whether component keys or other User Data keys, must be unique within the Embeddable. The only exception to this rule is if you're handling multiple input components that will be 'swapped out' - i.e. shown/hidden such that only one of them is visible at a time. The most common use-case of this is a [Split Test](/features/split-testing), e.g. testing between two different email capture pages, both containing an input component with the key `email`. ## Default User Data Default User Data allows you to set initial values for User Data properties when an Embeddable loads. These values are automatically added to the User Data object before the user interacts with your Embeddable. ### How it works When you set default User Data: 1. The values are stored in the `defaults.userData` property of your Embeddable configuration 2. When a user loads the Embeddable, these default values are automatically added to their User Data 3. Default values are applied during the reset process - when User Data is cleared, default values are preserved and reapplied ### Setting default User Data You can set default User Data through the Builder UI: 1. In the Builder, navigate to the Options panel for your Embeddable 2. Look for the "Edit default user data JSON" option 3. Click to open the JSON editor modal 4. Add your default values as key-value pairs in JSON format Example: ```json theme={null} { "user_type": "new_visitor", "source": "organic", "language": "en" } ``` ### Use cases Default User Data is useful for: * **Pre-filling form fields** - Set default values that users can modify * **Setting initial state** - Define starting values for conditions or logic * **Tracking metadata** - Add default tracking parameters that persist throughout the user's session ### Important limitations <Warning> **Not recommended for Computed Fields** - Default User Data should not be used to set values for computed fields. The update sequence could cause computed fields to override default values before you intend them to. Computed fields should set their own default values, and using both would create conflicts. </Warning> <Warning> **Not recommended for Experiment data** - Default User Data should not be used to initialize experiment-related data. The Experiments feature handles setting that data automatically, and using both would create conflicts. </Warning> <Note> **Requires Registered Keys** - If you want to set default User Data for keys that aren't already created by components, computed fields, or other features, you must add those keys to Registered Keys. This ensures the Embeddable is aware of them and can watch for changes. </Note> ### Behavior with reset When you reset User Data (either programmatically or through user actions), default User Data values are preserved and reapplied. This ensures that your Embeddable always starts with the correct initial state. The reset process: 1. Clears most User Data properties 2. Preserves system properties (like `entryId`, `current_page_id`, etc.) 3. Preserves computed fields and experiment keys 4. Reapplies all default User Data values <Note> Default User Data values will not override existing User Data. If a user already has a value for a key, the default will not replace it unless the User Data is reset. </Note> # Components: Input Box Source: https://docs.embeddables.com/guides/components/input-box How to use the Input Box component in Embeddables The Input Box component collects user input data and stores it in your User Data. Configure it through four main sections: Configuration, Content, Validation, and More. This documentation includes both **Input Box-specific settings** (unique to Input Box components) and **general settings** (shared across multiple component types but also available for Input Box). ## Configuration settings Configure basic input behavior and appearance. <AccordionGroup> <Accordion title="User Input"> Control whether users are required to complete this input before proceeding. **Options**: * Optional (value: `false`) * Required (value: `true`) <Info> This is a general setting that applies to Input Box, Option Selector, Button, Calendly, File Upload, and PayPal components. </Info> When set to Required, users cannot proceed until they provide a value for this input field. </Accordion> <Accordion title="Height (Multiline)"> Control whether the input renders as a single-line field or a multi-line textarea. **Options**: * Single-Line (value: `false`) * Multi-Line (value: `true`) When enabled, the input field expands vertically to support multiple lines of text. </Accordion> <Accordion title="Autofocus"> Automatically focus this input when the page loads. **Options**: * Off (value: `false`) * On (value: `true`) <Warning> Only use autofocus on one element per page to avoid accessibility issues and unexpected behavior. </Warning> </Accordion> <Accordion title="Range Min"> Set the minimum value for number, range, date, or month inputs. <Info> Only available when subtype is `"range"`, `"date"`, or `"month"`. </Info> For number and range inputs, this prevents users from entering values below the specified minimum. For date and month inputs, it sets the earliest selectable date. </Accordion> <Accordion title="Range Max"> Set the maximum value for number, range, date, or month inputs. <Info> Only available when subtype is `"range"`, `"date"`, or `"month"`. </Info> For number and range inputs, this prevents users from entering values above the specified maximum. For date and month inputs, it sets the latest selectable date. </Accordion> <Accordion title="Range Step"> Set the step increment for number and range inputs. <Info> Only available when subtype is `"range"`. </Info> Defines the amount by which the value changes when using increment/decrement controls or arrow keys. For example, a step of `10` means values will increment by 10 (0, 10, 20, 30, etc.). </Accordion> </AccordionGroup> ## Content settings Customize labels and placeholder text shown to users. <AccordionGroup> <Accordion title="Label"> Text label displayed before the input field. Use this to provide context about what information you're collecting. The label appears above or beside the input field depending on your design settings. </Accordion> <Accordion title="Placeholder"> Placeholder text shown inside the input when it's empty. <Info> Not available when subtype is `"switch"`. </Info> Placeholders provide hints about expected input format or example values. They disappear when the user starts typing. </Accordion> <Accordion title="Show Labels on both sides"> Display labels on both sides of toggle switches. <Info> Only available when subtype is `"switch"`. </Info> When enabled, you can provide separate labels for the on and off states of a switch component, making the toggle's states clearer to users. </Accordion> <Accordion title="Off label"> Label for the 'off' state of toggle switches. <Info> Only available when subtype is `"switch"` AND "Show Labels on both sides" is enabled. </Info> This appears on the side of the switch representing the inactive state, helping users understand what happens when the switch is turned off. </Accordion> </AccordionGroup> ## Validation settings Configure validation rules and error messages to ensure data quality. <AccordionGroup> <Accordion title="Validate Value"> Enable or disable validation for this input field. **Options**: * Disabled (value: `false`) * Enabled (value: `true`) When enabled, the input will warn or block users from entering values that don't meet your specified validation criteria. All other validation settings depend on this being enabled. </Accordion> <Accordion title="Formula"> Choose a validation formula or expression to validate the input value. <Info> Only available when "Validate Value" is enabled. </Info> Available options (dynamic based on subtype): | Option | Value | Available For | | ----------------- | ------------------- | ------------------------------ | | None | `"none"` | All subtypes | | Full Name | `"full_name"` | Text subtype only | | Email | `"email_address"` | Email subtype only | | Business Email | `"business_email"` | Email subtype only | | Phone Number (US) | `"phone_number_us"` | Phone subtype only | | Number Range | `"number_range"` | Number subtype only | | Custom formula | `"custom"` | All subtypes (opens JS editor) | <Tip> When you select "Custom formula", a JavaScript editor opens where you can write custom validation logic that returns `true` for valid values and `false` for invalid ones. Custom validation functions receive the input value as the first argument and optionally the full current User Data as the second argument: ```javascript theme={null} // value only function validate(value) { return value.length >= 5; } // value + userData (access other fields in the flow) function validate(value, userData) { return userData.country === 'US' ? /^\d{5}$/.test(value) : true; } ``` </Tip> </Accordion> <Accordion title="Validate on blur"> Trigger validation when the field loses focus. **Options**: * Off (value: `false`) * On (value: `true`) <Info> Only available when "Validate Value" is enabled. </Info> When enabled, validation runs after the user clicks away from the field, providing immediate feedback without interrupting typing. </Accordion> <Accordion title="Trim whitespace on blur"> Remove leading and trailing whitespace when the field loses focus. **Options**: * Off (value: `false`) * On (value: `true`) <Info> Only available when "Validate Value" is enabled. </Info> Automatically cleans up user input by removing spaces at the beginning and end of the entered value, which helps prevent validation errors from accidental spaces. </Accordion> <Accordion title="Format email on blur"> Format email addresses when the field loses focus. **Options**: * Off (value: `false`) * On (value: `true`) <Info> Only available when "Validate Value" is enabled AND subtype is `"email"` or `"text"`. </Info> Automatically formats email addresses to a standard format (e.g., converting `JOHN@EXAMPLE.COM` to `john@example.com`) when the user leaves the field. </Accordion> <Accordion title="Empty message"> Error message displayed when the field is empty but required. <Info> Only available when "Validate Value" is enabled AND field is required (isRequired is true). </Info> Customize the message users see when they try to proceed without filling in a required field. </Accordion> <Accordion title="Invalid message"> Error message displayed when input fails validation. <Info> Only available when "Validate Value" is enabled. </Info> Provide clear, actionable feedback when users enter invalid data. For example: "Please enter a valid email address" or "Phone number must be 10 digits". </Accordion> <Accordion title="Max length of input"> Maximum number of characters allowed in the input. <Info> Only available when subtype is `"text"` or `"password"`. </Info> Limits the length of text inputs to prevent overly long entries. Users cannot type beyond this limit. </Accordion> <Accordion title="Range min (Validation)"> Minimum value for number inputs when using number range validation. <Info> Only available when "Validate Value" is enabled AND "Formula" is `"number_range"` AND subtype is `"number"`. </Info> Validates that entered numbers are not below this minimum value. Different from the Configuration "Range Min" setting, which affects the input control's constraints. </Accordion> <Accordion title="Range max (Validation)"> Maximum value for number inputs when using number range validation. <Info> Only available when "Validate Value" is enabled AND "Formula" is `"number_range"` AND subtype is `"number"`. </Info> Validates that entered numbers are not above this maximum value. Different from the Configuration "Range Max" setting, which affects the input control's constraints. </Accordion> <Accordion title="Allow Integers only"> Restrict input to whole numbers only (no decimals). **Options**: * Off (value: `false`) * On (value: `true`) <Info> Only available when "Validate Value" is enabled AND "Formula" is `"number_range"` AND subtype is `"number"`. </Info> When enabled, users can only enter integers (e.g., 1, 42, 100) and decimal values (e.g., 3.14, 99.9) will be rejected. </Accordion> </AccordionGroup> ## More settings Additional configuration options for advanced use cases. <AccordionGroup> <Accordion title="Repeat Component"> Use this to programmatically repeat this component for each item in an array. **Toggle Options**: * Off (value: `"hide"`) * On (value: `"show"`) When enabled, you can select a repeater key from a dropdown: **Repeater key**: The User Data property that contains the array to use for repeating this component. The dropdown shows a dynamic list of Embeddable keys (excluding keys starting with `"split_"`). <Info> This is a general setting that applies to all components (except Page and Embeddable). The component will be repeated for each item in the specified array. </Info> </Accordion> <Accordion title="Ignore in QA tests"> Whether to avoid clicking on this element during Embeddables' automated QA tests. **Options**: * Off (value: `false`) * On (value: `true`) <Info> This is a general setting that applies to Button and Input Box components. </Info> Enable this if you want the automated QA system to skip interacting with this component during tests. </Accordion> <Accordion title="Input Debounce Time"> Delay before updating User Data after the user types a character. <Info> Not available when subtype is `"switch"` or `"checkbox"`. </Info> Debouncing waits for the user to finish typing before updating the User Data. This reduces the number of updates and can improve performance, especially when other components or computed fields react to this input. For example, setting this to `500` means User Data will only update 500 milliseconds after the user stops typing. <Tip> Use debouncing when you have computed fields or actions that depend on this input, as it prevents unnecessary recalculations while the user is still typing. </Tip> </Accordion> <Accordion title="Element ID"> Unique identifier for the component element. <Info> This is a general setting that applies to all components except Page. Use this to set a custom HTML element ID for styling or JavaScript targeting. </Info> Set a unique identifier that can be used to target this specific component with custom CSS or JavaScript code. </Accordion> </AccordionGroup> # Writing Custom Code in Embeddables Source: https://docs.embeddables.com/guides/custom-code How to get the most out of custom code blocks in Embeddables Custom Code blocks in Embeddables are where you can escape the confines of no-code and create entirely custom functionality. Custom Code exists in two main places: [Computed Fields](#computed-fields) and [Actions](#actions). <CodeGroup> ```javascript Example Computed Field theme={null} function result(userData, helperFunctions, triggerContext) { ... } ``` ```javascript Example Action theme={null} function output(userData, helperFunctions, triggerContext) { ... } ``` </CodeGroup> ### Available arguments in Custom Code functions Custom Code blocks provide various bits of context through arguments in the root functions. They are, in order: | # | Argument | Description | | - | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | 1 | `userData` | The current User Data | | 2 | `helperFunctions` | An object containing various helper functions (see [full list below](#available-helper-functions)) | | 3 | `triggerContext` | An object containing data on what triggered the Computed Field or Action (see [full list below](#available-trigger-context)) | ## Helper Functions The following helper functions are available in both Computed Fields and Actions through the `helperFunctions` argument: ### Page Navigation Functions <AccordionGroup> <Accordion title="goToNextPage()"> Navigates to the next page in the flow. This is useful when you want to navigate to the next page in the flow, but don't want to use the default navigation logic. ```javascript theme={null} // Example usage await helperFunctions.goToNextPage(); ``` </Accordion> <Accordion title="goToPrevPage()"> Navigates to the previous page in the flow. This is useful when you want to navigate to the previous page in the flow, but don't want to use the default navigation logic. ```javascript theme={null} // Example usage await helperFunctions.goToPrevPage(); ``` </Accordion> <Accordion title="goToPage(page: object)"> Navigates to a page. Pass an object with **one** of the following properties: * **`key`** (string) — the page key * **`id`** (string) — the page id * **`index`** (number) — the page index (1-based) * **`idOrKey`** (string) — accepts either a page id or a page key ```javascript theme={null} // Example usage helperFunctions.goToPage({ key: 'page_key_1234567890' }); helperFunctions.goToPage({ id: 'page-test-id' }); helperFunctions.goToPage({ index: 2 }); helperFunctions.goToPage({ idOrKey: 'page_key_1234567890' }); // works with id or key ``` </Accordion> </AccordionGroup> ### Data Management Functions <AccordionGroup> <Accordion title="getUserData()"> Returns the current [User Data](/features/user-data) from the flow controller. This is useful when you need to read the current state of User Data, as opposed to the initial userData value that was passed in as the first argument. ```javascript theme={null} // Example usage const currentData = helperFunctions.getUserData(); ``` </Accordion> <Accordion title="setUserData(key: string, value: any)"> Updates a specific key/value pair in the [User Data](/features/user-data). ```javascript theme={null} // Example usage helperFunctions.setUserData('userName', 'John'); ``` </Accordion> <Accordion title="setUserData(updates: Record<string, unknown>)"> Updates multiple fields in the User Data at once. Accepts an object containing key/value pairs to update - i.e. to merge into the existing User Data. Note that nested objects will be replaced entirely, not merged. ```javascript theme={null} // Example usage helperFunctions.setUserData({ userName: 'John', age: 30, preferences: { theme: 'dark' } }); ``` </Accordion> <Accordion title="resetUserData()"> Resets all User Data to its initial state, effectively starting fresh. Use with caution as this will clear all previously stored User Data. ```javascript theme={null} // Example usage helperFunctions.resetUserData(); ``` </Accordion> </AccordionGroup> ### Component and UI Functions <AccordionGroup> <Accordion title="getComponentElement(key: string)"> Returns the DOM element for a component with the specified key. Useful for direct DOM manipulation, accessing element properties (such as `value` for a password input field), or setting up custom event listeners. ```javascript theme={null} // Example usage const element = helperFunctions.getComponentElement('submit_button'); element.disabled = true; ``` </Accordion> <Accordion title="openInfoBox(infoBoxPageKey: string)"> Opens an info box with the specified page key. The page key must refer to a page which is designed to be shown within an info box. Returns a Promise that resolves when the info box is opened. ```javascript theme={null} // Example usage await helperFunctions.openInfoBox('infobox_faqs_popup_page'); ``` </Accordion> <Accordion title="closeInfoBox()"> Closes the currently open info box. Returns a Promise that resolves when the info box is closed. ```javascript theme={null} // Example usage await helperFunctions.closeInfoBox(); ``` </Accordion> </AccordionGroup> ### Action and Flow Control <AccordionGroup> <Accordion title="triggerAction(actionId: string)"> Programmatically triggers an action by its ID. Returns a Promise that resolves when the action is complete. Useful for chaining actions or conditional action execution. ```javascript theme={null} // Example usage await helperFunctions.triggerAction('action_aaaaaaaaaaaa'); ``` </Accordion> <Accordion title="triggerValidation()"> Triggers form validation on the flow controller. Useful when you need to validate form inputs programmatically. ```javascript theme={null} // Example usage helperFunctions.triggerValidation(); ``` </Accordion> </AccordionGroup> ### Analytics and Events <AccordionGroup> <Accordion title="trackCustomEvent(customEventName: string, customEventProps: any)"> Tracks a custom analytics event with the given name and properties. Returns a `Promise<void>` that resolves once the event has been sent. You can `await` it if you need to wait for the event to be delivered before continuing (for example, before navigating away). ```javascript theme={null} // Example usage — fire and forget helperFunctions.trackCustomEvent('button_clicked', { buttonId: 'submit', timestamp: Date.now() }); // Example usage — await before navigating await helperFunctions.trackCustomEvent('purchase_completed', { orderId: userData.order_id }); await helperFunctions.goToNextPage(); ``` </Accordion> </AccordionGroup> ### Additional Helpers <AccordionGroup> <Accordion title="helpers"> An object containing various utility helper functions. The exact available helpers depend on the context and implementation. ```javascript theme={null} // Example usage const { formatDate, validateEmail } = helperFunctions.helpers; ``` </Accordion> </AccordionGroup> ## Trigger Context variables <CodeGroup> ```typescript Trigger Context for Computed Fields theme={null} type TriggerContext = { inputs_causing_recompute: string[]; // List of keys of inputs that caused the recompute old_input_values: string[]; // List of old values of those inputs (last time this Computed Field was computed) new_input_values: string[]; // List of current values of those inputs old_user_data: Record<string, unknown>; // Snapshot of User Data from when this Computed Field was last computed new_user_data: Record<string, unknown>; // Current value of User Data (identical to the first userData argument) }; ``` ```typescript Trigger Context for Actions theme={null} type TriggerContext = { trigger_type: string; // E.g. 'outputs_onclick' trigger_page_id: string; // The page that triggered the Action (if triggered by page change) trigger_page_key: string; // The page that triggered the Action (if triggered by page change) trigger_component_id: string; // The component that triggered the Action (if triggered by a component) trigger_component_key: string; // The component that triggered the Action (if triggered by a component) }; ``` </CodeGroup> <Note> **Please note** that `old_user_data` and `new_user_data` currently only contain the input keys for the Computed Field, not all of User Data. This may change in the future. </Note> # HIPAA Compliance in Embeddables Source: https://docs.embeddables.com/guides/hipaa-compliance Understanding best practices for being fully HIPAA compliant in Embeddables Embeddables provides a set of features to help you ensure that your data is HIPAA compliant. This guide outlines the main steps and best practices for maintaining HIPAA compliance in your workspace. <Tip> The **Compliance** tab in the sidebar of the Embeddables Web App contains a complete checklist of all the steps you need to take to be fully HIPAA compliant. We strongly recommend completing all the steps in the checklist to ensure that your workspace is fully HIPAA compliant. </Tip> ### 1. Set Your Team Permissions Properly configuring your team’s roles and permissions is the foundation of HIPAA compliance. For a full overview of roles and permissions, see [User Roles & Permissions](./user-roles-permissions.mdx). * **Assign a Data Privacy Officer**: Designate a single, trusted team member as your Data Privacy Officer. This person will be responsible for managing data privacy settings and completing the HIPAA checklist. * **Restrict Access to De-Anonymized Data**: Only grant Full Data Access to users who have a clear, documented need to view personally identifiable or health information. All other users should have Anonymized Data Access. * **Review Regularly**: Periodically review team roles and permissions to ensure they remain appropriate as your team or requirements change. ### 2. Label Health Data and Personally Identifiable Data Fields For each live Embeddable, you must label which fields count as: * **Health Data** (Protected Health Information, PHI) * **Personally Identifiable Data** (PII) * **Contact Info** (e.g., email, phone number) <Note> When defining field types, we recommend that the Data Privacy Officer be familiar with the following document: [Guidance Regarding Methods for De-identification of Protected Health Information](https://www.hhs.gov/hipaa/for-professionals/privacy/special-topics/de-identification/index.html) This guidance will help you determine what counts as identifiable health information and how to properly de-identify data. </Note> This labeling determines which users can access sensitive data and ensures that data is handled according to HIPAA requirements. * The Data Privacy Officer is responsible for defining these fields for each Embeddable. * Data that is not correctly labeled will be hidden from all users except those with Full Data Access. <Warning> **Make sure that your list of live (published) Embeddables is up-to-date.** Use this list as your checklist of Embeddables for which you need to label fields. </Warning> ### 4. Ongoing Responsibility HIPAA compliance is not a one-time task. The Data Privacy Officer must ensure that: * All new Embeddables are reviewed and labeled appropriately before going live. * Existing Embeddables are kept up-to-date as fields or data requirements change. * Team permissions are regularly reviewed and updated as needed. *** By following these steps and best practices, you can help ensure that your workspace remains HIPAA compliant and that sensitive data is protected according to regulatory requirements. # Best Practices: Deciding how best to send/receive data Source: https://docs.embeddables.com/guides/how-best-to-send-data Data Pipelines? API requests? Something else? ## Use-cases Some common use-cases that we often see: 1. I want to send data to an external SaaS app (e.g. a CRM, Airtable). 2. I want to trigger an email to be sent to me, or to the end-user. 3. I want to send data to my own backend/API. 4. I want to fetch content to display in my Embeddable, similar to a CMS. 5. ...many other combinations of the above... ## Questions to ask yourself If you're sending data somewhere: * Am I sending to an external SaaS app, or to my own backend/database/API? * Am I sending the data somewhere to access later, or will I need to access it instantly to avoid blocking the end-user? * Am I sending data to a domain that might be blocked by browsers or ad blockers? * Will I need to include some kind of private API key or token in the request? If you're fetching data from somewhere: * Do I need the content to load instantly on page load, or later (e.g. on a subsequent page, or after a button click)? * Does the data need to be secure, or is it public data that it's OK for people to access? * Am I fetching data from a domain that might be blocked by browsers or ad blockers? ## Which solution to choose - Sending data ### DataPipes - Integrations This is usually the best solution if: * ✅ You want to send data to one of these SaaS apps: HubSpot, Airtable, Sendgrid, Google Sheets. * ✅ You don't need to access it instantly (can take up to a minute to process). ### DataPipes - Webhooks This is usually the best solution if: * ✅ You want to send data to Zapier or your own backend/database/API. * ✅ You don't need to access it instantly (can take up to a minute to process). ### Frontend API request, using the Embeddables Secure Proxy This is usually the best solution if: * ✅ You want to send data to an external SaaS app, Zapier or your own backend/database/API. * ✅ You need to access it instantly (sooner than within 1 minute), or it will block the end-user from progressing. * ✅ You need to include some kind of private API key or token in the request - OR - you'll be sending to a domain that might be blocked by browsers or ad blockers. ### Frontend API request, direct This is usually the best solution if: * ✅ You want to send data to an external SaaS app, Zapier or your own backend/database/API. * ✅ You need to access it instantly (sooner than within 1 minute), or it will block the end-user from progressing. * ✅ You don't need to include any kind of private API key or token in the request. * ✅ You won't need to send to a domain that risks being blocked by browsers or ad blockers. ## Which solution to choose - Fetching data ### Embeddables CMS feature - currently in `BETA` This is usually the best solution if: * ✅ You want to fetch data from an Airtable. * ✅ You need the content to load instantly on page load. ### Frontend API request, using the Embeddables Secure Proxy This is usually the best solution if: * ✅ You want to fetch data from an external SaaS app, Zapier or your own backend/database/API. * ✅ The content doesn't need to load instantly on page load (it can be e.g. on a subsequent page, or after a button click). * ✅ You need to include some kind of private API key or token in the request - OR - you're fetching from a domain that might be blocked by browsers or ad blockers. ### Frontend API request, direct This is usually the best solution if: * ✅ You want to fetch data from an external SaaS app, Zapier or your own backend/database/API. * ✅ The content doesn't need to load instantly on page load (it can be e.g. on a subsequent page, or after a button click). * ✅ You don't need to include any kind of private API key or token in the request. * ✅ You won't need to send to a domain that risks being blocked by browsers or ad blockers. # Naming keys correctly in Embeddables Source: https://docs.embeddables.com/guides/naming-keys How to name component keys, computed field keys, and other keys in Embeddables correctly In Embeddables, there are many different places where you will need to choose a key name for something. This includes pages and components, but it also includes many features that will end up storing data in the User Data (including some types of components known as Input Components). Therefore, understanding how to correctly name keys, and the implications of doing so, is very important. ## Types of keys in Embeddables | Type | Example | Stored in User Data? | Key name... | | ------------------- | -------------------- | -------------------- | ----------------------- | | **Page** | `page_key` | No | Set in Builder | | **Component** | `component_key` | If Input Component | Set in Builder | | **Automatic Data** | `current_page_key` | Yes | Automatically generated | | **Answers** | `weight` | Yes | Set in Builder | | **Computed Fields** | `bmi` | Yes | Set in Builder | | **Experiment Data** | `split_title_length` | Yes | Set in Builder | | **URL Params** | `utm_source` | Yes | Set in Builder | | **Registered Keys** | `access_token` | Yes | Set in Builder | ### Input component keys Input components, i.e. components that collect data from the user, are special in that they store their data in the User Data. This means that **the key name you choose for an input component will also be the property key name for that field in the User Data**. For example, if you have an input component with the key `name`, the User Data will look like this: ```json theme={null} { ... "name": "John Doe" ... } ``` For this reason, it's especially important to choose good key names for input components. ## Choosing good key names * All keys must be in `snake_case`, meaning: * lowercase letters and underscores only * no numbers * no special characters * no spaces * no uppercase letters * All experiment keys must start with `split_`. ### Input component keys, and other keys that end up in the User Data: * Should be short and descriptive (e.g. `full_name`). * Should describe the data it's collecting, not the component (e.g. `full_name` not `full_name_input`). * Should be unique within the Embeddable, except in very special cases (see below). ### Keeping keys unique All keys, whether component keys or other User Data keys, must be unique within the Embeddable, otherwise you could have unintended consequences, e.g. one key overwriting the other. The only exception to this rule is if you're handling multiple input components that will be 'swapped out' - i.e. shown/hidden such that only one of them is visible at a time. The most common use-case of this is a [Split Test](/features/split-testing), e.g. testing between two different email capture pages, both containing an input component with the key `email`. # Best Practices: Passing data into/out of Embeddables to be prefilled Source: https://docs.embeddables.com/guides/passing-data-in-out URL params? Embed code? Window functions? Etc... ## Use-cases Some common use-cases that we often see: 1. I want to prefill data like email addresses into my Embeddable that users have already entered previously on my site. 2. I want to prefill data from my Embeddable into an embedded component inside it, like a Calendly booker or Stripe component. 3. I want to pass data like email addresses out of my Embeddable to be used on a subsequent page on my website. <Info> This doc is for passing data to be immediately prefilled somewhere in front of the user. For ways of sending/receiving the data to/from an external source like an API, database or SaaS app, check out [Guides: Deciding how best to send/receive data](/guides/how-best-to-send-data) </Info> ## Passing data into an Embeddable to prefill fields (e.g. email address) ### Method 1: URL params <Steps> <Step title="Add the fields into the URL as params"> * For example: `https://mywebsite.com/onboarding?email=ada@lovelace.com`. * Make sure that this is the URL when the Embeddable first loads, since that's when it will look for URL params. </Step> <Step title="Set those fields to be URL-controlled"> * Head to the Embeddable-wide settings * Go to the More tab * Add the fields to the URL Keys block, separated by newlines. </Step> </Steps> ### Method 2: Embed code keys/attributes <Steps> <Step title="Add each field as an attribute on the <savvy> HTML element"> * For example: `<savvy id="flow_abc" email="ada@lovelace.com"></savvy>`. * Make sure that these attributes are present when the element is first added to the DOM, since that's when it will look for URL params. </Step> <Step title="Set those fields to be Embed Code-controlled"> * Head to the Embeddable-wide settings * Go to the More tab * Add the fields to the Embed Code Keys block, separated by newlines. </Step> </Steps> ### Method 3: Custom code using Window variable <Note> This works best when you want to prefill data immediately (as soon as the Embeddable loads). </Note> <Steps> <Step title="Place the data on a Window variable in your website/webapp"> * From within your website or webapp, create a variable in the Window scope with a unique name. * Add the data you want to prefill into that variable. * **Make sure to that this happens before the Embeddable loads.** ```javascript Your Website/Webapp Code theme={null} window.myEmbeddablesPrefillData = { email: 'ada@lovelace.com' } ``` </Step> <Step title="Add a custom code Action, triggered on first load"> For more info on how to do this, check out: <Card title="How To: Run custom code when the Embeddable loads" href="/how-to/run-custom-code-on-load" icon="book-open"> Setting up a custom code Action that executes when the Embeddable first loads. </Card> </Step> <Step title="Grab the data from the Window variable, and then set the User Data"> * Pull out the data from the Window variable that you set up in Step 1. * Then use `helperFunctions.setUserData()` to set the User Data based on the payload sent with that event: ```javascript Custom Code Action - Triggered on First Load theme={null} function output(userData, helperFunctions) { const dataToPrefill = window.myEmbeddablesPrefillData if (dataToPrefill) { helperFunctions.setUserData(dataToPrefill); } }); } ``` </Step> </Steps> ### Method 4: Custom code using custom Window event <Note> This works best when you want to prefill data after the Embeddable has already loaded </Note> <Steps> <Step title="Add a custom code Action, triggered on first load"> For more info on how to do this, check out: <Card title="How To: Run custom code when the Embeddable loads" href="/how-to/run-custom-code-on-load" icon="book-open"> Setting up a custom code Action that executes when the Embeddable first loads. </Card> </Step> <Step title="Listen for a custom Window event, and then set the User Data"> * Use `window.addEventListener()` to listen for a custom event that you will define. * Then use `helperFunctions.setUserData()` to set the User Data based on the payload sent with that event: ```javascript Custom Code Action - Triggered on First Load theme={null} function output(userData, helperFunctions) { window.addEventListener('embeddables:my_custom_prefill_event', (event) => { const detail = event.detail || {}; const dataToPrefill = detail.prefill_data if (dataToPrefill) { helperFunctions.setUserData(dataToPrefill); } }); } ``` </Step> <Step title="Send the custom Window event from your website/webapp"> * From your website or webapp, create a new Custom Event with the prefill data, and send it to the Window: ```javascript Your Website/Webapp Code theme={null} const myEvent = new CustomEvent('embeddables:my_custom_prefill_event', { detail: { prefill_data: { email: 'ada@lovelace.com' } } }) window.dispatchEvent(myEvent); ``` </Step> </Steps> <Note> This page is unfinished and is queued up to be completed soon - watch this space! </Note> # Storing data, Local Storage and Cookies Source: https://docs.embeddables.com/guides/storing-data-cookies Understanding how Embeddables stores user data in the browser Embeddables uses **Local Storage** to persist User Data within the browser, so that when a user refreshes or returns later, they can pick up where they left off. Local Storage is similar to Cookies, but has a few key differences which are described below. ## Local Storage in Embeddables As described in our [User Data](/features/user-data) documentation, Embeddables stores user data in multiple contexts. In the Embeddables Builder, you can choose which context to store each value, on a field-by-field basis. The three contexts are: 1. **Embeddable Context**: In-memory storage that exists only during the current session. 2. **Local Storage Context**: Persistent browser storage that remains after tab/browser closure. *(Data stored here is also stored in #1).* 3. **Embeddables Cloud Context**: Data stored on our servers to provide the analytics insights in the Embeddables Web App. *(Data stored here is also stored in #1 and #2).* ## Local Storage vs Cookies ### Comparison table | Feature | Local Storage | Cookies | | ---------------------- | ---------------------------- | ---------------------------------- | | **Storage Capacity** | 5-10MB | 4KB | | **Expiration** | Doesn't expire automatically | Can be set to expire | | **Sent with Requests** | No, stays in browser | Yes, sent with every HTTP request | | **Accessibility** | Same-origin only | Can be configured for cross-origin | | **Manipulation** | JavaScript only | JavaScript or HTTP headers | ### Duration of Storage Data stored in Local Storage persists until: * The user manually clears their browser data/cache * The user uses private/incognito browsing * The user switches to a different browser or device Unlike cookies, Local Storage doesn't have built-in expiration mechanisms. This allows Embeddables to maintain a consistent user experience even if users return to your Embeddable weeks or months later. ### Privacy Considerations Local Storage is generally considered more privacy-friendly than cookies because: * It's not automatically sent with HTTP requests * It's limited to the domain that created it * It doesn't support the tracking capabilities that third-party cookies enable # User Roles & Permissions Source: https://docs.embeddables.com/guides/user-roles-permissions Understanding the different roles and permissions in Embeddables In your workspace, each user is assigned two key attributes that determine what they can do and what data they can access: ## Summary of Roles & Permissions * **Role**: Controls editing, publishing, and management capabilities. * **Admin**: Full editing and publishing access. * **Publisher**: Can save and push Embeddables live. * **Editor**: Can save Embeddables, but **cannot** push them live. * **Data Access Level**: Controls what end-user data the person can see. * **Data Privacy Officer**: Full access to all end-user data. * **Full Data Access**: Can access all end-user data, including personally identifiable information (PII/PHI). * **Anonymized Data Access**: Can only access anonymized data - end-user's personally identifiable + health info is hidden. ## How to change roles & permissions <Warning> You must be an **Admin** to change roles & permissions. </Warning> To change a user's Role or Data Access Level: 1. Navigate to the **Settings** page in the sidebar. 2. Select the **Team** tab. 3. Find the user you want to update in the table. 4. Use the dropdowns to change the user's Role and Data Access Level. *** ## Full Guide to Roles & Permissions ### 1. Roles Roles define what actions a user can take within the system. There are three main roles: #### **Admin** * Full editing and publishing access. * Can manage other users' roles and data access permissions. * Recommended for team leads or those responsible for overall workspace management. #### **Publisher** * Can save and push Embeddables live. * Cannot manage other users. * Suitable for users who need to publish changes but do not require admin privileges. #### **Editor** * Can save Embeddables, but **cannot** push them live. * Ideal for content creators or team members who prepare changes for review. ### 2. Data Access Levels Data Access Levels determine what kind of end-user data a person can view. There are three levels: #### **Data Privacy Officer** * Full access to all end-user data. * Responsible for defining which fields contain sensitive information. * **There should be only one person with this role**—this person sets the rules for the rest of your team. #### **Full Data Access** * Can access all end-user data, including personally identifiable information (PII/PHI). * Assign to team members who need the full set of customer data to do their job. #### **Anonymized Data Access** * Can only access anonymized data—personally identifiable information about end-users is hidden. * This is the **safest** form of access and is recommended for most users. *** ## Best Practices * Assign the **Data Privacy Officer** role to a single, trusted team member who understands privacy requirements. * Use **Anonymized Data Access** for most users to minimize risk and exposure to sensitive data. * Only grant **Full Data Access** to those who absolutely need it for their work. * Regularly review user roles and data access levels to ensure they are up to date and appropriate for each team member's responsibilities. # Using Embeddables Templates Source: https://docs.embeddables.com/guides/using-templates Starting a build from one of our pre-built templates Embeddables templates are a great way to get started and go live in a matter of days not weeks. <Tip> **Ask the Embeddables team for advice on what templates are available** to match your industry, product catalog and other specific needs. </Tip> ## Quickstart The following is a list of things that must be modified once you've cloned from a template. 1. **Stripe Component & Computed Field** * Update product\_data CF with product info. * Configure the Stripe component in the **Options** tab. 2. **POST Tellescope Action** * Requires `apikey`, `formid`, and `endpoint`. * Endpoint is preconfigured with Tellescope. * API key and form ID come from credentials in the webapp. 3. **Fetch Headers** * **Project ID** → Embeddables project number (`pr_123` in builder URL). * **Environment** → must match both fetch and credentials. 4. **Webapp Configuration (Settings → Credentials & Endpoints)** * **Stripe**: Create a new Stripe credential (type + environment). * **Tellescope**: Create a Custom Credential: * Type: EHR * Service: Tellescope * Key Type: API key * Environment: desired one * **Endpoint**: * Environment must match credentials. * Add regex URLs: * **Allowed Origins** → embedding URL. * **Allowed Destinations** → Tellescope API. * Link the Credential ID. ## Full template guides See below for complete guides to some of our most popular templates. <AccordionGroup> <Accordion title="Medvi Template"> **Card & Actions** * The button on the product card that’s on the checkout page triggers an **Action**. (there’s one for each specific card button, Semaglutide value and Tirzepatide value) * This Action sets **UserData** with the selected product and slides to the **Stripe** component. **Product Data & Stripe** * product\_data is a **Computed Field (CF)** that stores all products. These are saved in **UserData** and passed to **Stripe** via their price\_id. * stripe\_line\_items is another CF that retrieves the price\_id from product\_data (based on the environment) and passes it to Stripe. * The Stripe component is configured in the **Options tab**, where the template value stripe\_line\_items is already loaded. Several inputs are linked via their **keys**. **Payment Complete → POST Tellescope** * When payment is complete, the **POST Tellescope** Action is triggered. * This Action contains a **template** with a pre-set answers object that is sent as payload to Tellescope. * The answers are linked to builder components via their keys. * The Action also requires **apikey**, **formid**, and **endpoint**: * The **endpoint** is already configured with Tellescope information. * The **apikey** and **formid** are set up through **credentials** in the webapp (where we paste their template values). **Headers in the Fetch** * In the **fetch** (specifically in the headers), two values must be filled/replaced: * **Project ID** → matches the Embeddables project number (found in the builder URL as pr\_123). * **Environment** → must match the environment used for the fetch and the one defined in the credentials. **Configuration in the Webapp (Settings → Credentials & Endpoints)** * In the webapp, the last option in the side menu is **Settings**. Inside, go to the **Credentials & Endpoints** tab to configure **Stripe** and **Tellescope**. * **Stripe Credentials**: * The first option is for Stripe. * By clicking **New Stripe Credential**, you can create new credentials by providing **type** and **environment**. * **Custom Credentials (Tellescope)**: * The second option is **Custom Credentials**, where we configure the credentials for Tellescope. * Click **New Custom Credential** and provide: * **Type**: EHR (for Tellescope). * **Service Name**: Tellescope. * **Key Type**: API key. * **Environment**: the desired one. * **Endpoints**: * Finally, configure an endpoint by clicking **New Endpoint**. * The **environment** of the endpoint must match the one from the credentials. * Two URLs in **regex** format must be filled: * **Allowed Origins** → the URL where the flow is embedded (where the fetch will be executed). * **Allowed Destinations** → the URL of the Tellescope API. * The **Credential ID** must be the one configured in the previous step. </Accordion> <Accordion title="NewlyMeds Template"> **Option Selector & Product Data** * On the **checkout page**, there is an **option selector** with 4 default products. * The **keys** of these options match and connect the option selector with the products defined in a **Computed Field (CF)** called product\_data. * This way, when a user selects an option, that product is stored in the **UserData**. **Product Data & Stripe** * product\_data is a **Computed Field (CF)** that stores all products. These are saved in **UserData** and passed to **Stripe** via their price\_id. * stripe\_line\_items is another CF that retrieves the price\_id from product\_data (based on the environment) and passes it to Stripe. * The Stripe component is configured in the **Options tab**, where the template value stripe\_line\_items is already loaded. Several inputs are linked via their **keys**. **Payment Complete → POST Tellescope** * When payment is complete, the **POST Tellescope** Action is triggered. * This Action contains a **template** with a pre-set answers object that is sent as payload to Tellescope. * The answers are linked to builder components via their keys. * The Action also requires **apikey**, **formid**, and **endpoint**: * The **endpoint** is already configured with Tellescope information. * The **apikey** and **formid** are set up through **credentials** in the webapp (where we paste their template values). **Headers in the Fetch** * In the **fetch** (specifically in the headers), two values must be filled/replaced: * **Project ID** → matches the Embeddables project number (found in the builder URL as pr\_123). * **Environment** → must match the environment used for the fetch and the one defined in the credentials. **Configuration in the Webapp (Settings → Credentials & Endpoints)** * In the webapp, the last option in the side menu is **Settings**. Inside, go to the **Credentials & Endpoints** tab to configure **Stripe** and **Tellescope**. * **Stripe Credentials**: * The first option is for Stripe. * By clicking **New Stripe Credential**, you can create new credentials by providing **type** and **environment**. * **Custom Credentials (Tellescope)**: * The second option is **Custom Credentials**, where we configure the credentials for Tellescope. * Click **New Custom Credential** and provide: * **Type**: EHR (for Tellescope). * **Service Name**: Tellescope. * **Key Type**: API key. * **Environment**: the desired one. * **Endpoints**: * Finally, configure an endpoint by clicking **New Endpoint**. * The **environment** of the endpoint must match the one from the credentials. * Two URLs in **regex** format must be filled: * **Allowed Origins** → the URL where the flow is embedded (where the fetch will be executed). * **Allowed Destinations** → the URL of the Tellescope API. * The **Credential ID** must be the one configured in the previous step. </Accordion> </AccordionGroup> # How To: Add external scripts or style libraries in Embeddables Source: https://docs.embeddables.com/how-to/add-external-scripts Two ways to add external scripts or style libraries to your Embeddable There are two recommended methods to include external JavaScript packages in your Embeddable: 1. **Direct HTML Inclusion (Recommended):** Add the script tag directly to the HTML of the page where you're embedding your Embeddable, alongside the Embeddables embed code. 2. **JavaScript Action Method:** Load the package programmatically through an Action that triggers when the Embeddable loads. If you choose #2, here's sample code for inserting a `<link>` tag: ```js theme={null} // All Actions must contain a function called output() function output() { // Add the link tag loadStylesheet("<URL>"); } // Helper function to add a link tag to the head of the page function loadStylesheet(href) { const link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; document.head.appendChild(link); } ``` # Automatically set default payment methods in Stripe for subscriptions Source: https://docs.embeddables.com/how-to/assign-default-payment-methods How to set up a webhook in Stripe so that, when a payment method is first added to a customer's subscription, it is automatically set as the default payment method. <Note> This guide is only relevant for **Subscription** checkout mode. For **Payment** mode, all you need to do is set "Setup Future Usage" to "Off Session" in the Stripe component options. For **Setup** mode, you don't need to do anything - payment methods are automatically set as default in this mode. </Note> When a customer enters their credit card or other payment method for a subscription in Stripe, that payment method is not automatically set as the <strong>default</strong> payment method. Without this, the next payment in the customer's subscription may fail, unless you manually set the payment method as the default in your Stripe Dashboard. This guide explains how to set up a Webhook to ensure that when a new payment method is added to a Stripe customer, it is automatically set as the default payment method. <Frame> ![Example: Top customer has a default payment method set; bottom customer does not, which can cause payment issues.](https://assets.embeddables.com/Screenshot2025-07-21at9_19167427955247518.33.08am.png) </Frame> ## Adding a webhook to automatically set the default payment method in Stripe <Tabs> <Tab title="You have Workbench enabled"> <Note> Note: This requires a Stripe component that is set up to create <strong>subscriptions</strong> </Note> <Steps> <Step title="Create a new webhook in Stripe"> * Log in to your Stripe Dashboard. * Open the Workbench. * Navigate to the Webhooks tab. * Click <strong>+ Add destination</strong>. </Step> <Step title="Configure your webhook settings"> * Make sure "Events" is set to as "Your account". * Make sure "API version" is set to "Your current version". * Under "Events", find the `payment_method.attached` event and select it. * Click <strong>Continue</strong>. * Make sure "Webhook endpoint" is selected and click <strong>Continue</strong>. </Step> <Step title="Add your endpoint and create the webhook"> * Grab your Project ID from the URL in the Embeddables Web App - it's the ID after `/project/` that starts with `pr_`. * In the "Endpoint URL" field in Stripe, paste the following URL, replacing `<YOUR_PROJECT_ID>` with your Project ID: ``` https://webhooks.embeddables.com/webhooks/stripe/payment_method.attached?project_id=<YOUR_PROJECT_ID> ``` <Tip> If you're setting up a webhook for Stripe's test environment, add `&test=true` to the end of the endpoint URL. </Tip> * Click <strong>Create destination</strong>. </Step> </Steps> </Tab> <Tab title="You don't have Workbench enabled"> <Note> Note: This requires a Stripe component that is set up to create <strong>subscriptions</strong> </Note> <Steps> <Step title="Create a new webhook in Stripe"> * Log in to your Stripe Dashboard. * Go to the Developers section (bottom-left in your sidebar). * Navigate to the Webhooks tab. * Click <strong>+ Add endpoint</strong>. </Step> <Step title="Add your endpoint"> * Grab your Project ID from the URL in the Embeddables Web App - it's the ID after `/project/` that starts with `pr_`. * In the "Endpoint URL" field in Stripe, paste the following URL, replacing `<YOUR_PROJECT_ID>` with your Project ID: ``` https://webhooks.embeddables.com/webhooks/stripe/payment_method.attached?project_id=<YOUR_PROJECT_ID> ``` <Tip> If you're setting up a webhook for Stripe's test environment, add `&test=true` to the end of the endpoint URL. </Tip> * Add a description of what the endpoint is for. </Step> <Step title="Configure your webhook settings"> * Make sure "Listen to" is set to as "Events on your account". * Make sure "Version" is set to "Your current version". * Click "Select events", find the `payment_method.attached` event and select it. * Click <strong>Add endpoint</strong> </Step> </Steps> </Tab> </Tabs> *** ## Migrating from the old webhook URL to the new format <Tabs> <Tab title="You have Workbench enabled"> #### Migrating to the new webhook URL with Workbench enabled <Steps> <Step title="Update your webhook endpoint URL"> * Go to your Stripe Dashboard and open the Workbench. * Navigate to the Webhooks tab and find your existing webhook for `payment_method.attached`. * Edit the endpoint URL to use the new format: ``` https://webhooks.embeddables.com/webhooks/stripe/payment_method.attached?project_id=<YOUR_PROJECT_ID> ``` <Tip> If you're setting up a webhook for Stripe's test environment, add `&test=true` to the end of the endpoint URL. </Tip> * You can get your project ID from the URL in the Embeddables Web App - it's the ID after `/project/` that starts with `pr_`. * Save your changes. </Step> </Steps> </Tab> <Tab title="You don't have Workbench enabled"> #### Migrating to the new webhook URL with Workbench disabled <Steps> <Step title="Update your webhook endpoint URL"> * Go to your Stripe Dashboard and open the Developers > Webhooks section. * Find your existing webhook for `payment_method.attached`. * Edit the endpoint URL to use the new format: ``` https://webhooks.embeddables.com/webhooks/stripe/payment_method.attached?project_id=<YOUR_PROJECT_ID> ``` <Tip> If you're setting up a webhook for Stripe's test environment, add `&test=true` to the end of the endpoint URL. </Tip> * You can get your project ID from the URL in the Embeddables Web App - it's the ID after `/project/` that starts with `pr_`. * Save your changes. </Step> </Steps> </Tab> </Tabs> # How To: Control FAQs from the Embeddables CMS Source: https://docs.embeddables.com/how-to/control-faqs-from-cms Learn how to manage your FAQs through the Embeddables CMS and display them using an OptionSelector The **Embeddables CMS** provides an excellent way to manage your Frequently Asked Questions (FAQs) without needing to modify code. This guide will show you how to set up and use the CMS to control your FAQ content, making it easy for non-technical team members to update questions and answers. ## Overview This approach allows you to: * Manage all FAQs through a user-friendly CMS interface * Store FAQs in a single place for all Embeddables in your project * Maintain a single source of truth for your FAQs * Update questions and answers without code changes or deployments * Keep an audit trail of FAQ changes, via the Embeddables CMS's version control system ## Setting Up Your FAQ Table ### 1. Create a New Table 1. Navigate to the CMS section in the Embeddables Web App 2. Click "Edit Tables" 3. Click "+ Add Table" 4. Select the "FAQs" template ### 2. Configure Your Columns The template comes with recommended columns, but here's the essential structure you'll need: | Column Name | Column Key | Type | Purpose | | ------------ | ---------- | ------ | -------------------------------------------- | | **Question** | `question` | Text | The FAQ question to display on the button | | **Answer** | `answer` | Text | The detailed answer to display when selected | | **Order** | `order` | Number | Optional: Control the display order of FAQs | <Info> You could add additional columns based on your needs, such as: * A `category` column to group FAQs by topic * A `tags` column for better organization * A `last_updated` column to track when FAQs were modified </Info> <Tip> You can use a column like `category` or `tags` above to show different subsets of FAQs in different places. For example, you could have a tag called "Pricing", and then filter the FAQs in a Computed Field called `pricing_faqs`. Then you could [display the results of this](#optional-filter-faqs-by-tags) in an OptionSelector component on your pricing page, or [offer users tabs to let them filter FAQs by tag](#optional-add-a-selector-so-users-can-filter-faqs-by-tag). </Tip> ### 3. Add Your FAQs Add your FAQ content by adding rows to the table. Each row represents one FAQ item. ## Connecting FAQs to Your Embeddable ### 1. Connect to the CMS <Steps> <Step title="Add a Content Source"> * Create a new Embeddable or open your existing one * In the Embeddables Builder, go to Embeddable Options * Scroll to the CMS section * Click "+ Add Content Source" </Step> <Step title="Configure the Connection"> * Enter a User Data key (e.g., `faqs`) * Select your FAQs table * Under "Which records should get shown?", choose "Show all records" * Check "Multiple Records" since we want to display all FAQs </Step> <Step title="Register the Key"> * Add your User Data key to the list of Registered Keys in Embeddable Options </Step> </Steps> ### 2. Create a Computed Field You'll need a Computed Field to transform the CMS data into the format required by the OptionSelector: * Go to Embeddable Options * Create a new Computed Field (e.g., `faq_options`) * Add `faqs` to the inputs * Add the following code: ```js theme={null} // Computed Field: faq_options function result({ faqs }) { // Sort FAQs by order if the order column exists const sortedFaqs = faqs?.sort((a, b) => (a.order || 0) - (b.order || 0)) || []; // Transform the FAQs into the format needed by the OptionSelector return sortedFaqs.map(faq => ({ text: faq.question, description: faq.answer })); } ``` <AccordionGroup> <Accordion title="Optional: Filter FAQs by Tags"> If you added a `tags` column to your FAQs table, you can filter FAQs in your computed field. For example, to show only pricing-related FAQs: ```js theme={null} // Computed Field: faq_options function result({ faqs }) { // Filter for pricing FAQs and sort them const pricingFaqs = faqs?.filter(faq => faq.tags?.includes('pricing')) .sort((a, b) => (a.order || 0) - (b.order || 0)) || []; return pricingFaqs.map(faq => ({ text: faq.question, description: faq.answer })); } ``` This lets you create different FAQ sections (like pricing, features, or support) using the same CMS table. </Accordion> <Accordion title="Optional: Add a selector so users can filter FAQs by tag"> You can create a dynamic tag filter by adding an OptionSelector above your FAQs. Here's how: 1. First, create a computed field to get unique tags (e.g., `faq_tags`) - this will power the OptionSelector that lets users filter FAQs by tag. ```js theme={null} // Computed Field: faq_tags function result({ faqs }) { // Get all unique tags from FAQs const tags = [...new Set(faqs?.flatMap(faq => faq.tags || []) || [])]; return tags.map(tag => ({ text: tag, value: tag })); } ``` 2. Then modify your FAQ computed field to accept a selected tag - this will filter the FAQs based on the selected tag, and the results will be passed to the main FAQs OptionSelector: ```js theme={null} function result({ faqs, selected_tag }) { // Filter FAQs by selected tag if one is chosen const filteredFaqs = selected_tag ? faqs?.filter(faq => faq.tags?.includes(selected_tag)) : faqs; // Sort and transform the FAQs return (filteredFaqs || []) .sort((a, b) => (a.order || 0) - (b.order || 0)) .map(faq => ({ text: faq.question, description: faq.answer })); } ``` 3. Finally, add the two OptionSelectors to your page: * First OptionSelector: Set its key to `selected_tag` and its "Repeatable Buttons" field to `{{faq_tags}}` (which uses the first computed field above). * Second OptionSelector: Set its key to `expanded_faq` and its "Repeatable" field to `{{faq_options}}` (which uses the second computed field above). This creates an interactive FAQ section where users can filter FAQs by selecting different tags. </Accordion> </AccordionGroup> ### 3. Set Up the OptionSelector <Steps> <Step title="Add an OptionSelector"> * Add an OptionSelector component to your page * Enable "Repeatable Buttons" * In the "Repeatable" field, enter `{{faq_options}}` * Configure the OptionSelector to show descriptions only when selected </Step> <Step title="Style Your FAQs"> * Use the Designer to style the buttons and descriptions * Consider adding transitions for smooth description reveals * Ensure the text is readable and properly spaced </Step> </Steps> ## Previewing and Publishing <Warning> When testing your Embeddable on a preview link, add <code>savvy\_test=false</code> to the URL to ensure CMS data loads correctly. </Warning> To preview your FAQs with different CMS versions: * Use `savvy_cms=latest` to preview with the latest CMS version * Use `savvy_cms=123` to preview a specific version * Use `savvy_cms=staging` to preview the staging version ## Best Practices 1. **Content Organization** * Keep questions clear and concise * Use consistent formatting in answers * Consider grouping related FAQs using categories * Maintain a logical order for your FAQs 2. **User Experience** * Keep answers concise but informative * Use formatting (like lists or bold text) in answers where appropriate * Ensure the OptionSelector is easily accessible * Consider adding a search function for longer FAQ lists 3. **Content Management** * Regularly review and update FAQs * Use the CMS version control for major updates * Keep an audit trail of changes * Consider seasonal or temporary FAQs ## Example Use Cases 1. **Product FAQs** * Common questions about features * Pricing and subscription details * Technical requirements * Support information 2. **Service FAQs** * Process explanations * Service limitations * Contact information * Terms and conditions 3. **Company FAQs** * About the company * Team information * Office locations * Career opportunities <Tip> Use the CMS version control system to create and test different FAQ variations before pushing them live. This is especially useful when making major updates to your FAQ content. </Tip> ## Troubleshooting 1. **FAQs Not Loading** * Verify the Content Source is properly configured * Check that you're using `savvy_test=false` in preview URLs * Ensure the Computed Field is correctly transforming the data 2. **Formatting Issues** * Check that the OptionSelector is properly configured * Verify the Computed Field output format * Ensure all required columns are present in the CMS table For additional help or questions, please contact the Embeddables support team. # Control Stripe Prices from the Embeddables CMS Source: https://docs.embeddables.com/how-to/control-stripe-prices-from-cms Using the Embeddables CMS to define multiple Stripe prices, and pull them into your Embeddable If you handle multiple Stripe products or prices in your Embeddable, the **Embeddables CMS** is a great way to store them and keep them updated. Using the Embeddables CMS for this has the following benefits: * It lets you clearly view your list of Stripe products and prices without needing to read JS code. * It means non-technical users can update prices without needing to read or edit any code. * It makes it possible to update prices without needing to push live a new version of your Embeddable. * It gives you a clear audit trail if prices are changed. ## How to add Stripe prices to the Embeddables CMS, and use them to control prices in your Embeddable <Steps> <Step title="Add a table called Prices in the Embeddables CMS"> * Head to the [Embeddables Web App](https://app.embeddables.com) and select **CMS** in the sidebar. * Click **Edit Tables** to start editing. * Click **+ Add Table**, select the **Prices** template, and confirm by clicking the **+ Add Table** button. * Edit your new table's columns so that you've included the following: | Column Name | Column Key | What it's for | | ------------------------------------------------ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Plan Name** | `plan_name` | Helps you identify what the price corresponds to. | | **Stripe Price ID** | `stripe_price_id` | Stores the price ID from Stripe. | | **Price** (optional) | `price_amount` | Helps you remember what the amount is set to for this price in Stripe. | | One or more price attributes e.g. **Price Plan** | `price_plan` | Set one or more of these columns to distinguish your plans, and be used later in the logic of your Embeddable to determine which price to use for a user. | </Step> <Step title="Add the prices to your new table"> * Copy the price information across from Stripe into the new table. * You can add as many prices as you like, but in order for the Embeddable's logic to choose the correct price, you'll need to make sure that each price has a unique combination of price attributes. </Step> <Step title="Pull the prices into your Embeddable"> * Copy the table ID of the table you just created. * Head to the Embeddables Builder. * Make sure you don't have any components or pages selected, then go to Options and click **Edit JSON**. * Add the following JSON to your Embeddable JSON (remove the comments since JSON doesn't support comments): ```json theme={null} { // ...Your Embeddable JSON..., "content_sources": [ { "source_type": "embeddables_cms", "source_config": { "table_id": "<YOUR TABLE ID>" }, "multiple_records": true, "key": "stripe_prices" } ], // ...Embeddable JSON continues..., } ``` What this does: * Adds a new **Content Source** to your Embeddable, which is how you pull in external data. * Tells the Embeddable that the type of source is the **Embeddables CMS**, and provide the ID of the table you want to pull data from. * Tells the Embeddable that you want to allow **multiple records** to be returned (as opposed to pulling a single record based on the URL of the page, which is a different use-case of the Embeddables CMS). * Tells the Embeddables to store the table's content under the key `stripe_prices` in the **User Data** (it will be stored as an array of objects, where each object represents a row from the table). <Tip> A UI for connecting an Embeddable to a table in the Embeddables CMS, so that you don't need to modify the JSON, is coming soon! </Tip> </Step> <Step title="Set up a Computed Field to choose the correct price"> * Add `stripe_prices` to the list of **Registered Keys** in the Embeddable Options, so that you can access it in Computed Fields. * Create a new Computed Field called `stripe_line_items` - this will calculate the exact line items to pass to the Stripe Checkout. * Add `stripe_prices` to the inputs of the Computed Field, along with any other User Data keys you'll need in order to choose the correct price (e.g. a `chosen_price_plan` key from an Option Selector). * Add the following code to the Computed Field, modifying it where necessary to match your pricing plan structure and the options you give to your users: ```js theme={null} function result({ stripe_prices, chosen_price_plan }) { const TEST_PRICE_ID = "price_1JMwNRIQ3JVMlttKsvCn3lK3"; // Test Recurring Price ID for use when in test mode // Find the correct price from the CMS const stripePrice = stripe_prices?.find(price => price.price_plan === chosen_price_plan) // Return the live items // If in test mode, use the test price ID; otherwise use the live price ID return [ { price: isTestMode() ? testPriceId : stripePrice?.stripe_price_id, quantity: 1, }, ]; } // This function mimics the logic of the Embeddable when it uses the URL to decide whether to be in test mode or not function isTestMode() { const url = new URL(window.location); const params = url.searchParams return params.get("savvy_test") === "true" || (!params.get("savvy_test") && params.get("savvy_env")) || (!params.get("savvy_test") && url.host.endsWith("embeddables.com")); } ``` <Note> It's important to use a test price ID when in test mode, because Stripe's test mode insists on using test price IDs, not live ones. </Note> </Step> <Step title="Finally, update the Stripe Checkout to use the output of the Computed Field"> * Go to your checkout page and select your Stripe Checkout component (if you haven't already set this up, you can read how to do that [here](/features/payments/stripe/overview)). * In the Line Items section of the Stripe component, enable "Use template line items". * In the "Line items template" field, enter the key of the Computed Field you created: `{{stripe_line_items}}`. This tells the Stripe component to use the output of your Computed Field as the value of `line_items`, which gets sent to Stripe. </Step> </Steps> <Card title="Learn more about Stripe Payments in Embeddables" icon="database" href="/features/payments/stripe/overview"> Read more about how payments works in Embeddables, including how to set up a Stripe Checkout and accept payments. </Card> # How To: Display answers back to the user using Templating Source: https://docs.embeddables.com/how-to/display-answers-back-to-user Using templating to show User Data in text components You can use Embeddables' **Templating** feature, a simple templating syntax available in the Builder, to display [User Data](/features/user-data) values within text components or components with text elements. This allows you to create dynamic, personalized content that updates based on user input or computed values. ## Basic Templating Syntax To display a User Data value in text, wrap the key name in double curly braces: ```text theme={null} {{key_name}} ``` You can do this inside `Plain Text` and `Rich Text` components, as well as inside text fields of other components, such as Labels in `Button` components. <Check> The value displayed will automatically update whenever the referenced User Data value changes. </Check> ## Common Examples Here are some common ways to use templating: ### Display user input If you've collected a user's name through an input component with the key `first_name`, you can display it like this: ```text theme={null} Thanks for that info, {{first_name}}! ``` ### Display computed values If you have a [Computed Field](/features/computed-fields) that calculates a value (like BMI), you can display the result: ```text theme={null} Your BMI is {{bmi}} ``` ### Display nested properties For User Data that contains nested objects (like API responses), you can access nested properties using dot notation: ```text theme={null} Error: {{api_response.error_message}} ``` ## Tips for using templating * Make sure the key exists in User Data before trying to display it * For numbers that need formatting, consider using a [Computed Field](/features/computed-fields) to format the value first * If the value might be undefined, you can provide a default value using a Computed Field * Keys are case-sensitive and must match exactly what's in the User Data ## Computed Field + Templating Example: Full name display Here's a complete example that shows how to display a user's full name using both direct input values and a Computed Field: 1. Collect the first and last name in separate input components with keys `first_name` and `last_name` 2. Create a Computed Field called `full_name` that combines them: ```js theme={null} function result(userData) { return `${userData.first_name} ${userData.last_name}`; } ``` 3. Display the full name using templating: ```text theme={null} Welcome back, {{full_name}}! ``` <Card title="Learn more about User Data" icon="database" href="/features/user-data"> Read more about how User Data works in Embeddables, including how it's stored and accessed. </Card> # How To: Dynamically switch brands within an Embeddable Source: https://docs.embeddables.com/how-to/dynamically-switch-brands Using CSS variables to reuse a single Embeddable for multiple brands The powerful logic available in Embeddables means that it often makes sense to re-use a single Embeddable for multiple brands, if the general logic and content is consistent across those brands. This doc shows you how to inject styles and content into an Embeddable from the parent page, so that its appearance can change based on the URL. ## Updating the styles based on the URL <Tabs> <Tab title="Method 1: Adding a <style> tag to the page"> This method works well if you have a different page for each brand, each with the same Embeddable embedded in it. <Steps> <Step title="Create CSS variables in a <style> tag in the parent page"> * On the parent page of the first brand, where you embed the Embeddable, add a `<style>` tag in the HTML that looks like this, replacing the variable names and styles with what you need for your brand: ```html theme={null} <style> body { --font-headings: "Raleway", sans-serif; --font-body: "Open Sans", sans-serif; --color-primary: #e74c3d; --color-secondary: #03c4eb; --color-buttons-primary: #d9534f; } </style> ``` * Each of the lines inside the `body {}` creates a CSS variable, which can be referred to later from inside the Embeddable in the Builder. * Repeat this for the other pages that will host the other brands. </Step> <Step title="Use those CSS variables in the Builder"> * In the Embeddables Builder, refer to those CSS variables in the Designer using the format `var(--font-headings)`. * This will mean that the Embeddable will use whichever value is present for that CSS variable in the current page. * To set a default value for the style property (e.g. to view it in the Builder, or as a backup for parent pages that might not have set up those styles), you can use the format `var(--my-variable-name, [DEFAULT_VALUE])`. * For example: `var(--font-color, #676767)`. </Step> </Steps> </Tab> <Tab title="Method 2: Setting rules from the URL"> <Warning> Depending on the exact load sequence, you may see the original styles flash up for a second. </Warning> <Steps> <Step title="Create an onload Action that sets the CSS variables"> * In the Builder, create an Action, and set up a Trigger so that it runs when the Embeddable Loads. * In the Action custom code, set the CSS variables on the page based on the URL, like in this example: ```js theme={null} function output() { const domain = window.location.host; const styles = getStylesFromDomain(domain); if (styles && typeof styles === "object") { const root = document.documentElement; Object.entries(styles).forEach(([key, value]) => { root.style.setProperty(key, value); }); } } function getStylesFromDomain(domain) { switch (domain) { case "example.com": return { "primary-color": "#ff5733", "secondary-color": "#33ff57", "font-size": "16px", }; case "another-site.com": return { "primary-color": "#3366ff", "secondary-color": "#ffcc00", "font-size": "18px", }; default: return {}; } } ``` </Step> <Step title="Use those CSS variables in the Builder"> * In the Embeddables Builder, refer to those CSS variables in the Designer using the format `var(--font-headings)`. * This will mean that the Embeddable will use whichever value is present for that CSS variable in the current page. * To set a default value for the style property (e.g. to view it in the Builder, or as a backup for parent pages that might not have set up those styles), you can use the format `var(--my-variable-name, [DEFAULT_VALUE])`. * For example: `var(--font-color, #676767)`. </Step> </Steps> </Tab> </Tabs> <Note>Instructions on changing content based on the URL are coming soon.</Note> # Embed via backend integration Source: https://docs.embeddables.com/how-to/embed-backend-integration How to publish your Embeddable from the backend via Server-Side Rendering One of the most powerful ways to publish an Embeddable is to serve it using Server-Side Rendering (SSR) from your backend. To implement this, please reach out to the Embeddables team in order to: 1. Discuss your tech stack and how the integration would work in your case (optional) 2. Get the Embeddables team to enable this approach for your account (required) <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## Embedding via backend SSR integration <Steps> <Step title="Fetch the Embeddable data from our Renderer API endpoint"> Within your backend code that serves your site, start by fetching the Embeddable data from our Renderer API endpoint. **You must pass all query parameters from the request to the Renderer API endpoint** for all Embeddables features to work correctly. ```javascript theme={null} // ... Your existing code ... // @TODO: Replace with your Embeddable ID const EMBEDDABLE_ID = 'flow_aaaaaaaaaaaaa'; // @TODO: Update to create an object with all query parameters appended to the URL requested by the end-user const queryParams = request.query // Construct the URL to the Renderer API endpoint const embeddablesEndpointUrl = new URL(`https://engine.embeddables.com/${EMBEDDABLE_ID}`); // Copy query parameters from request to destination URL Object.entries(queryParams).forEach(([key, value]) => { embeddablesEndpointUrl.searchParams.append(key, value); }); // Fetch the Embeddable HTML and other data from the Renderer API endpoint const response = await fetch(embeddablesEndpointUrl.toString()); const data = await response.json(); ``` </Step> <Step title="Construct the HTML"> Construct the necessary HTML pieces that will need to be included in the final HTML response. This includes, in order of appearance: * **Boilerplate page styles** (optional) - the minimum styles and meta tags to ensure the page is rendered correctly on all devices, if needed. * **Embeddables bundle script** - this loads asynchronously and is the full JS code that makes the Embeddables interactive. * **Embeddable HTML** - this is the initial rendering of the Embeddable, which is inserted here so that the end-user immediately sees the content, even before the bundle script has loaded. * **Embeddables Onload Script** - this is some initial JS that runs immediately on load. It contains code that affects the initial load (e.g. choosing the correct breakpoint for the end-user's device size), meaning that we can't wait for the bundle script to load before executing it. ```javascript theme={null} // Add the minimum styles and meta tags to ensure the page is rendered correctly on all devices. // @TODO: Remove if you've already handled this. const boilerplatePageStyles = ` <style>body { margin: 0; }</style> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8"> `; // Include the Embeddables bundle script. const embeddablesBundleScript = `<script src="https://bundle.trysavvy.com/bundle.js" defer="defer"></script>`; // Include the HTML of the Embeddable itself. const embeddableElement = ` <savvy id="flow-${EMBEDDABLE_ID}"> ${data.html} </savvy> `; // Include the JavaScript that initializes the Embeddable on the client side. const clientSideOnloadScript = `<script>(${data.client_side_js})()</script>`; ``` </Step> <Step title="Combine into the final HTML response"> Combine all the pieces. This is the final HTML response that should be sent to the client (along with any other HTML you want to include, e.g. fonts, scripts, etc.): ```javascript theme={null} const fullHtml = ` <!-- Boilerplate Page Styles --> ${boilerplatePageStyles} <!-- Embeddables Bundle Script --> ${embeddablesBundleScript} <!-- Embeddable HTML --> ${embeddableElement} <!-- Embeddables Onload Script --> ${clientSideOnloadScript} `; // @TODO: Then send the full HTML response to the client. // ... the rest of your code ... ``` </Step> </Steps> ## Complete code examples <Tabs> <Tab title="Node.js (Express)" icon="nodejs"> Here's the complete code example for a Node.js server using Express: ```javascript theme={null} const express = require('express'); const app = express(); app.get('/embed/:flowId', async (req, res) => { // @TODO: Replace with your Embeddable ID const EMBEDDABLE_ID = 'flow_aaaaaaaaaaaaa'; // @TODO: Update to create an object with all query parameters appended to the URL requested by the end-user const queryParams = request.query // Construct the URL to the Renderer API endpoint const embeddablesEndpointUrl = new URL(`https://engine.embeddables.com/${EMBEDDABLE_ID}`); // Copy query parameters from request to destination URL Object.entries(queryParams).forEach(([key, value]) => { embeddablesEndpointUrl.searchParams.append(key, value); }); // Fetch the Embeddable HTML and other data from the Renderer API endpoint const response = await fetch(embeddablesEndpointUrl.toString()); const data = await response.json(); // Boilerplate Page Styles const boilerplatePageStyles = ` <style>body { margin: 0; }</style> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8"> `; // Add the minimum styles and meta tags to ensure the page is rendered correctly on all devices. // @TODO: Remove if you've already handled this. const embeddablesBundleScript = `<script src="https://bundle.trysavvy.com/bundle.js" defer="defer"></script>`; // Include the HTML of the Embeddable itself. const embeddableElement = ` <savvy id="flow-${EMBEDDABLE_ID}"> ${data.html} </savvy> `; // Include the JavaScript that initializes the Embeddable on the client side. const clientSideOnloadScript = `<script>(${data.client_side_js})()</script>`; const fullHtml = ` <!-- Boilerplate Page Styles --> ${boilerplatePageStyles} <!-- Embeddables Bundle Script --> ${embeddablesBundleScript} <!-- Embeddable HTML --> ${embeddableElement} <!-- Embeddables Onload Script --> ${clientSideOnloadScript} `; res.setHeader('Content-Type', 'text/html;charset=UTF-8'); res.send(fullHtml); }); app.listen(3000, () => { console.log('Server running on port 3000'); }); ``` </Tab> <Tab title="Cloudflare Workers" icon="python"> Here's a complete code example for a page served by a Cloudflare Worker: ```javascript theme={null} export default { async fetch(request, env, ctx) { // @TODO: Replace with your Embeddable ID const EMBEDDABLE_ID = 'flow_aaaaaaaaaaaaa'; // @TODO: Update to create an object with all query parameters appended to the URL requested by the end-user const queryParams = request.query // Construct the URL to the Renderer API endpoint const embeddablesEndpointUrl = new URL(`https://engine.embeddables.com/${EMBEDDABLE_ID}`); // Copy query parameters from request to destination URL Object.entries(queryParams).forEach(([key, value]) => { embeddablesEndpointUrl.searchParams.append(key, value); }); // Fetch the Embeddable HTML and other data from the Renderer API endpoint const response = await fetch(embeddablesEndpointUrl.toString()); const data = await response.json(); // Boilerplate Page Styles const boilerplatePageStyles = ` <style>body { margin: 0; }</style> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta charset="UTF-8"> `; // Add the minimum styles and meta tags to ensure the page is rendered correctly on all devices. // @TODO: Remove if you've already handled this. const embeddablesBundleScript = `<script src="https://bundle.trysavvy.com/bundle.js" defer="defer"></script>`; // Include the HTML of the Embeddable itself. const embeddableElement = ` <savvy id="flow-${EMBEDDABLE_ID}"> ${data.html} </savvy> `; // Include the JavaScript that initializes the Embeddable on the client side. const clientSideOnloadScript = `<script>(${data.client_side_js})()</script>`; const fullHtml = ` <!-- Boilerplate Page Styles --> ${boilerplatePageStyles} <!-- Embeddables Bundle Script --> ${embeddablesBundleScript} <!-- Embeddable HTML --> ${embeddableElement} <!-- Embeddables Onload Script --> ${clientSideOnloadScript} `; return new Response(fullHtml, { headers: { "content-type": "text/html;charset=UTF-8", }, }); } } ``` </Tab> </Tabs> # Upgrade to the Embed Code 2.0 Source: https://docs.embeddables.com/how-to/embed-code-2-0 How to install the new embed code and upgrade the Stripe component to match The Embeddables new Embed Code 2.0 is **now available in beta**. <Warning> The old Embed Code will be **deprecated on February 28th, 2026**. </Warning> ## The new Embed Code 2.0 ### The HEAD script <Tip> This script is always the same, regardless of which Embeddable you're embedding. </Tip> To get the Embed Code, head to your Embeddable in the [Embeddables Web App](https://app.embeddables.com/), and click on the Embed button in the top-right corner. ### The BODY script <Tip>This part is unchanged from before.</Tip> ```html theme={null} <savvy id="YOUR_EMBEDDABLE_ID"></savvy> ``` ## Upgrading to the Embed Code 2.0 <Steps> <Step title="Grab the new embed code"> In the [Embeddables Web App](https://app.embeddables.com/), open your Embeddable and click the Embed button in the top-right corner to get the HEAD script. </Step> <Step title="Replace the old embed code with the new one"> In your website or web app, look for a script element (likely in the `<HEAD>` of your page) that starts with `<script>const SAVVY_PRELOAD_IDS=...`. This is the part to replace with the new script. Make sure that when you're done, the new one is sandwiched inside two `<script>` tags. Make sure to do this on a staging URL/environment first, so that you can QA your Stripe changes (described below) on both the old and new embed code. </Step> <Step title="Make any Stripe changes that you need to"> See the Stripe section below for a list of things to watch out for and update before pushing your changes live. <Tip> If you're on the Embeddables Pro Plan, your Primary Contact will do this part for you. </Tip> </Step> <Step title="Test your changes thoroughly"> Run some end-to-end tests, including testing any payments and promo codes, to make sure that everything is working as expected. Then push live! </Step> </Steps> ## Upgrading Stripe components <Note> The Embed Code 2.0 comes with the latest version of Stripe's JS SDK, which includes [certain breaking changes](https://docs.stripe.com/checkout/elements-with-checkout-sessions-api/changelog) to account for. See below for a list of things to watch out for and update before upgrading to the new embed code. </Note> The main changes to watch out for are: 1. The default layout for Stripe components has changed from 'Tabs' to 'Accordion'. *(Step 1 below)* 2. The `create_element_options` settings structure has changed. *(Step 2 below)* 3. The `applyPromotionCode` response structure has changed. *(Steps 3-4 below)* <Steps> <Step title="Set any unset Stripe component 'layout' settings to 'Tabs'"> In Stripe's latest version, the default layout is switching from 'Tabs' to 'Accordion'. Therefore, if you have any Stripe components with the 'layout' setting unset, you'll need to set it to 'Tabs' before upgrading to the Embed Code 2.0. 1. Select your Stripe component in the Web App editor. In the right sidebar under **Options**, find the **Layout** setting. 2. Select the **Tabs** option (only if no option is currently selected). </Step> <Step title="Update create_element_options if applicable"> <Note> This step only applies if you've customized the `create_element_options` settings in your Stripe component. </Note> Open your Stripe component's JSON by clicking the three-dot icon (**⁝**) in the **Options** tab, then select **Edit JSON**. Check if the `create_element_options` key exists. If it contains a `billingDetails` property with multiple keys set to `never`, update `create_element_options.fields.billingDetails` to `never`. </Step> <Step title="Update your applyPromotionCode function logic to handle both 'success' and 'session' response formats"> Stripe's latest SDK version has changed the response schema of `applyPromotionCode`: instead of `response.success` it now returns `response.session`, which contains a new object format. Make sure your function logic handles both formats, so that you can smoothly upgrade to the new embed code. <Tip> If you prefer, you can get your changes ready on the latest version of each Embeddable, and then push all of those versions live at the same time that you push the new embed code live. That way, you can avoid the additional logic of handling both old and new formats in your code. However, the risk here is that there is a window of time when one is live and not the other, or that you later need to roll back for some reason, so take this approach at your own risk! </Tip> 1. Find all references to `applyPromotionCode` in your custom code. 2. Replace conditional checks that use `response.type === 'success'` with a structure that supports both formats (*see the next step for details on the new format*): ```javascript theme={null} if (response.success) { // Keep your existing logic here } else if (response.session) { // Add new logic here (see next step) } ``` 3. Ensure there are no early `return` statements or `throw` errors that would prevent reaching the `session` branch. <Tip> Once you've upgrade to the new embed code, and are confident that you won't need to revert to the old one, you can safely simplify the logic to stop handling the old `response.success` format. </Tip> </Step> <Step title="Copy and adapt logic for the new response schema"> Copy all logic from inside the `response.success` branch into the `response.session` branch. Update field accesses to use the new structure: * Replace `response.success.total` with `response.session.total`. * Access numeric values through `minorUnitsAmount`: * `total`: `response.session.total.total.minorUnitsAmount` * `subtotal`: `response.session.total.subtotal.minorUnitsAmount` * `discount`: `response.session.total.discount.minorUnitsAmount` * `appliedBalance`: `response.session.total.appliedBalance.minorUnitsAmount` * `shippingRate`: `response.session.total.shippingRate.minorUnitsAmount` * `taxExclusive`: `response.session.total.taxExclusive.minorUnitsAmount` * `taxInclusive`: `response.session.total.taxInclusive.minorUnitsAmount` <Tip> For other keys not listed here, refer to the [Stripe Custom Checkout Session object documentation](https://docs.stripe.com/js/custom_checkout/session_object). </Tip> </Step> <Step title="Add error handling"> You can access errors via `response.error?.message`. See the [Stripe applyPromotionCode documentation](https://docs.stripe.com/js/custom_checkout/apply_promotion_code) for details. </Step> <Step title="Test your changes"> Test the modified logic thoroughly: * **With old Stripe component**: Apply a promo code and verify it's reflected in the UI, applied to the amount, and that payment completes successfully. * **With updated Stripe component**: Repeat the same tests. </Step> </Steps> ### Example: Before and After **Before:** ```javascript theme={null} async function applyPromoCode() { try { const response = await window.StripeCheckout.applyPromotionCode( discountCodeUppercase ); if (response.type === "success") { context.setUserData({ promo_code_message: `Your discount $${( response.success.total.discount / 100 ).toFixed(2)} has been applied`, discount_code_applied: discountCodeUppercase, subtotal: (response.success.total.subtotal / 100).toFixed(2), discount: (response.success.total.discount / 100).toFixed(2), total: (response.success.total.total / 100).toFixed(2), checking_promo_code: false, }); } else { context.setUserData({ promo_code_message: response.error.message, checking_promo_code: false, }); } } catch (error) { console.error("Error applying promo code:", error); context.setUserData({ promo_code_message: `Error trying to apply the promo code ${userData.discount_code}`, checking_promo_code: false, }); } } ``` **After:** ```javascript theme={null} async function applyPromoCode() { try { const response = await window.StripeCheckout.applyPromotionCode( discountCodeUppercase ); let subtotal, discount, total, message; if (response.success) { // Old response structure subtotal = (response.success.total.subtotal / 100).toFixed(2); discount = (response.success.total.discount / 100).toFixed(2); total = (response.success.total.total / 100).toFixed(2); message = `Your discount $${discount} has been applied`; } else if (response.session) { // New response structure subtotal = ( response.session.total.subtotal.minorUnitsAmount / 100 ).toFixed(2); discount = ( response.session.total.discount.minorUnitsAmount / 100 ).toFixed(2); total = (response.session.total.total.minorUnitsAmount / 100).toFixed(2); message = `Your discount $${discount} has been applied`; } if (response.success || response.session) { context.setUserData({ promo_code_message: message, discount_code_applied: discountCodeUppercase, subtotal, discount, total, checking_promo_code: false, }); } else { context.setUserData({ promo_code_message: response.error?.message || "Unknown error applying promo code", checking_promo_code: false, }); } } catch (error) { console.error("Error applying promo code:", error); context.setUserData({ promo_code_message: `Error trying to apply the promo code ${userData.discount_code}`, checking_promo_code: false, }); } } ``` # Embed in a React app Source: https://docs.embeddables.com/how-to/embed-in-react How to publish your Embeddable inside a React app Embedding your Embeddable in a React app is straightforward, and looks similar to the process of embedding in a regular website. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## Embedding in React <Steps> <Step title="Grab the embed code"> Head to the Embeddable in the [Web App](https://app.embeddables.com/), click **Embed Code** in the top-right corner, and you'll see the two-part embed code in the pop-up modal. <Frame> <img /> </Frame> </Step> <Step title="Insert the first part in the Head"> The first part of the embed code (the large part) should go in the `<HEAD>` of the page. **This only needs to be added once** - if it's already installed on all pages of your site then you can skip this step. This is the large part that starts like this: ```html Embed Code - Part 1/2 theme={null} <script> const SAVVY_PRELOAD_IDS=... ``` </Step> <Step title="Insert the second part in the Body"> The second part of the embed code should go in the `<BODY>` of the page, wherever you want the Embeddable to appear on the page. The is the part that looks like this (where `flow_abcdefhijklm` is your Embeddable ID): ```html Embed Code - Part 2/2 theme={null} <savvy id="flow_abcdefhijklm"></savvy> ``` </Step> </Steps> ## Embedding in a website or native app? <CardGroup> <Card title="Embed in your site" icon="code" href="/how-to/embed-in-your-site"> How to publish your Embeddable to your site </Card> <Card title="Embed in a Webflow site" icon="webflow" href="/how-to/embed-in-your-webflow-site"> How to publish your Embeddable to your Webflow site </Card> <Card title="Embed in a WordPress site" icon="wordpress" href="/how-to/embed-in-your-wordpress-site"> How to publish your Embeddable to your WordPress site </Card> <Card title="Embed in a Framer site" icon="framer" href="/how-to/embed-in-your-framer-site"> How to publish your Embeddable to your Framer site </Card> <Card title="Embed in a React Native app" icon="mobile-screen-button" href="/how-to/embed-in-react-native"> How to publish your Embeddable inside a React Native app </Card> </CardGroup> ## Troubleshooting ### React is giving me a warning about an unrecognized `<savvy>` tag Sometimes in the console, you will see this warning log from React: ```mdx Console theme={null} Warning: The tag <savvy> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter. ``` This warning can be safely ignored, and should have no impact on your app. However, if you want to avoid the warning appearing in the console, a workaround is to insert the savvy just after the initial render using React's `useEffect` hook: ```javascript App.js theme={null} useEffect(() => { const htmlElement = `<savvy id="flow_abcdefhijklm"></savvy>`; const container = document.getElementById("my-container"); // Select the container that the Embeddable should appear in container.innerHTML = htmlElement; // Use '+=' if you want to append the Embeddable to the contents of the container, instead of replacing the contents }); ``` <Note>Typescript samples will be added to this page soon</Note> # Embed in a React Native app Source: https://docs.embeddables.com/how-to/embed-in-react-native How to publish your Embeddable inside a React Native app Embedding your Embeddable in a React Native app involves using the WebView component that comes as part of the `react-native-webview` library. We use `WebView` to embed an Embeddables preview URL, since we can't pass HTML directly into the `WebView`. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> <Steps> <Step title="Install the react-native-webview library"> If you don't already have it installed, run: ```shell theme={null} npm i react-native-webview ``` </Step> <Step title="Import WebView into your React file"> ```javascript theme={null} import { WebView } from "react-native-webview"; ``` </Step> <Step title="Insert a WebView"> ```javascript theme={null} <WebView source={{ uri: "https://engine.embeddables.com/preview/flow_abcdefhijklm", }} /> ``` </Step> <Step title="Optional: Give it dimensions"> If you can't see it initially, make sure to give it some styles to ensure that it has non-zero zero dimensions: ```javascript theme={null} <WebView style={{ width: 400, }} source={{ uri: "https://engine.embeddables.com/preview/flow_abcdefhijklm", }} /> ``` </Step> <Step title="Optional: Pass properties into User Data"> To pass User Data properties into the Embeddable, include them in the URL as URL params. In the Builder, make sure to add those keys into the Embed URL settings of the Embeddable. ```javascript theme={null} <WebView source={{ uri: "https://engine.embeddables.com/preview/flow_abcdefhijklm?my_key=my_value", }} /> ``` </Step> </Steps> ## Embedding in a website or React app? <CardGroup> <Card title="Embed in your site" icon="code" href="/how-to/embed-in-your-site"> How to publish your Embeddable to your site </Card> <Card title="Embed in a Webflow site" icon="webflow" href="/how-to/embed-in-your-webflow-site"> How to publish your Embeddable to your Webflow site </Card> <Card title="Embed in a WordPress site" icon="wordpress" href="/how-to/embed-in-your-wordpress-site"> How to publish your Embeddable to your WordPress site </Card> <Card title="Embed in a Framer site" icon="framer" href="/how-to/embed-in-your-framer-site"> How to publish your Embeddable to your Framer site </Card> <Card title="Embed in a React app" icon="react" href="/how-to/embed-in-react"> How to publish your Embeddable inside a React app </Card> </CardGroup> *** <Note>Typescript samples will be added to this page soon</Note> # Embed in your Framer site Source: https://docs.embeddables.com/how-to/embed-in-your-framer-site How to publish your Embeddable in your Framer website You can use the 2-part embed code to easily embed your Embeddable in your Framer website. You can choose to install it site-wide or on specific pages. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## Embedding in Framer <Steps> <Step title="Grab the embed code"> Head to the Embeddable in the [Web App](https://app.embeddables.com/), click **Embed Code** in the top-right corner, and you'll see the two-part embed code in the pop-up modal. <Frame> <img /> </Frame> </Step> <Step title="Install the Embeddables script on the site or page"> <Note> **This only needs to be added once** per site or page, depending on your chosen installation method. If it's already installed, you can skip this step. </Note> You have two options for installing the Embeddable in Framer: ### Option 1: Site-wide Installation To install Embeddables across your entire Framer site: 1. Click the gear icon in the top-right corner to open Project Settings 2. Go to the General tab 3. Scroll down to the Custom Code section 4. Paste the first part of the embed code (the long script) in the "End of head tag" input 5. Click Save ### Option 2: Page-specific Installation To install Embeddables on a specific page: 1. Click the gear icon in the top-right corner to open Project Settings 2. Select the specific page under Page Settings in the sidebar 3. Paste the first part of the embed code (the long script) in the "End of head tag" input 4. Click Save </Step> <Step title="Add the Embeddable to your page"> To add the specific Embeddable where you want it to appear: 1. Click the gear icon in the top-right corner to open Project Settings 2. Select the page where you want the Embeddable to appear 3. Go to the "Start of body tag" section 4. Paste the second part of the embed code (the short `<savvy>` tag) 5. Click Save <Note> The Embeddable will not be visible in the Framer preview - you'll need to publish your site to see it. </Note> </Step> </Steps> ## Need to embed in a different platform? <CardGroup> <Card title="Embed in your site" icon="code" href="/how-to/embed-in-your-site"> How to publish your Embeddable to your site </Card> <Card title="Embed in a Webflow site" icon="webflow" href="/how-to/embed-in-your-webflow-site"> How to publish your Embeddable to your Webflow site </Card> <Card title="Embed in a WordPress site" icon="wordpress" href="/how-to/embed-in-your-wordpress-site"> How to publish your Embeddable to your WordPress site </Card> <Card title="Embed in a React app" icon="react" href="/how-to/embed-in-react"> How to publish your Embeddable inside a React app </Card> <Card title="Embed in a React Native app" icon="mobile-screen-button" href="/how-to/embed-in-react-native"> How to publish your Embeddable inside a React Native app </Card> </CardGroup> ## Troubleshooting ### The Embeddable isn't showing up on my Framer site Make sure you've: 1. Added the first part of the code correctly in the "End of head tag" section 2. Added the second part of the code in the "Start of body tag" section 3. Published your site (the Embeddable won't show up in preview) 4. Checked that the Embeddable is pushed live in the Embeddables Builder (if not then you can add `?savvy_flow_version=latest` to the URL to view the latest version) 5. Cleared your browser cache and refreshed the page ### Need more help? Check out Framer's documentation on [adding custom code](https://www.framer.com/academy/lessons/custom-code) or contact our support team for assistance with the Embeddable specifically. # Embed in your site Source: https://docs.embeddables.com/how-to/embed-in-your-site How to publish your Embeddable to your website You can use the 2-part embed code to easily embed your Embeddable in your website or web app. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## Embedding in your site <Steps> <Step title="Grab the embed code"> Head to the Embeddable in the [Web App](https://app.embeddables.com/), click **Embed Code** in the top-right corner, and you'll see the two-part embed code in the pop-up modal. <Frame> <img /> </Frame> </Step> <Step title="Insert the first part in the Head"> The first part of the embed code (the large part) should go in the `<HEAD>` of the page. <Note> **This only needs to be added once**. If it's already installed on all pages of your site then you can skip this step. </Note> This is the large part that starts like this: ```html Embed Code - Part 1/2 theme={null} <script> const SAVVY_PRELOAD_IDS=... ``` </Step> <Step title="Insert the second part in the Body"> The second part of the embed code should go in the `<BODY>` of the page, wherever you want the Embeddable to appear on the page. The is the part that looks like this (where `flow_abcdefhijklm` is your Embeddable ID): ```html Embed Code - Part 2/2 theme={null} <savvy id="flow_abcdefhijklm"></savvy> ``` </Step> </Steps> ## Embedding in another platform? <CardGroup> <Card title="Embed in a Webflow site" icon="webflow" href="/how-to/embed-in-your-webflow-site"> How to publish your Embeddable to your Webflow site </Card> <Card title="Embed in a WordPress site" icon="wordpress" href="/how-to/embed-in-your-wordpress-site"> How to publish your Embeddable to your WordPress site </Card> <Card title="Embed in a Framer site" icon="framer" href="/how-to/embed-in-your-framer-site"> How to publish your Embeddable to your Framer site </Card> <Card title="Embed in a React app" icon="react" href="/how-to/embed-in-react"> How to publish your Embeddable inside a React app </Card> <Card title="Embed in a React Native app" icon="mobile-screen-button" href="/how-to/embed-in-react-native"> How to publish your Embeddable inside a React Native app </Card> </CardGroup> ## Troubleshooting ### React is giving me a warning about an unrecognized `<savvy>` tag Head to our [guide for embedding in React](/how-to/embed-in-react). # Embed in your Webflow site Source: https://docs.embeddables.com/how-to/embed-in-your-webflow-site How to publish your Embeddable in your Webflow website You can use the 2-part embed code to easily embed your Embeddable in your Webflow website. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## Embedding in Webflow <Steps> <Step title="Grab the embed code"> Head to the Embeddable in the [Web App](https://app.embeddables.com/), click **Embed Code** in the top-right corner, and you'll see the two-part embed code in the pop-up modal. <Frame> <img /> </Frame> </Step> <Step title="Add the first part to Webflow's Custom Code"> The first part of the embed code (the large part) should be added to the `<HEAD>` of your Webflow site in the [Custom Code settings](https://help.webflow.com/hc/en-us/articles/33961357265299-Custom-code-in-head-and-body-tags#head-code). This is the large part that starts like this: ```html Embed Code - Part 1/2 theme={null} <script> const SAVVY_PRELOAD_IDS=... ``` <Note> **This only needs to be added once**. If it's already installed on your Webflow site then you can skip this step. </Note> <Tip> Here are Webflow's [official docs on adding custom code to the `<HEAD>`](https://help.webflow.com/hc/en-us/articles/33961357265299-Custom-code-in-head-and-body-tags#head-code). </Tip> To add this in Webflow: 1. Go to Site Settings 2. Navigate to the Custom Code tab 3. Paste the first part of the embed code in the "Head Code" section 4. Click Save </Step> <Step title="Add the second part using Webflow's Embed element"> The second part of the embed code should be added using Webflow's Code Embed element where you want the Embeddable to appear on the page. The is the part that looks like this (where `flow_abcdefhijklm` is your Embeddable ID): ```html Embed Code - Part 2/2 theme={null} <savvy id="flow_abcdefhijklm"></savvy> ``` <Tip> Here are Webflow's [official docs on the Code Embed element](https://help.webflow.com/hc/en-us/articles/33961332238611-Custom-code-embed). </Tip> To add this in Webflow: 1. Drag a Code Embed element onto your page 2. Double-click the Embed element 3. Paste the second part of the embed code 4. Click Save & Close </Step> </Steps> ## Need to embed in a different platform? <CardGroup> <Card title="Embed in your site" icon="code" href="/how-to/embed-in-your-site"> How to publish your Embeddable to your site </Card> <Card title="Embed in a WordPress site" icon="wordpress" href="/how-to/embed-in-your-wordpress-site"> How to publish your Embeddable to your WordPress site </Card> <Card title="Embed in a Framer site" icon="framer" href="/how-to/embed-in-your-framer-site"> How to publish your Embeddable to your Framer site </Card> <Card title="Embed in a React app" icon="react" href="/how-to/embed-in-react"> How to publish your Embeddable inside a React app </Card> <Card title="Embed in a React Native app" icon="mobile-screen-button" href="/how-to/embed-in-react-native"> How to publish your Embeddable inside a React Native app </Card> </CardGroup> ## Troubleshooting ### The Embeddable isn't showing up on my Webflow site Make sure you've: 1. Added the first part of the code to the Head Code section in Project Settings 2. Published your site after making these changes 3. Checked that the Embeddable is pushed live in the Embeddables Builder (if not then you can add `?savvy_flow_version=latest` to the URL to view the latest version) 4. Cleared your browser cache and refreshed the page ### Need more help? Check out Webflow's documentation on [adding custom code to the `<HEAD>`](https://help.webflow.com/hc/en-us/articles/33961357265299-Custom-code-in-head-and-body-tags#head-code) and [adding a Code Embed element](https://help.webflow.com/hc/en-us/articles/33961332238611-Custom-code-embed) or contact our support team. # Embed in your WordPress site Source: https://docs.embeddables.com/how-to/embed-in-your-wordpress-site How to publish your Embeddable in your WordPress website You can use the 2-part embed code to easily embed your Embeddable in your WordPress website. The method you use will depend on whether you're using WordPress.com or WordPress.org, and your technical comfort level. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## WordPress.com vs WordPress.org Before we begin, it's important to understand the difference: * **WordPress.com**: A hosted platform with different plan levels (Free, Personal, Premium, Business, Commerce) * **WordPress.org**: Self-hosted WordPress where you have full control over your site ## Embedding in WordPress.com <Steps> <Step title="Check your plan level"> The ability to add custom code depends on your WordPress.com plan: * **Free, Personal, and Premium plans**: Limited code support * **Business and Commerce plans**: Full code support If you're on a limited plan, you'll need to upgrade to add custom code. </Step> <Step title="Grab the embed code"> Head to the Embeddable in the [Web App](https://app.embeddables.com/), click **Embed Code** in the top-right corner, and you'll see the two-part embed code in the pop-up modal. <Frame> <img /> </Frame> </Step> <Step title="Add the code to your site"> For WordPress.com sites, you have two options: ### Option 1: Using the Custom HTML Block (Easiest) 1. Edit the page where you want to add the Embeddable 2. Add a Custom HTML block 3. Paste both parts of the embed code into the block 4. Save and publish your changes ### Option 2: Using the Customizer (More Control) 1. Go to Appearance → Customize 2. Click on Additional CSS 3. Add the first part of the code (the script) in the CSS section 4. Save and publish 5. Add the second part (the `<savvy>` tag) where you want the Embeddable to appear using a Custom HTML block </Step> </Steps> ## Embedding in WordPress.org <Steps> <Step title="Grab the embed code"> Head to the Embeddable in the [Web App](https://app.embeddables.com/), click **Embed Code** in the top-right corner, and you'll see the two-part embed code in the pop-up modal. <Frame> <img /> </Frame> </Step> <Step title="Choose your embedding method"> For WordPress.org sites, you have several options based on your technical comfort level: ### Option 1: Using a Plugin (Easiest) 1. Install and activate the "Insert Headers and Footers" plugin 2. Go to Settings → Insert Headers and Footers 3. Paste the first part of the code (the script) in the "Scripts in Header" section 4. Save changes 5. Add the second part (the `<savvy>` tag) where you want the Embeddable to appear using a Custom HTML block ### Option 2: Using Theme Files (Intermediate) 1. Install and activate a child theme 2. Edit your child theme's `functions.php` file 3. Add the first part of the code using the `wp_head` hook 4. Add the second part (the `<savvy>` tag) where you want the Embeddable to appear using a Custom HTML block ### Option 3: Direct File Editing (Advanced) 1. Access your site files via FTP or your hosting control panel 2. Edit your theme's `header.php` file 3. Add the first part of the code just before the closing `</head>` tag 4. Add the second part (the `<savvy>` tag) where you want the Embeddable to appear using a Custom HTML block </Step> </Steps> ## Need to embed in a different platform? <CardGroup> <Card title="Embed in your site" icon="code" href="/how-to/embed-in-your-site"> How to publish your Embeddable to your site </Card> <Card title="Embed in a Webflow site" icon="webflow" href="/how-to/embed-in-your-webflow-site"> How to publish your Embeddable to your Webflow site </Card> <Card title="Embed in a Framer site" icon="framer" href="/how-to/embed-in-your-framer-site"> How to publish your Embeddable to your Framer site </Card> <Card title="Embed in a React app" icon="react" href="/how-to/embed-in-react"> How to publish your Embeddable inside a React app </Card> <Card title="Embed in a React Native app" icon="mobile-screen-button" href="/how-to/embed-in-react-native"> How to publish your Embeddable inside a React Native app </Card> </CardGroup> ## Troubleshooting ### The Embeddable isn't showing up on my WordPress site Make sure you've: 1. Added the first part of the code correctly (in the header or using a plugin) 2. Published your changes 3. Checked that the Embeddable is pushed live in the Embeddables Builder (if not then you can add `?savvy_flow_version=latest` to the URL to view the latest version) 4. Cleared your browser cache and refreshed the page ### Need more help? For WordPress.com users, check out the [WordPress.com support documentation](https://wordpress.com/support/code/). For WordPress.org users, you can: * Check the [WordPress Codex](https://codex.wordpress.org/) * Visit the [WordPress Support Forums](https://wordpress.org/support/forums/) * Contact our support team for assistance with the Embeddable specifically # How To: Handle Partial Data Capture in Embeddables Source: https://docs.embeddables.com/how-to/partial-data-capture Storing incomplete user answers, even when they haven't yet completed the entire funnel. **Partial data capture** is easy to achieve in Embeddables. In fact, it is typically enabled by default, because Embeddables collects users' answers as they provide them, and (depending on the field's settings) stores them for use in analytics later on. There are also ways to implement custom partial data capture where you send user answers to your own backend, e.g. your API or a CRM. ## Storing partial user data in Embeddables As described in the [User Data](/features/user-data) guide, each user answer can be stored on three different levels: 1. Only in the context of the current page load (i.e. data is lost when the user refreshes or closes the tab). 2. \#1 and also in Local Storage in the user's browser (i.e. the session is resumed when the user reloads the page again later). 3. \#1, #2 and also in Embeddables' database (i.e. the data is also available for use in analytics in the Embeddables Web App). Therefore, any fields that are configured with option #3 will automatically be captured as partial data, as the user enters the information, and will be visible later in the Embeddables Web App (to logged-in users with sufficient permissions). ## Sending partial user data to your own backend To send partial user data to your own backend, you can: 1. Add an Action with custom code that sends the data to your backend, 2. Add a Trigger to execute that Action when the user reaches a key page or clicks an important button. ### Instructions: <Steps> <Step title="Create a custom code Action"> * Go to the Logic sidebar and click on the Actions tab * Click `+ Add New Action` * Give it a descriptive name like "Send Data to Backend" * Hit `Add` </Step> <Step title="Write your custom code"> * Add code similar to the example below, which sends user data to your backend API: ```js theme={null} // All Actions must contain a function called output() function output(userData) { // Construct the data object with the user's answers const dataToSend = { ...userData, }; // Replace with your backend API endpoint const url = "https://your-api.example.com/data"; // Optional: Add any required headers const headers = { "Content-Type": "application/json", }; // Send POST request fetch(url, { method: "POST", headers, body: JSON.stringify(dataToSend), }); } ``` </Step> <Step title="Add Triggers for user events"> * Switch to the Triggers tab * Add a new Trigger * Choose the event that should trigger sending data. For example: * `WHEN Page IS Changed` to send data on every page change * `WHEN Button KEY submit_button IS Clicked` to send on button click * Select your "Send Data to Backend" Action * Hit `Add` * Repeat for any other events you want to trigger the Action, e.g. all key milestone pages in your funnel </Step> <Step title="Test the integration"> * Preview your Embeddable * Open your browser's developer tools to monitor network requests * Verify that data is being sent to your backend when the triggers fire * Check that the data structure matches what your backend expects </Step> </Steps> <Card title="Learn more about Actions and Triggers" icon="code" href="/features/actions"> Read more about Actions and Triggers in Embeddables. </Card> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> # Place a Hold on a Payment Source: https://docs.embeddables.com/how-to/place-a-hold-on-payment How to place a hold on a customer's payment method and handle delayed subscriptions This guide explains how to place a hold on a customer's payment method, which is particularly useful when you need to verify something (like a prescription approval) before charging the customer. ## What is a Payment Hold? A payment hold: 1. Ensures that the customer has enough funds to cover the cost of the order. 2. Holds the funds for a certain period of time, to ensure they're available to complete the payment. 3. Allows you to verify necessary information before actually charging the customer. ## Important Limitations <Warning> **You will need to manually release the hold on the user's card at a later date to complete the payment.** This is not handled by Embeddables, since it happens at a later stage when the user is no longer interacting with the Embeddable. </Warning> <Note> Stripe only supports placing a hold on one-time payments, not recurring payments. To achieve the effect of a hold on a recurring payment, you'll need to use a combination of a one-time payment and a delayed subscription, as explained below. </Note> ## Common Use Case: Prescription Approval A common use case for payment holds is when you need to verify a prescription before charging the customer. Here's how this typically works: 1. At checkout, place a hold on a one-time payment for the first month's cost. 2. When the prescription is approved, release the hold to charge the customer. 3. At the same time, set up a subscription that's delayed by 30 days. This approach ensures that: * The customer isn't charged until their prescription is approved. * The subscription doesn't start charging until 30 days after the prescription is approved. * The one-time payment effectively replaces the first month of the subscription. ## Implementation Steps <Steps> <Step title="Set up the one-time payment hold"> <Note>This part is handled by **Embeddables**.</Note> * In your Stripe component, select "Payment" as the Checkout mode. * Enable the "Place a hold on the customer's card" option. * Use a one-time payment price ID (not a subscription price ID). </Step> <Step title="Handle the prescription approval"> <Note>This part is handled by **your own backend systems** (not Embeddables).</Note> * When the prescription is approved, release the hold on the payment. * Create a new subscription for the customer [with a 30-day delay](https://docs.stripe.com/changelog/2018-02-05/subscriptions-delay-first-full-invoice-future-date). * The subscription should hopefully use the same payment method that was used for the hold, but you will need to verify this as it's outside of Embeddables' control. </Step> </Steps> <Tip> When you place a hold on a payment, Stripe automatically creates a customer record and stores the payment method. This means you can later attach a subscription to this customer without requiring them to re-enter their payment details. </Tip> ## Best Practices 1. **Clear Communication:** * Clearly communicate to customers that their payment method is being held but not charged. * Explain when they can expect to be charged. * Provide information about the subscription that will start after the first payment. 2. **Error Handling:** * Have a process in place for handling declined payments when releasing the hold. * Consider what happens if the prescription is not approved. 3. **Subscription Management:** * Ensure your backend system can handle the creation of delayed subscriptions. * Consider how you'll manage the transition from the one-time payment to the subscription. For more information on how placing a hold works in Stripe, see the [Stripe documentation](https://docs.stripe.com/payments/place-a-hold-on-a-payment-method). ## Frequently Asked Questions <AccordionGroup> <Accordion title="Can't I just place a hold on the subscription itself?"> No, Stripe does not support placing holds on subscriptions. This is a limitation of Stripe's API. The only way to place a hold on a payment is to use a one-time payment, which is why we recommend the combination of a one-time payment hold followed by a delayed subscription. </Accordion> <Accordion title="Why not cancel the on-hold payment and replace it with a subscription that starts straight away?"> This approach isn't recommended because there can be a delay in processing the refund from the on-hold payment. During this time, the customer might not have enough funds available to cover both the refund and the new subscription charge. This could lead to failed payments and a poor customer experience. </Accordion> <Accordion title="What if I just collect card details at checkout?"> This is a viable approach, and would be simpler because it would enable you to begin a regular subscription as soon as the prescription is approved. However, this wouldn't guarantee that the customer has sufficient funds to cover the subscription. </Accordion> <Accordion title="Can Embeddables handle the release of funds when we're ready to take the payment?"> No, Embeddables cannot handle the release of funds. This needs to be handled by your backend system when you're ready to take the payment. This is because the release of funds typically happens after the user has completed their interaction with the Embeddable (e.g., after prescription approval). </Accordion> <Accordion title="Can Embeddables handle setting up the subscription for me?"> We recommend that you handle the subscription setup in your own backend system. This is because if Embeddables were to set up the subscription for you, the 30-day delay would begin at checkout, rather than when the funds are relased and the customer's first month begins. When you release the hold on the payment, we recommend that you also create the subscription with a 30-day delay [using Stripe's API](https://docs.stripe.com/changelog/2018-02-05/subscriptions-delay-first-full-invoice-future-date). </Accordion> <Accordion title="Do I need to collect the customer's payment details again when setting up the subscription?"> No, you don't need to collect the payment details again. When you place a hold on a payment, Stripe automatically creates a customer record and stores the payment method. You can use this same payment method when creating the subscription. </Accordion> <Accordion title="What happens if the prescription is not approved?"> If the prescription is not approved, you should cancel the hold on the payment. The customer will not be charged, and no subscription will be created. </Accordion> <Accordion title="Can I use this approach for other types of verification, not just prescriptions?"> Yes, this approach can be used for any scenario where you need to verify something before charging the customer. The same principles apply - place a hold on a one-time payment, verify the necessary information, then release the hold and set up the subscription when ready. </Accordion> <Accordion title="Why use a delayed subscription instead of a free trial?"> While both approaches can achieve a similar result, delaying the subscription's first invoice is generally preferred over using a free trial. This is because: * It creates a clearer invoice history for the customer, showing their first payment followed by regular subscription charges. * It avoids potential confusion that can arise from seeing "free trial" followed by a charge. * It better matches the actual business logic (you're not giving away a free month, you're just delaying the start of the subscription). [Stripe supports this](https://docs.stripe.com/changelog/2018-02-05/subscriptions-delay-first-full-invoice-future-date) through the `billing_cycle_anchor` parameter, which lets you set a future date for the first full invoice. This can be combined with prorations if needed. </Accordion> </AccordionGroup> # How To: Create Programmatic SEM Pages using the Embeddables CMS Source: https://docs.embeddables.com/how-to/programmatic-sem-pages-from-cms Learn how to use the Embeddables CMS to create and manage dynamic SEM pages from a single template The **Embeddables CMS** can be used to create and manage multiple landing pages from a single template, making it perfect for Search Engine Marketing (SEM) campaigns. This guide will show you how to set up and use the CMS for programmatic page creation. ## Overview This approach allows you to: * Create hundreds of targeted landing pages from a single template * Manage all page content through a user-friendly CMS interface * Update content without code changes or deployments * Target different audiences and keywords effectively * Maintain consistent design while varying content * Keep an audit trail of updates to your SEM pages, via the Embeddables CMS's version control system ## Setting Up Your CMS Table ### 1. Create a New Table 1. Navigate to the CMS section in the Embeddables Web App 2. Click "Edit Tables" 3. Click "+ Add Table" 4. Select the "Programmatic Pages" template ### 2. Configure Your Columns The template comes with recommended columns, but you can customize them based on your needs. Here's a suggested structure: | Column Name | Column Key | Type | Purpose | | ------------------ | ---------------- | ---- | ---------------------------------------------- | | **Base Path** | `base_path` | Text | Defines the URL pattern to match with the page | | **Title** | `title` | Text | Page title to display in the h1 | | **Subtitle** | `subtitle` | Text | Page subtitle to display in the h2 | | **Hero Image URL** | `hero_image_url` | Text | URL of the hero image for the page | | **CTA Text** | `cta_text` | Text | Call-to-action button text | | **CTA Link** | `cta_link` | Text | Call-to-action button URL | <Tip> You can add additional columns based on your specific needs, such as: * Extra columns for more custom content further down the page * A `product_keys` column with a comma-separated list of keys for the products to feature on the page - you could set up a Computed Field that uses this to filter from a larger list of products, and use Repeatable Option Buttons to display them in the page's content. </Tip> ### 3. Add some rows Add some content by adding at least the first few rows - i.e. the first few pages of content. After the next few steps, you can use this to test out your new programmatic pages. ## Creating Your Embeddable Template ### 1. Connect to the CMS <Steps> <Step title="Add a Content Source"> * Create a new Embeddable * In the Embeddables Builder, go to Embeddable Options * Scroll to the CMS section * Click "+ Add Content Source" </Step> <Step title="Configure the Connection"> * Enter a User Data key (e.g., `cms`) * Select your programmatic pages table * Under "Which records should get shown?", choose "Records that match a URL pattern" * Set the Controller Key to `base_path` * Leave "Multiple Records" unchecked since we want one page per record </Step> <Step title="Register the Key"> * Add your User Data key to the list of Registered Keys in Embeddable Options </Step> </Steps> ### 2. Design Your Template Create a single Embeddable template that will serve as the base for all your SEM pages. Include: * A dynamic title section that pulls from the CMS * Content areas that can be populated from the CMS * Image components that use the CMS image URLs * CTA buttons with dynamic text and links In each of the content areas, use `{{cms.title}}`, `{{cms.subtitle}}` etc to display the content from the CMS. ## Previewing and Publishing <Warning> When testing your Embeddable on a preview link, add <code>savvy\_test=false</code> to the URL to ensure CMS data loads correctly. </Warning> To preview the page, open a preview link and add the part of the URL that matches the `base_path` column after a `?` in the URL. This will match the URL pattern required for the particular page. For example, if the `base_path` is `seo-services`, the URL will be `https://engine.embeddables.com/preview/flow_aaaaaaa?savvy_test=false&base_path=/product-iphone`. * Use the `savvy_cms` URL parameter to preview the page with the latest version of the CMS data: * `savvy_cms=latest` to preview the latest version * `savvy_cms=123` to preview a specific version * `savvy_cms=staging` to preview the staging version ## Best Practices 1. **URL Structure** * Use SEO-friendly URL slugs * Keep URLs short and descriptive * Use hyphens to separate words 2. **Content Management** * Maintain consistent formatting across pages * Use the rich text editor for content formatting * Keep meta descriptions between 150-160 characters 3. **Testing** * Preview all pages before publishing * Test different CMS versions using the version control system * Verify all dynamic content loads correctly ## Example Use Cases 1. **Service Pages** * Create targeted pages for different services * Customize content for specific industries * Use location-based variations 2. **Campaign Landing Pages** * Create pages for different marketing campaigns * A/B test different content variations * Target specific audience segments 3. **Product Pages** * Create pages for different product categories * Customize content for different price points * Target specific customer needs <Tip> Use the CMS version control system to create and test different content variations before pushing them live. This is especially useful for A/B testing different approaches to your SEM pages. </Tip> ## Troubleshooting 1. **Content Not Loading** * Verify the URL slug matches exactly * Check that the Content Source is properly configured * Ensure you're using `savvy_test=false` in preview URLs 2. **Version Control Issues** * Make sure you're using the correct `savvy_cms` parameter * Verify that the version exists in the CMS * Check that you have permission to access the version For additional help or questions, please contact the Embeddables support team. # How To: Retrieve User Data on Different Devices Source: https://docs.embeddables.com/how-to/retrieve-user-data-on-different-device Resuming a user's progress after remarketing to them by email or SMS This guide explains how to implement user session recovery when users access your application from a different device, typically through email or SMS remarketing campaigns. ## Overview To securely allow users to resume their session on a different device, you'll need to: 1. Create a **Token Signing Secret** for your project in the Embeddables dashboard. 2. Use that secret to generate a **Hashed Token** from the user's Entry ID and an expiration timestamp. 3. Send users a secure URL containing the Hashed Token, Entry ID, and expiration timestamp. 4. When a user opens the URL, use the Hashed Token and other parameters to call the Embeddables API from the client, which will return the User Data. 5. Use the retrieved User Data to restore the user's session. ## Setting up your Token Signing Secret <Steps> <Step title="Open Settings"> In the Embeddables web app, go to **Settings → Credentials & Endpoints**. </Step> <Step title="Create a new credential"> Click **+ New Internal Credential**. * Set **Key type** to **Token Signing Secret**. * Choose the production **Environment**. * Add a descriptive **Label** (e.g. `Token Signing Secret`). </Step> <Step title="Store the secret securely"> Copy the generated value and store it as a backend environment variable (e.g. `EMBEDDABLES_LOAD_ENTRY_SECRET_KEY`). <Warning> The secret is only shown once at creation time. If you lose it, you'll need to create a new one — the old key will stop working immediately. Store it somewhere safe before leaving the page. </Warning> <Warning> Never expose this secret in client-side code or commit it to version control. </Warning> </Step> </Steps> ## Generating the Hashed Token and secure URL Use the secret from the previous step to generate a signed token on your backend: ```javascript theme={null} // Required parameters const secretKey = process.env.EMBEDDABLES_LOAD_ENTRY_SECRET_KEY const projectId = "proj_aaabbbccc" // Your Project ID const entryId = "entry_aaabbbcccdddeeefff" // The ID of the user's entry const expiresAt = "2025-01-01T00:00:00.000Z" // ISO format timestamp // Generate the hash const stringToHash = `${secretKey}---${entryId}---${expiresAt}` const token = sha256(stringToHash, 'utf8', 'hex') // Construct the final URL const url = `https://yourwebsite.com/flow?token=${token}&entry_id=${entryId}&expires_at=${expiresAt}` ``` <Tip> Each URL should have a reasonable expiration window, such as 7 or 30 days. </Tip> ## Client-side implementation Create an Action in your Embeddable, triggered on **Embeddable Load**, with the following code to fetch and restore the user's session: ```javascript theme={null} // @TODO: Replace these with your own values const EMBEDDABLE_ID = '<EMBEDDABLE_ID>' // Your Embeddable ID const PROJECT_ID = '<PROJECT_ID>' // Your Project ID const TOKEN_URL_PARAM_KEY = 'token' const ENTRY_ID_URL_PARAM_KEY = 'entry_id' const EXPIRY_URL_PARAM_KEY = 'expires_at' // All Actions must contain a function called output() function output(userData, helperFunctions, triggerContext) { // Grab the token from the URL const urlParams = (new URL(window.location)).searchParams const token = urlParams.get(TOKEN_URL_PARAM_KEY) const entryId = urlParams.get(ENTRY_ID_URL_PARAM_KEY) const expiresAt = urlParams.get(EXPIRY_URL_PARAM_KEY) // If we don't have all of token + entry ID + expiry date, // stop here (this is a new user or the provided data is incomplete) if (!token || !entryId || !expiresAt) return // Fetch the User Data from the Embeddables API console.log('Retrieving User Data', { token }) fetch( "https://load-entry-data-worker.heysavvy.workers.dev", { body: JSON.stringify({ flow_id: EMBEDDABLE_ID, project_id: PROJECT_ID, entry_id: entryId, token, expires_at: expiresAt, }), headers: { "Content-Type": "application/json", }, method: "POST", } ).then(async (res) => { const data = await res.json(); console.log('Retrieved User Data: ', { data }) // If the User Data is successfully retrieved, // set the user data and navigate to the user's current page if (data && data.entry) { helperFunctions.setUserData({ ...data.entry }); window.Savvy.goToPage(EMBEDDABLE_ID, data.entry.current_page_key) } }).catch((err) => { console.error('Error Retrieving User Data:', { err }) }) } ``` ## Important Notes 1. Your Token Signing Secret should always be stored as an encrypted backend environment variable and never exposed in client-side code. 2. Each URL should have a reasonable expiration window, e.g. 7 days or 30 days. 3. The Hashed Token is unique per user and expiration date. ## Troubleshooting If you're experiencing issues: 1. Verify that the secret key is correctly set in your environment variables. 2. Ensure the expiration timestamp is in ISO 8601 format (e.g. `2025-01-01T00:00:00.000Z`). 3. Check that all URL parameters (`token`, `entry_id`, `expires_at`) are properly URL-encoded. 4. Confirm the `project_id` in the POST body matches the project your Token Signing Secret belongs to. 5. Make sure you're using a **Token Signing Secret** created under **Settings → Credentials & Endpoints** — legacy per-flow keys are no longer provisioned and the legacy endpoint does not accept these credentials. *** <AccordionGroup> <Accordion title="Legacy endpoint (deprecated)"> <Warning> **New Token Signing Secrets are no longer provisioned via the legacy system as of May 26, 2026.** Existing legacy keys continue to work on the legacy endpoint, but migration to the new endpoint is strongly recommended. </Warning> If you set up this feature before May 26, 2026, your integration may be using the legacy endpoint. It remains functional for existing keys but will not accept credentials created through the Embeddables dashboard. **Legacy endpoint:** `https://ierxexdtyashuotcsjyo.supabase.co/functions/v1/load_entry_data` **Differences from the current endpoint:** | | Legacy | Current | | ----------------------- | --------------------------- | ----------------------------------------------------- | | Endpoint | Legacy endpoint (above) | `https://load-entry-data-worker.heysavvy.workers.dev` | | `expires_at` field name | `expires` | `expires_at` | | `project_id` in body | Not required | Required | | Key provisioning | Contact Embeddables support | Self-service via Settings | **Legacy client code (for reference only):** ```javascript theme={null} fetch( "https://ierxexdtyashuotcsjyo.supabase.co/functions/v1/load_entry_data", { body: JSON.stringify({ flow_id: EMBEDDABLE_ID, entry_id: entryId, token, expires: expiresAt, // Note: "expires", not "expires_at" }), headers: { "Content-Type": "application/json" }, method: "POST", } ) ``` To migrate, follow the [Setting up your Token Signing Secret](#setting-up-your-token-signing-secret) section above and update your client code to use the new endpoint and POST body. </Accordion> </AccordionGroup> # Publish your Embeddable via a Reverse Proxy Source: https://docs.embeddables.com/how-to/reverse-proxy-setup Configure a reverse proxy to serve Embeddables on your domain This guide explains how to set up a reverse proxy to serve Embeddables content through your own domain. <Tip> Looking for other ways to publish your Embeddable? Check out the [Publish to your site](/features/publish-to-your-site) guide. </Tip> ## Overview Publishing your Embeddable via a reverse proxy can be done in 2 steps: 1. Point your page to our proxy server (`https://proxy.embeddables.com`) 2. Pass the `X-Embeddables-Id` header with the correct Embeddable ID ## Advantages of using a reverse proxy * **Simplicity**: Unlike embedding in your site via our embed code, there is no need to set up a blank page on your site to host the Embeddable. * **Speed**: The Embeddable rendered directly from our proxy server, so the extra client-side fetch from our embed code is skipped. ## Setting up a reverse proxy <Steps> <Step title="Point your page to our proxy server"> Configure your reverse proxy to point to `https://proxy.embeddables.com`. This is the main endpoint that will serve your Embeddable content. For example, in Nginx: ```nginx theme={null} location /your-reverse-proxy-page { proxy_pass https://proxy.embeddables.com; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } ``` </Step> <Step title="Verify the proxy is pointing to the correct page"> Visit `https://yourdomain.com/your-reverse-proxy-page` in your browser. You should see content similar to what appears on `https://proxy.embeddables.com`. <Frame> <img alt="Reverse Proxy Test" /> </Frame> </Step> <Step title="Pass the correct Embeddable ID"> Add the `X-Embeddables-Id` header to your proxy configuration. This header tells the proxy which specific Embeddable to serve. In Nginx, add this to your configuration: ```nginx theme={null} proxy_set_header X-Embeddables-Id "your_embeddable_id"; ``` In Apache: ```apache theme={null} RequestHeader set X-Embeddables-Id "your_embeddable_id" ``` </Step> <Step title="Add metadata headers (optional)"> You can customize the page metadata by adding these optional headers: * `X-Embeddables-Title`: Sets the page title * `X-Embeddables-Description`: Sets the page meta description * `X-Embeddables-Favicon`: Sets the page favicon URL </Step> <Step title="Verify that it's working"> Refresh your page at `https://yourdomain.com/your-reverse-proxy-page`. You should now see your specific Embeddable being served with your custom metadata. Common issues at this stage: * Incorrect Embeddable ID * Missing or malformed headers * Caching issues (try clearing your browser cache) * Propagation time (try waiting a few minutes) </Step> </Steps> ## Example Configurations ### Apache ```apache theme={null} <Location "/your-reverse-proxy-page"> ProxyPass https://proxy.embeddables.com ProxyPassReverse https://proxy.embeddables.com RequestHeader set X-Embeddables-Id "your_embeddable_id" RequestHeader set X-Embeddables-Title "Your Custom Title" RequestHeader set X-Embeddables-Description "Your custom description for SEO" RequestHeader set X-Embeddables-Favicon "https://yourdomain.com/favicon.ico" </Location> ``` ### Nginx ```nginx theme={null} location /your-reverse-proxy-page { proxy_pass https://proxy.embeddables.com; proxy_set_header X-Embeddables-Id "your_embeddable_id"; proxy_set_header X-Embeddables-Title "Your Custom Title"; proxy_set_header X-Embeddables-Description "Your custom description for SEO"; proxy_set_header X-Embeddables-Favicon "https://yourdomain.com/favicon.ico"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } ``` ## Common Issues 1. **Content Not Loading** * Verify your reverse proxy configuration. * Check that the `X-Embeddables-Id` header is correctly set. * Check that the Embeddable you're trying to access has been **pushed to production**. * If not, then you'll need to add the `?savvy_flow_version=latest` query parameter to the URL to preview the latest version. * Ensure your domain's SSL certificate is valid. * Clear your browser cache. * Wait a few minutes for propagation to take effect. 2. **Wrong Content Appearing** * Double-check the Embeddable ID. * Verify the proxy\_pass URL is correct. * Clear your browser cache. # How To: Run custom code when the Embeddable loads Source: https://docs.embeddables.com/how-to/run-custom-code-on-load Running JS code on first load, e.g. to run a function or add an event listener <Frame> <img /> </Frame> <Steps> <Step title="Add a Trigger for on first load"> * Open the Logic card. * Switch to the Triggers tab. * Add a new Trigger. * Select `When "Flow" Is "Loaded"`. </Step> <Step title="Create a custom code Action"> * Click "Create New Action". * Give it a name. * Hit Apply. </Step> <Step title="Write your custom code"> * Add whatever custom JS you like inside the `output()` function. * The first argument of `output()` is the User Data, and the other arguments are [described here](/guides/custom-code). * The code will execute on the frontend, in the main window environment, so has access to window functions and variables. </Step> </Steps> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> # How To: Call APIs securely via the Embeddables Secure Proxy Source: https://docs.embeddables.com/how-to/securely-call-apis-via-proxy Learn how to securely call external APIs using the Embeddables Proxy by configuring credentials and endpoints in your project settings. The Embeddables Secure Proxy is great for solving one or both of the following when calling APIs from the frontend in an Embeddable: * You want to call an API that requires a credential, but you don't want to that credential to be exposed on the frontend. * You're having issues with CORS when calling an API from the frontend. ## How the Secure Proxy Works The new proxy comes with the following features: * **Allowlisting of endpoints and credentials**: Only allowlisted endpoints and credentials can be used for the specific environment + project combination. Disallowlisted endpoints or credentials will be blocked. * **Request results are returned as-is**: The proxy does not tamper with the received requests in any way. What you see is what you get. * **`X-Proxy-Worker-Success` header**: This header will be `true` if the proxy made your request, and `false` if it wasn't able to for some reason. If you receive a 400 response and are unsure if it's coming from the proxy or the final destination, check this header. ## Adding Credentials and Endpoints in Project Settings <Steps> <Step title="Go to Project Settings"> Navigate to your project's settings and select the <strong>Credentials & Endpoints</strong> tab. </Step> <Step title="Add Credentials"> Click the <strong>+ New Credential</strong> button and fill in the required fields: <ul> <li> <strong>Service Type</strong>: Select the type of service (e.g., EHR, CRM, etc.) </li> <li> <strong>Service Name</strong>: The name of the service (e.g., Tellescope, CareGLP, etc.) </li> <li> <strong>Key Type</strong>: The type of key (e.g., API Key, Token, etc.) </li> <li> <strong>Environment</strong>: Choose the environment (Production, Staging, etc.) </li> <li> <strong>Label</strong>: A descriptive label for your credential </li> <li> <strong>Value</strong>: The actual credential value (the secret that you're protecting) </li> </ul> Your credentials will appear in the list, each with a unique identifier and label. </Step> <Step title="Add Endpoints"> Click the <strong>+ New Endpoint</strong> button and fill in the required fields: <ul> <li> <strong>Label</strong>: A descriptive label for the endpoint </li> <li> <strong>Environment</strong>: Select the environment </li> <li> <strong>Allowed Origins</strong>: The user-facing URLs that requests will come from (e.g., your frontend domains). Use <strong>Domain</strong> mode to enter a root domain (e.g. <code>example.com</code>) — all subdomains and paths are matched automatically. Switch to <strong>Regex</strong> mode if you need a custom pattern (e.g. <code>^[https://example\\.com\$](https://example\\.com\$)</code>). </li> <li> <strong>Allowed Destinations</strong>: The API or server URLs that requests will be forwarded to. Same Domain/Regex toggle applies — enter just the domain or a full regex pattern depending on your needs. </li> <li> <strong>Credential IDs</strong>: Select the credentials that can be used with this endpoint </li> </ul> Your endpoints will appear in the list, showing the allowlisted origins, destinations, and associated credentials. </Step> </Steps> ## Using Credentials in Your API Requests Once your credentials and endpoints are set up, you can use them in your API calls through the proxy. Here’s an example of how to use a credential in your request headers: ```js filename="secure-proxy-body-templating.js" theme={null} const weightLossProductsOptions = { method: "POST", headers: { "My-Secret-Header": "{{cred_aaaaaaaaaaaaaa}}", // Use the credential reference here "Content-Type": "application/json", "X-Environment": "prod", "X-Project-ID": "pr_bbbbbbbbbbbbbb", }, body: JSON.stringify(weightLossProductsBody), }; fetch( `https://proxy-worker.heysavvy.workers.dev/?url=${encodeURIComponent( "https://api.example.com/api/v1/endpoint" )}`, weightLossProductsOptions ) .then((response) => { // handle response }) .catch((err) => console.error(err)); ``` * Replace the following placeholders with your actual values: * `My-Secret-Header` --> the header name that you want to use to pass the credential value. * `{{cred_aaaaaaaaaaaaaa}}` --> the credential ID that you created previously. You can use credential placeholders in the URL, headers, or request body (see below). Either way, the proxy will inject the actual credential value for you. * `prod` --> the environment you want to call the endpoint in. * `pr_bbbbbbbbbbbbbb` --> the project ID for your project (grab it from the URL in the Embeddables Web App - it’s the ID after `/project/`). * `https://api.example.com/api/v1/endpoint` --> the actual endpoint you want to call. * Make sure the endpoint and credential are both allowlisted with the project and environment that you’re using. ## Using Credentials in the Request Body By default the proxy streams the request body through to the destination unchanged (efficient, no buffering). To substitute `{{ credential_id }}` placeholders in the body as well, add the `X-Use-Body-Templating: true` header to your request: ```js theme={null} const options = { method: "POST", headers: { "Content-Type": "application/json", "X-Environment": "prod", "X-Project-ID": "pr_bbbbbbbbbbbbbb", "X-Use-Body-Templating": "true", // opt in to body templating }, body: JSON.stringify({ client_secret: "{{cred_aaaaaaaaaaaaaa}}", // placeholder in the body other_field: "some value", }), }; fetch( `https://proxy-worker.heysavvy.workers.dev/?url=${encodeURIComponent( "https://api.example.com/api/v1/endpoint" )}`, options ); ``` **Important notes:** * Body templating applies to POST, PUT, PATCH, and DELETE requests — it is ignored on GET and HEAD. * The `X-Use-Body-Templating` header is stripped before the request is forwarded to the destination. * The body is read as UTF-8 text and the same credential allowlist as URL/header substitution is enforced. Requests referencing a credential not allowlisted for the endpoint will be blocked with a 404. * Do **not** enable body templating for binary payloads — use streaming pass-through (the default) for those. # Embeddables and Customer.io Source: https://docs.embeddables.com/how-to/send-data-to-customer-io Setting up an Action with custom code that sends user data to Customer.io's API The best way to send data to Customer.io is using custom code in an Action, that is Triggered when a user takes an action, such as entering their email or completing a purchase. ## Sending User Data to Customer.io <Note> This is implemented using the [Customer.io Pipelines API](https://docs.customer.io/integrations/api/cdp/#operation/identify). </Note> <Steps> <Step title="Set up Customer.io HTTP Source"> * Go to Customer.io and navigate to the Sources tab * Click "Add Source" * Select "HTTP" and click "Next" * Give the source a name and copy your API Key </Step> <Step title="Create a custom code Action"> * Go to the Logic sidebar and click on the Actions tab * Click `+ Add New Action` * Give it a descriptive name (e.g. "Identify User in Customer.io") * Hit `Add` </Step> <Step title="Write your custom code"> Add the following code to your Action: <Tip> If your Customer.io account is in the EU region, use `https://cdp-eu.customer.io/v1/track` as the endpoint instead. </Tip> ```js theme={null} async function output(userData, context) { // @TODO: Replace with your actual company name, // and provide the API key to Embeddables to store securely on the backend const API_KEY_IDENTIFIER = "{{YOUR_COMPANY_NAME---customerio---api_key}}"; // Construct the customer data object const customerData = { type: "identify", userId: userData.entryId, // Using entryid as the identifier traits: { email: userData.email, name: userData.name, first_name: userData.first_name, last_name: userData.last_name, // Add any other user attributes you want to track }, context: { // Optional: Add context about the source of the data // For example, you could add the page URL or referrer page: { url: window.location.href, title: document.title, referrer: document.referrer }, userAgent: navigator.userAgent, channel: "browser", } }; // Construct the fetch options const requestOptions = { method: "POST", headers: { "Authorization": API_KEY_IDENTIFIER, "Content-Type": "application/json", }, body: JSON.stringify(customerData), }; // Customer.io Pipelines API endpoint (US region) const url = "https://cdp.customer.io/v1/track"; try { // Send POST request, using the Embeddables backend proxy to keep the API key secure const response = await fetch( `https://proxy-secure.trysavvy.com/?url=${encodeURIComponent(url)}`, requestOptions ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } } catch (error) { console.error("Error identifying/updating user in Customer.io:", error); throw error; } } ``` </Step> <Step title="Add a Trigger for your Action"> * Switch to the Triggers tab * Add a new Trigger * Choose when to identify/update the user * Example: `WHEN Page KEY user_info_page IS Completed` * Select your "Identify User in Customer.io" Action * Hit `Add` </Step> </Steps> ## Optional: Tracking Events in Customer.io <Accordion title="Optional: Tracking Events"> If you also want to track specific events in Customer.io, you can create an additional Action: <Note> This is implemented using the [Customer.io Pipelines API](https://docs.customer.io/integrations/api/cdp/#operation/track). </Note> 1. Create another Action named "Track Event in Customer.io" 2. Add this code: <Tip> If your Customer.io account is in the EU region, use `https://cdp-eu.customer.io/v1/track` as the endpoint instead. </Tip> ```js theme={null} async function output(userData, context) { // @TODO: Replace with your actual company name, // and provide the API key to Embeddables to store securely on the backend const API_KEY_IDENTIFIER = "{{YOUR_COMPANY_NAME---customerio---api_key}}"; // Construct the event data const eventData = { type: "track", userId: userData.entryId, // Must match the identifier used in identify/update event: "form_completed", // Change this to your event name properties: { form_name: context.formName, // Add any other event properties you want to track }, context: { page: { url: window.location.href, title: document.title, referrer: document.referrer }, userAgent: navigator.userAgent, channel: "browser" }, timestamp: new Date().toISOString() // ISO 8601 timestamp }; // Construct the fetch options const requestOptions = { method: "POST", headers: { "Authorization": API_KEY_IDENTIFIER, "Content-Type": "application/json", }, body: JSON.stringify(eventData), }; // Customer.io Pipelines API endpoint (US region) const url = "https://cdp.customer.io/v1/track"; try { // Send POST request, using the Embeddables backend proxy to keep the API key secure const response = await fetch( `https://proxy-secure.trysavvy.com/?url=${encodeURIComponent(url)}`, requestOptions ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } } catch (error) { console.error("Error tracking event in Customer.io:", error); throw error; } } ``` 3. Add a Trigger for when you want to track an event * Example: `WHEN Button KEY submit_button IS Clicked` * Select your "Track Event in Customer.io" Action </Accordion> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> <Card title="Learn more about Customer.io's APIs" icon="book" href="https://customer.io/docs/api/"> Read more about Customer.io's APIs, including detailed information about available endpoints and data formats. </Card> # How To: Send analytics events to Embeddables Source: https://docs.embeddables.com/how-to/send-data-to-embeddables Sending data server-side to the Embeddables API Although most user events occur when the user interacts with your Embeddable, meaning Embeddables will automatically capture that event, there are times when user events occur elsewhere. For example, a user completing a doctor's appointment, or a user completing a purchase that isn't handled by Embeddables' Stripe integration. In those cases, you can [send data to the Embeddables API using a server-side request](/api-reference/events/create-event). ## How to send data to Embeddables <Steps> <Step title="Generate an Embeddables API key"> * Contact the Embeddables team to generate an API key for your project. * API keys are Restricted Keys, and always start with `rk_` - for example: `rk_myexampleapikey`. </Step> <Step title="Prepare your event data"> * Create a JSON object with the following fields. ### Required fields * `embeddable_id`: The ID of your Embeddable * `timestamp`: The time the event occurred (in ISO 8601 format) * `custom_event_name`: A name for your custom event (must be snake\_case) * `identifiers`: A list of key-value pairs to identify the user (e.g., email) ### Optional fields * `custom_event_props`: Any custom properties as a JSON string. For events where you want to send structured metrics and dimensions (for example purchase events), we recommend sending an object with `payload_metrics` (numbers) and `payload_dimensions` (strings) as described below. <Tip> The `identifiers` field is an array of objects, each containing a `key` and `value`. Currently, we only support one identifier per request (i.e. one object in the array). </Tip> <Warning> **Watch out for this:** The `value` field must be a stringified JSON value. E.g. for email addresses, the value must be `"\"test@test.com\""` (note the double quotes). </Warning> </Step> <Step title="Send the event to the API"> * Make a POST request to `/projects/{projectId}/events` * Include your API key in the `X-Api-Key` header * Send the event data in the request body Example request: ```bash theme={null} curl -X POST https://api.embeddables.com/projects/pr_myexampleproject/events \ -H "X-Api-Key: rk_myexampleapikey" \ -H "Content-Type: application/json" \ -d '{ "embeddable_id": "flow_myexampleflow", "timestamp": "2022-01-01T00:00:00.000Z", "custom_event_name": "my_custom_event", "custom_event_props": "{\"my_prop\": \"my_val\"}", "identifiers": [ { "key": "email", "value": "\"test@test.com\"" } ] }' ``` </Step> </Steps> ## Example: Structured metrics and dimensions payload For events where you want to send a mix of numeric metrics and categorical dimensions, we recommend structuring `custom_event_props` as a JSON string containing: * **payload\_metrics**: A key–value object where all values are numbers (for example `order_value`, `revenue`, `subtotal`). * **payload\_dimensions**: A key–value object where all values are strings (for example `plan`, `bundle`, `purchase_type`). This follows a metrics-vs-dimensions approach and keeps your data easy to analyze. The same schema applies when tracking [custom client-side events](/features/analytics#custom-client-side-events). Example request for a purchase event using this schema: ```bash theme={null} curl -X POST https://api.embeddables.com/projects/pr_myexampleproject/events \ -H "X-Api-Key: rk_myexampleapikey" \ -H "Content-Type: application/json" \ -d '{ "embeddable_id": "flow_myexampleflow", "timestamp": "2022-01-01T00:00:00.000Z", "custom_event_name": "purchase_completed", "custom_event_props": "{\"payload_metrics\": {\"order_value\": 99.99, \"revenue\": 89.99}, \"payload_dimensions\": {\"plan\": \"premium\", \"bundle\": \"annual\", \"purchase_type\": \"subscription\"}}", "identifiers": [ { "key": "email", "value": "\"test@test.com\"" } ] }' ``` Inside `custom_event_props`, the JSON object has numeric values under `payload_metrics` and string values under `payload_dimensions`. You can add your own keys while respecting those types. <Note> The API will return a 200 status code if the event was successfully created. If there are any issues with the request (e.g., invalid API key, missing required fields), you'll receive an appropriate error response. </Note> ## Troubleshooting ### I'm getting a 404 `ENTRY_NOT_FOUND` response This means that the Embeddables API is trying to find a user that matches the `identifiers` you provided, for the `embeddable_id` you provided, but couldn't find one. Things to look out for: 1. Check that you are passing the correct `embeddable_id` - this should be the ID of the Embeddable that matches the user you're trying to add an event for. 2. Check that you are passing the correct `key` inside `identifiers` - this should be the key in the User Data that matches the input component that the user typed in, such as `email` or `user_email`. 3. Check that you are passing the correct `value` inside `identifiers` - make sure that this matches the user that you're trying to track, and matches the key and Embeddable ID that you're using. 4. Check that you are formatting `identifiers` correctly - the value must be a stringified JSON value. E.g. for email addresses, the value must be `"\"test@test.com\""` (note the double quotes). This can become double-escaped (e.g. `"\\"test@test.com\\""`) in certain code environments. 5. Check that the user has definitely provided their unique identifier before the track event is triggered - trying to track a user based on an email address that hasn't been captured yet is impossible. # Embeddables and Klaviyo Source: https://docs.embeddables.com/how-to/send-data-to-klaviyo Setting up an Action with custom code that sends user data to Klaviyo's API The best way to data to Klaviyo is using custom code in an Action, that is Triggered when a user takes an action, such as entering their email or completing a purchase. <Steps> <Step title="Create a custom code Action"> * Go to the Logic sidebar and click on the Actions tab. * Click `+ Add New Action`. * Give it a name like "Send Data to Klaviyo". * Hit `Add`. </Step> <Step title="Write your custom code"> * Add code similar to the example below, to construct the data object and send it to Klaviyo's API. ```js theme={null} // All Actions should start with a function called output() async function output({ first_name, last_name, email }) { // @TODO: Replace with your actual company name, // and provide the API key to Embeddables to store securely on the backend const API_KEY_IDENTIFIER = "{{YOUR_COMPANY_NAME---klaviyo---api_key}}"; // @TODO: Update to match your User Data keys, and add any additional data you want to send const importProfileBody = { data: { type: "profile", attributes: { email: email || "", first_name: first_name || "", last_name: last_name || "", }, }, }; // Constructs the fetch options object const importProfileRequestOptions = { method: "POST", headers: { accept: "application/vnd.api+json", revision: "2024-10-15", "content-type": "application/vnd.api+json", Authorization: API_KEY_IDENTIFIER }, body: JSON.stringify(importProfileBody), }; // Sets the URL to the Klaviyo API endpoint const url = "https://a.klaviyo.com/api/profile-import/"; // Sends POST request, using the Embeddables backend proxy to keep the API key secure const response = await fetch(`https://proxy-secure.trysavvy.com/?url=${encodeURIComponent(url)}`, importProfileRequestOptions) .then(response => response.json()) .catch(err => { console.error("Error importing profile", err); throw err; }); // If the response doesn't contain an id, throw an error if (!response?.data?.id) { console.error("Failed to import profile - no id returned"); throw new Error("Failed to import profile"); } // If the response is successful, add the user to a list const id = response.data.id; const listId = "listID"; const addToListBody = { data: [ { type: "profile", id: id, }, ], }; // Constructs the fetch options object const addToListRequestOptions = { method: "POST", headers: { accept: "application/vnd.api+json", revision: "2024-10-15", "content-type": "application/vnd.api+json", Authorization: API_KEY_IDENTIFIER, }, body: JSON.stringify(addToListBody), }; // Constructs the URL to the Klaviyo API endpoint const url = `https://a.klaviyo.com/api/lists/${listId}/relationships/profiles`; // Sends POST request, using the Embeddables backend proxy to keep the API key secure fetch( `https://proxy-secure.trysavvy.com/?url=${encodeURIComponent(url)}`, addToListRequestOptions ) .then(response => { console.log("Response adding to list", response); }) .catch(err => { console.error("Error adding to list", err); throw err; }); } ``` </Step> <Step title="Add a Trigger for the relevant user event"> * Switch to the Triggers tab. * Add a new Trigger. * Choose the event that you want to trigger the Action on. * For example: `WHEN Page KEY email_page IS Completed`. * Select the `Send Data to Klaviyo` Action. * Hit `Add`. </Step> </Steps> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> # Embeddables and Telegra MD Source: https://docs.embeddables.com/how-to/send-data-to-telegra-md Setting up an Action with custom code that sends user data to Telegra MD's API The best way to data to Telegra MD is using custom code in an Action, that is Triggered when a user takes an action, such as entering their email or completing a purchase. <Steps> <Step title="Create a custom code Action"> * Go to the Logic sidebar and click on the Actions tab. * Click `+ Add New Action`. * Give it a name like "Send Data to Telegra MD". * Hit `Add`. </Step> <Step title="Write your custom code"> * Add code similar to the example below, to construct the data object and send it to Telegra MD's API. ```js theme={null} // All Actions should start with a function called output() function output(userData, {setUserData}) { // If the user data has already been sent, and a portal URL has been received, don't resend it if (userData.telegra_md_portal_url) return // @TODO: Replace with your actual company name, // and provide the API key to Embeddables to store securely on the backend const API_KEY_IDENTIFIER = "{{YOUR_COMPANY_NAME---telegra---api_key}}"; const INTEGRATION_CONFIGURATION_ID = "<INTEGRATION_CONFIGURATION_ID>"; const TELEGRA_MD_HOST = "https://telegramd-rest.telegramd.com"; // @TODO: Update to match your User Data keys, and add any additional data you want to send const requestBody = { email: userData.email, firstName: userData.first_name, lastName: userData.last_name, name: userData.first_name + ' ' + userData.last_name, gender: userData.gender, phone: userData.phone, requested_medications: [ { productVariation: userData.selected_product_data.product_variation, quantity: 1 } ], payment_configuration: { system: "stripe", system_customer_identifier: userData.customer_id, system_payment_intent_identifier: userData.payment_intent_id }, externalIdentifier: userData.payment_intent_id, }; console.log('[Submit to Telegra] - requestBody:', requestBody) const url = `${TELEGRA_MD_HOST}/integrations/embeddables/${INTEGRATION_CONFIGURATION_ID}/visit/initiate?access_token=${API_KEY_IDENTIFIER}`; // Sends POST request, using the Embeddables backend proxy to keep the access token secure fetch(`https://proxy-secure.trysavvy.com/?url=${encodeURIComponent(url)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(requestBody) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { console.log("[Submit to Telegra] - Response:", data); // Adds the portal URL to the user data, so it can be used in future Actions setUserData({ telegra_md_portal_url: data.visitUrl }) }) .catch(error => { console.error("[Submit to Telegra] - Error:", error); }); } ``` </Step> <Step title="Add a Trigger for the relevant user event"> * Switch to the Triggers tab. * Add a new Trigger. * Choose the event that you want to trigger the Action on. * For example: `WHEN Page KEY email_page IS Completed`. * Select the `Send Data to Telegra MD` Action. * Hit `Add`. </Step> </Steps> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> # Embeddables and Tellescope Source: https://docs.embeddables.com/how-to/send-data-to-tellescope Setting up an Action with custom code that sends user data to Tellescope's API The best way to data to Tellescope is using custom code in an Action, that is Triggered when a user takes an action, such as entering their email or completing a purchase. <Steps> <Step title="Create a custom code Action"> * Go to the Logic sidebar and click on the Actions tab. * Click `+ Add New Action`. * Give it a name like "Send Data to Tellescope". * Hit `Add`. </Step> <Step title="Write your custom code"> * Add code similar to the example below, to construct the data object and send it to Tellescope's API. ```js theme={null} function output(userData, context) { const tellescopeData = { answers: [ { key: "first_name", value: userData.first_name, }, { key: "last_name", value: userData.last_name, }, { key: "phone", value: userData.phone, }, { key: "email", value: userData.email, }, // Add any other user data properties that you want to send to Tellescope here ], variant_label: "main", variant_uuid: "<VARIANT_UUID>", finalized: true, created_at: generateFormattedDate(), experiment_uuid: null, flow_label: "<FLOW_LABEL>", }; // @TODO: Replace with your actual company name, // and provide the API key to Embeddables to store securely on the backend const API_KEY_IDENTIFIER = "{{YOUR_COMPANY_NAME---tellescope---api_key}}"; const formId = "<FORM_ID>"; const endpoint = `https://api.tellescope.com/v1/webhooks/formsort/${API_KEY_IDENTIFIER}?formId=${formId}`; // Send POST request, using the Embeddables backend proxy to avoid CORS issues fetch( `https://proxy-secure.trysavvy.com/?url=${encodeURIComponent(endpoint)}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(tellescopeData), } ); } function generateFormattedDate() { const date = new Date(); const isoString = date.toISOString(); return isoString.replace("Z", "+00:00"); } ``` </Step> <Step title="Add a Trigger for the relevant user event"> * Switch to the Triggers tab. * Add a new Trigger. * Choose the event that you want to trigger the Action on. * For example: `WHEN Page KEY email_page IS Completed`. * Select the `Send Data to Tellescope` Action. * Hit `Add`. </Step> </Steps> <Card title="Learn more about Custom Code" icon="code" href="/guides/custom-code"> Read more about writing Custom Code in Embeddables, including all the available arguments passed in to the function. </Card> # Embeddables and Hotjar Source: https://docs.embeddables.com/how-to/set-up-hotjar Track user behavior across Embeddable pages using Hotjar heatmaps, recordings, and analytics [Hotjar](https://www.hotjar.com/) is a popular tool for heatmaps, session recordings, and user behavior analytics. Because Embeddables handle page navigation internally (without full browser page loads), **Hotjar needs a small configuration tweak to correctly detect page changes inside your Embeddable**. The solution relies on syncing Embeddable page changes to the URL as fragments (e.g. `#page_key`), and telling Hotjar to watch for those fragment changes. <Steps> <Step title="Turn on Sync URL with Pages in Embeddables"> In the Embeddables editor, open the **Embeddable-Wide Options** sidebar (press Escape to deselect any pages or components, then open the Options panel on the right). Enable the **Sync Pages With The URL** setting. This updates the browser URL with a `#page_key` fragment whenever the user navigates to a new page inside the Embeddable (e.g. `yoursite.com/get-started#intro_page`). </Step> <Step title="Enable fragment tracking in Hotjar"> In your Hotjar dashboard, go to your site's tracking settings and enable **"Track changes automatically, including fragments"**. This tells Hotjar to treat changes to the `#hash` portion of the URL as page navigations, so each Embeddable page is tracked as a separate page view. </Step> <Step title="Verify the integration"> Open your Embeddable in a browser and navigate through a few pages. In Hotjar, confirm that each page change appears as a distinct page view with the corresponding `#page_key` in the URL. </Step> </Steps> <Info> Once this is set up, Hotjar heatmaps, recordings, and funnels will reflect individual Embeddable pages rather than treating the entire flow as a single page view. </Info> <Card title="Learn more about Sync Pages With The URL" icon="link" href="/how-to/sync-pages-with-url"> Read more about how URL syncing works, including back/forward button behavior and edge cases. </Card> # How To: Set up a 'none of the above' option Source: https://docs.embeddables.com/how-to/set-up-none-of-the-above Add an option button that deselects all other options Many multi-select questions require a `None of the above` button. This button should behave differently from the other options - it should not only be selected when clicked, but should also deselect all other options. Fortunately, there's an easy way to add this functionality in Embeddables. <Warning> This feature is not yet implemented in the Builder UI. For now, you will need to add `single_select: true` to the button in the component JSON. </Warning> <Steps> <Step title="Select the component and open the button settings"> * Select the Option Selector component. * Navigate to the Options sidebar. * Click the <Icon icon="pencil" /> Edit icon button next to the "None of the above" button. </Step> <Step title="Set the button to be a 'single select' option"> * Check the option called "Single select". * This makes the button single-select only, meaning that once it's selected, all of the other buttons are deselected. </Step> </Steps> # How To: Set up Webhooks in Embeddables Source: https://docs.embeddables.com/how-to/set-up-webhooks What the Webhooks feature is for, and how to use it ## What the Webhooks feature is for Sometimes instead of using one of our built-in integrations, you will need to send [User Data](#user-data) out to an external application that we don't yet support, or perhaps a custom application of your own (e.g. your own API or database). Webhooks are perfect for that scenario, and Embeddables has a dedicated Webhooks feature that you can use. <Info> **What are Webhooks?** Webhooks are a technology that are similar to APIs except they work in the reverse direction! So while the provider of an API ***listens*** for requests from other applications (and responds accordingly), the provider of a webhook ***sends*** events to any applications that tell it they're listening for events. In other words, with APIs each event is triggered by the external application, whereas with webhooks each event is triggered by the webhook provider themselves. </Info> <Note> Currently, you can add multiple destination URLs, but they will all receive all events. You cannot subscribe to specific events. </Note> ## How to use Webhooks in Embeddables Webhooks in Embeddables are powered by [Svix](https://svix.com/), an excellent service that integrates nicely into our systems and handles all the detailed features that we would ever want to build. <Steps> <Step title="Create a Webhook in the Embeddables Builder"> * In the Builder, go to the Logic sidebar and click on the Actions tab. * Click `+ Add New Action`. * Give it a name, and select `Webhook` as the Action type. * Hit `Add`. </Step> <Step title="Trigger the Webhook from an event"> * Go to the Triggers tab. * Add a new Trigger. * Choose the event that you want to trigger the Action on. * Select the Webhook Action you just created. * Hit `Add`. </Step> <Step title="Add your destination URL to the list of endpoints in Embeddables"> * In the [Embeddables Web App](https://app.embeddables.com), go to Settings > Integrations. * In the Webhooks card, click `Manage Webhooks`. * Your Svix portal will open in a new tab. * On the Endpoints page, click `+ Add Endpoint`. * Add your URL into the `Endpoint URL` field. * Leave the `Subscribe to events` field blank to receive all events. * Hit `Add`. </Step> </Steps> ## What events can I subscribe to? Using the above method, you can trigger Webhook events at any point during the user's journey. **The main limitation is that you cannot currently define and subscribe to specific events.** All Webhook events triggered will be sent to all destination endpoints URLs. To handle different event types with different destinations, we recommend using a service like Zapier, or your own backend, to route the events to the appropriate destination based on a User Data property such as `current_page_key`. # How To: Sync page changes with the browser URL Source: https://docs.embeddables.com/how-to/sync-pages-with-url Updating the URL on page changes so that the browser back buttons control the Embeddable ## What's the issue? Because Embeddables are, well, embedded in the webpage, they have a slightly strange relationship with the browser: namely, that pages *inside* an Embeddable are distinct from webpages in the browser. The main consequence of this that people tend to notice is that pressing the browser's Back button doesn't go back a page in the Embeddable - in fact, it goes back to the previous webpage (normally a page without an Embeddable on at all). This is expected behavior, but in some situations can confuse users who were actually trying to just go back one step in the flow, and find that they've gone back to before the beginning. ## The solution: syncing Embeddable pages with the URL The solution to this is our **Sync Pages With The URL** feature. <Steps> <Step title="Turn on the Sync Pages With The URL setting"> You can find this in the Embeddable-Wide Options. To find these options, make sure to press Escape to deselect and pages or components, and then open the Options sidebar on the right-hand side. </Step> <Step title="Test for edge-cases and enjoy."> This feature can have some interesting edge-cases, especially in Embeddables that already have custom logic to do with navigation (such as blocking users from going back after auth or payments). Tread carefully! </Step> </Steps> ## How does it work? When turned on, this works by automatically updating the URL with an <Tooltip> anchor link</Tooltip> that corresponds to the current page key of the Embeddable. So, if a user heads to `mywebsite.com/get-started` then that will immediately become `mywebsite.com/get-started#intro_page`, and when the user clicks next inside the Embeddables their URL bar will then show `mywebsite.com/get-started#your_name_page`, etc etc. Why is this helpful? Well, it means that if the user presses the browser's Back button, it will just return to `mywebsite.com/get-started#intro_page` - a different anchor link but still on the same webpage. Embeddables then listens for changes to the URL and updates the Embeddable page accordingly, so the user will feel no difference between that and using the Back button inside the Embeddable itself. The browser's Forward button works in the same way, meaning that users should have a smooth experience pretty much whatever they choose to do. <Note> There are **certain edge-cases** to be aware of with this feature - details to be added to this page soon! </Note> <Info> Another use-case (to be explained further on this page soon) for this feature is to make it easier to track analytics, since tools like Google Analytics tend to prefer to track user progress via changes to the URL. </Info> # Use Stripe Coupons and Promo Codes Source: https://docs.embeddables.com/how-to/use-stripe-coupons-and-promo-codes How to implement and apply Stripe coupons and promo codes in your Embeddable Stripe offers two ways to provide discounts to your customers: Coupons and Promo Codes. While they're related, they serve different purposes and are implemented differently. ## Understanding Coupons vs Promo Codes ### Coupons * Coupons are the underlying discount rules you create in Stripe. * They define the actual discount (e.g., 20% off, \$10 off, etc). * They are not customer-facing and are identified by IDs in Stripe, that can either look like `A1b2C3d4` or an ID chosen by you. * You can create coupons in your [Stripe Dashboard](https://dashboard.stripe.com/coupons). ### Promo Codes * Promo codes are customer-facing codes that are linked to coupons. * They are what customers actually enter to get a discount. * They have a customer-friendly format (e.g., "BLACKFRIDAY50"), and they also have a unique ID generated by Stripe that looks like `promo_1A2B3C4D5E6F7G8H9I0J`. * You can create promo codes [in your Stripe Dashboard from within a Coupon](https://dashboard.stripe.com/promotion_codes). <Note> Both coupons and promo codes are environment-specific - they exist either in test mode or live mode in Stripe, but not both. Make sure you're using the correct mode for your Embeddable. If you want to test coupons or promo codes in both modes, you'll need to create equivalent coupons and promo codes in both modes. </Note> ## Implementing Promo Codes in Your Embeddable There are two ways to implement promo codes in your Embeddable: ### 1. Manual Entry by Customer To let customers enter their own promo codes: <Steps> <Step title="Add Input and Button Components"> * Add a Container component to house your promo code components. * Add a 2nd Container component to display the input and button components side-by-side. * Add an Input Box component for use to enter the promo code, with a key such as `new_promo_code`. * Add a Button component for the user to click to submit the code. * Add 2 Plain Text components to display success + error messages, giving them the content of `{{promo_code_success_message}}` and `{{promo_code_error_message}}` respectively. </Step> <Step title="Style the components"> * Style the inner Container to use Flex to display the input and button components side-by-side, and consider giving it a max-width. * Style the input box to be a varying width, based on the width of the container. * Style the success message to be green, and the error message to be red. </Step> <Step title="Create an Action"> Create an Action that will be triggered when the button is clicked: ```javascript theme={null} function output(userData, { setUserData }) { // Get the promo code from the input, using its key const promoCode = userData.new_promo_code; // Apply the promo code using the Stripe Checkout function window.StripeCheckout.applyPromotionCode(promoCode) .then((result) => { if (result.type === 'success') { // On success: // - Update the success message to show the applied discount // - Clear any error message // - Reset the input box to empty setUserData({ promo_code_success_message: `Promo code "${promoCode}" applied successfully!`, promo_code_error_message: "", new_promo_code: "", }); } else { // Handle error case setUserData({ promo_code_success_message: "", promo_code_error_message: result.error.message || "Invalid promo code", }); } }) .catch((error) => { // Handle any unexpected errors setUserData({ promo_code_success_message: "", promo_code_error_message: "An unexpected error occurred", }); }); } ``` </Step> <Step title="Register User Data Keys"> * In your Embeddable settings, add the following keys to the "Registered Keys" section: * `promo_code_success_message` * `promo_code_error_message` * This ensures these keys are properly tracked and managed by the Embeddables reactive engine. </Step> <Step title="Add a Trigger to the Action"> * Add a Trigger to the Action that fires when the button is clicked. </Step> <Step title="Recommended: Reset Messages on Page Load"> Since the Stripe checkout session is refreshed when the user refreshes or changes pages, you'll want to reset the success and error messages when the page loads, to avoid the user thinking that their promo code is still applied. * Create a new Action to reset the success and error messages when the page loads. ```javascript theme={null} function output(userData, { setUserData }) { setUserData({ promo_code_success_message: "", promo_code_error_message: "", }); } ``` * Add a Trigger to this Action that fires on page load. * This ensures the messages are cleared when the Stripe checkout session is refreshed. </Step> </Steps> ### 2. Automatic Application Use this method to automatically apply a promo code for all customers. <Note> A UI for handling this without editing the JSON is coming soon! </Note> In your Stripe component Options, open the JSON editor and add the following to the checkout session configuration: ```json theme={null} { "checkout_session": { // ... other checkout session configuration ... "discounts": [ // Stripe currently only supports one object in the array { "promotion_code": "YOUR_PROMO_CODE_ID", // EITHER THIS "coupon": "YOUR_COUPON_ID" // OR THIS } ] } } ``` Tips: * Replace `YOUR_PROMO_CODE_ID` or `YOUR_COUPON_ID` with your actual promo code ID from Stripe. * Or, if you prefer, replace them with `{{promo_code_id}}` or `{{coupon_id}}` to use an ID calculated from a Computed Field or Action. * Make sure to remove either the `promotion_code` or `coupon` key, not both. * Make sure to remove all the comments (`// ...`) since JSON doesn't support comments. * Make sure to remove the `// ... other checkout session configuration ...` line, since you're only adding the discounts section. <Warning> When using automatic application, make sure you're using the correct promo code for your environment (test vs live mode). </Warning> ## Troubleshooting <AccordionGroup> <Accordion title="Error: Invalid promo code"> This usually means one of the following: * The promo code doesn't exist in your Stripe account. * You're using a test mode promo code in live mode (or vice versa). * The promo code has expired or been deactivated. * The promo code doesn't apply to the current products in the cart. * You're using the wrong type of value, out of the 3 options (promo code ID, coupon ID, or customer-facing promo code) - [read about the differences above](#understanding-coupons-vs-promo-codes). </Accordion> <Accordion title="Error: Promo code already applied"> This means the customer has already applied a promo code to their checkout session. Only one promo code can be applied at a time. </Accordion> <Accordion title="Error: Coupon ID instead of Promo Code"> If you're getting errors when trying to apply a discount, make sure you're using a promo code (customer-facing code) and not a coupon ID. The `applyPromotionCode` function expects a promo code, not a coupon ID. </Accordion> </AccordionGroup> ## Using coupons with multiple Stripe Checkout components on the same page If you have more than one Stripe Checkout component on the same page, set `use_shared_stripe_session: true` on each component. Without it, each component creates its own independent Stripe Checkout session — meaning a coupon applied to one component will not be reflected on the others, and the "Pay" button totals will get out of sync. When all components on the page share a session, applying a promo code via `window.StripeCheckout.applyPromotionCode(...)` updates the session once and every component's "Pay" button automatically reflects the discounted total. <Warning> All participating Stripe Checkout components must use the same **Checkout mode** (e.g. all Payment, or all Subscription) and the same **currency**. Mismatched values will produce an error. </Warning> <Note> Session sharing is scoped to a single page. Components on different pages each create their own independent session, so `use_shared_stripe_session` has no effect across page navigations. </Note> See the [Sharing a Stripe session across multiple components](/features/payments/stripe/overview#share-a-single-stripe-session-across-multiple-stripe-checkout-components) section in the Stripe Payments docs for setup instructions. ## Best Practices 1. **Test Both Modes**: Always test your promo codes in both test and live modes to ensure they work as expected. 2. **Clear Messaging**: Make sure your UI clearly communicates: * Where to enter the promo code. * When a code has been successfully applied. * When a code is invalid and why. * That only one promo code can be applied at a time. 3. **Error Handling**: Implement proper error handling in your Action to provide a good user experience when: * The code is invalid. * The code has expired. * The code doesn't apply to the current products. * The code has already been applied. 4. **Security**: Remember that promo codes are public-facing. Don't include sensitive information in them, and consider implementing rate limiting if you're concerned about brute force attempts. 5. **Data Management**: * Register all user data keys in your Embeddable settings to ensure proper tracking and management. * Reset success/error messages on page load to handle Stripe checkout session refreshes. * Keep your user data structure clean by clearing input fields after successful code application. # Changelog Source: https://docs.embeddables.com/reference/changelog/changelog What we've shipped, when we shipped it <Update label="23 June 2026"> ### 🔥 Features and Updates * 💳 **Stripe Checkout Mounted & Unmounted Triggers** – Stripe Checkout components now expose **mounted** and **unmounted** lifecycle events in the Builder's Logic tab, alongside the existing payment events. Add a trigger to a Stripe Checkout component to run an Action when the component is mounted (loaded onto the page) or unmounted (removed from the page). The new triggers appear with readable labels in the trigger cards and layer tooltips. ### 🐞 Bug Fixes * **Experiment Date/Time Filter Crash** – Clearing a time segment with Backspace or Delete in the date/time range filter on the experiment page no longer crashes the page. The time-change handlers now ignore the transient `null` value React Aria reports while a segment is mid-edit, instead of attempting to read `.hour` / `.minute` from it. </Update> <Update label="21 May 2026"> ### ⚡️ Improvements * 🌍 **Project Region API for Internal Services** – A new `POST /api/projects/region` endpoint lets trusted server-side services (such as Trigger.dev jobs and sub-agents) look up a project's configured timezone region and its offset in minutes without needing a user session. Authenticated via the `x-api-key` header against the shared internal API key, it returns `{ region, offsetInMinutes }` — falling back to `America/Los_Angeles` when no region is set — so background jobs can perform date/time work in the project's timezone. </Update> <Update label="20 May 2026"> ### 🔥 Features and Updates * 🌐 **Browser Filter on Users List** – The Users page now has a **Browser** filter to narrow down contacts by browser (Chrome, Firefox, Safari, etc.). The filter is multi-select, syncs with the URL so filtered views are shareable, and uses the same UAParser-based browser names as the Device column. </Update> <Update label="19 May 2026"> ### ⚡️ Improvements * 🔐 **Internal Credential Lookup RPC** – The Custom Credentials worker now exposes a `getInternalCredential` RPC so other Embeddables Workers can fetch internal credentials by project, key type, and environment. Live credentials are matched and the most recently created one is returned; when no live credential matches the call returns `null`, letting callers cleanly distinguish "not configured" from hard errors. ### 🐞 Bug Fixes * Copy/pasting nested global components in the Builder no longer flattens their hierarchy — child components inside copied containers now keep their parent/child structure instead of becoming siblings. * Merge replay during concurrent Builder edits now anchors new and moved components only to same-bucket peers (same parent, same `_location` slot, or same root group), preventing accidental hierarchy or slot corruption when other people are editing the same embeddable. </Update> <Update label="18 May 2026"> ### ⚡️ Improvements * ♿ **Accessibility: Focus Moves to Top on Page Change** – When a flow advances to a new page and scrolls to the top, focus is now programmatically moved to the form at the top of the page so screen readers and keyboard users land in the right place. Focus only moves when the embed itself scrolls (not when the document scrolls), and the programmatic focus ring is suppressed so sighted users don't see an unexpected outline on every page change. ### 🐞 Bug Fixes * The QA tests table no longer shows a stale failed run as the **Latest Result** after a successful re-run — embedded QA runs are now ordered and limited at the database level so the most recent run is always picked. </Update> <Update label="13 May 2026"> ### 🐞 Bug Fixes * **Experiment Results Contact Matching** – The experiment results query now includes `contact_id` in its joins and groups by both `entry_id` and `contact_id`, fixing inconsistent or duplicated rows that could appear when the same contact had multiple entries. </Update> <Update label="07 May 2026"> ### 🔥 Features and Updates * 🛑 **Cancel QA Runs** – Running QA runs can now be cancelled directly from the runs list. Open the run's actions menu and choose **Cancel run** to stop the worker; the run is then marked as **Cancelled**, which appears as a badge on the run and as a new option in the QA runs status filter dropdown. * 📊 **Progress Bar Styling Selectors** – The Progress Bar component now exposes two additional styling elements in the Design sidebar — **Progress text** and **Reset button** — so you can style the progress label and reset control independently from the bar. ### 🐞 Bug Fixes * Reordering conversion events no longer drops the payload metric keys attached to each conversion. </Update> <Update label="04 May 2026"> ### 🔥 Features and Updates * 📊 **Payload Metric Key for Experiments** – Experiments can now track a custom payload metric in addition to conversions. In the Edit Experiment modal, a new **Event metric key** dropdown appears once you have selected impact metrics whose conversions have payload metric keys configured. Select one key to enable per-variant metric aggregation in experiment results. ### 🐞 Bug Fixes * **Team Invite Modal Auto-Close on Pending Invite** – Re-inviting a user who already has a pending invitation now automatically dismisses the invite modal after the error toast appears, instead of leaving the dialog open in an ambiguous state. * **QA Scheduler Cron Timing** – The QA scheduler now uses the Cloudflare-provided `scheduledTime` for cron matching instead of reading the current time after async work, preventing scheduled QA tests from being skipped when the handler runs slightly after the scheduled minute. * **QA Multiselect OptionSelector** – QA tests driving OptionSelector components now correctly handle multiselect personas: when a persona's `userdata` key is an array of values, each value is clicked in turn so all intended options are selected. </Update> <Update label="30 April 2026"> ### 🔥 Features and Updates * 🔐 **Credential Templating in Proxy Request Bodies** – The Secure Proxy now supports substituting `{{ credential_id }}` placeholders in request bodies. Send `X-Use-Body-Templating: true` on any POST, PUT, PATCH, or DELETE request and the proxy will replace credential placeholders in the body before forwarding, using the same allowlisted credentials as URL and header substitution. The header is stripped from the outbound request. Omit the header (or leave it unset) to keep the default streaming pass-through, which avoids buffering and is recommended for large or binary payloads. </Update> <Update label="23 April 2026"> ### 🔥 Features and Updates * 💳 **Disable Pay Button Until Page Is Valid** – A new `disable_button_until_page_valid` property on the Stripe Checkout 2 component reactively disables the Pay button whenever required page fields are incomplete, giving users an immediate visual cue rather than waiting until they click Pay. ### ⚡️ Improvements * **Faster Builder Page List** – Page thumbnails in the Builder now lazy-load their images, significantly speeding up the virtualized page list for embeddables with many pages. * **Faster Submissions Downloads** – Large submission exports now use a chunk size of 10,000 records (reduced from 25,000), resulting in faster and more reliable CSV downloads for projects with high submission volumes. * **`goToNextPage`, `goToPrevPage`, and `triggerAction` on `window.Embeddables`** – `triggerAction` is now available on `window.Embeddables` in addition to `window.Savvy`, so you can fire any named action in your Embeddable directly from the host page using either alias. </Update> <Update label="16 April 2026"> ### 🔥 Features and Updates * 🌐 **Stripe Checkout Translation Support** – Pay button text and success message in Stripe Checkout 2 components now respond to language changes when translation variants are configured, keeping your payment UI fully localised. * 📡 **`trackCustomEvent` on `window.Embeddables`** – `trackCustomEvent` is now exposed on `window.Embeddables`. ### ⚡️ Improvements * **`trackCustomEvent` returns a Promise** – `window.Embeddables.trackCustomEvent()` now returns a `Promise<void>`, so you can `await` the call before redirecting or performing follow-up actions. * **Experiment Overlap Warning** – The Experiments page now shows a warning when the same experiment ID appears in both the Regular and Legacy lists, so you can catch and clean up incomplete migrations before they cause hidden or duplicated experiments. </Update> <Update label="14 April 2026"> ### 🔥 Features and Updates * 🌐 **New Generation Embeddables** – Goals, Tasks, QA, Inbox, and more are now available to everyone. The "+" Create dropdown also now includes New Task, New Goal, and New QA Test for everyone — no special access required. * ⚡ **Optimized Submissions Engine (Beta)** – A new toggle in **Project Settings → General** lets admins and Data Privacy Officers enable a faster submissions query engine and parallelized CSV downloads for this project. When enabled, the Submissions page shows a "Using optimized queries (beta)" badge. Great for projects with large volumes of submission data. ### 🐞 Bug Fixes * An error toast is now shown when attempting to download submissions with no answer keys, rather than silently failing. </Update> <Update label="09 April 2026"> ### 🔥 Features and Updates * 💰 **Currency Unit for Goals** – Numeric Goals with Sum or Average aggregation now have a **Unit** selector. Choose \*\*USD $** to display values as currency (e.g. $1,234.56) instead of a plain number. * 🔍 **AND Response Filters in Overview** – The response filter in the Embeddable Overview now supports stacking multiple key/value pairs combined with AND logic. Click **+ Add filter** to narrow down the responses you're analyzing to users who match all selected criteria. * 🔗 **Domain Mode for Credentials & Endpoints** – Allowed Origins and Allowed Destinations in Endpoint settings now offer a simple **Domain** tab so you can enter a root domain (e.g. `example.com`) instead of writing a regex. All subdomains and paths on that domain are matched automatically. Switch to **Regex** mode for advanced matching patterns. * 📦 **Deprecated Integrations Toggle** – Google Sheets and other deprecated integrations are now tucked behind a collapsible "Show deprecated integrations" section on the Integrations settings page, keeping the main list focused on actively supported integrations. ### ⚡️ Improvements * Analytics date pickers now highlight the time input and show a **Reset** button when the time has been changed from the default, making it easy to spot and clear any custom time that is active. * Goal metric values now display with two decimal places for greater precision (e.g. **1,234.56** rather than **1,235**). * Conversion rate percentages in the Embeddable Overview now display two decimal places (e.g. **12.34%** instead of **12%**). ### 🐞 Bug Fixes * Funnel chart embeddable thumbnails no longer flicker or reload on every mouse move when hovering. * Editing a conversion-rate Goal no longer resets the selected metric and other form fields when the edit modal opens. * The Save button for experiment variants is now correctly disabled until the group data has finished loading, preventing premature or failed saves. </Update> <Update label="07 April 2026"> ### 🔥 Features and Updates * 💳 **Shared Stripe Checkout Session** – Multiple Stripe Checkout components on the same page can now share a single Stripe Checkout session. Enable `use_shared_stripe_session` on each component and they will all use the same underlying session, ensuring coupons and discounts apply uniformly across all line items and that all "Pay" buttons update in sync when a promo code is applied. </Update> <Update label="26 March 2026"> ### 🔥 Features and Updates * 🖼️ **Page Thumbnail Previews** – Hover over any page key in funnel charts, the user activity timeline, or the goal creation modal to see a live preview thumbnail of that page. ### 🐞 Bug Fixes * Switching Stripe Checkout payment methods rapidly no longer causes duplicate entries. * The metric dropdown in the goal creation modal now includes a search bar, making all metrics discoverable (previously capped at 50 visible options). </Update> <Update label="01 July 2025"> ### 🔥 Features and Updates * 📁 **FileUploader v2: Multi-File & Validation** – The FileUploader component now supports uploading multiple files, setting a maximum file limit, and displaying custom validation error messages. Upgrade to version 2 for these features. * 🧪 **Experiment Results Conversion Picker** – The experiments page now features a dropdown to select which conversion is used for the results table when several are available. * 🗂️ **OptionSelector Missing Key Warnings** – The layers tab now warns you if any buttons inside an OptionSelector are missing a key, helping prevent configuration issues. * 💸 **Stripe Checkout Discounts** – You can now apply a promo code or coupon to a Stripe Checkout component automatically on load. ### ⚡️ Improvements * AI chat now shows detailed error messages, making it easier to understand and resolve issues. * Switching personas in the Builder now resets userdata, ensuring a clean slate instead of combining data from different personas. ### 🐞 Bug Fixes * Downloading 10,000 submissions at a time now works as expected. * AI now uses HTML tags in the rich text component, ensuring correct formatting instead of markdown. * Plain Text components now set the correct `parent_id` property when added to a container. * Searching for style tags by number (e.g., typing `500` for `font_weight_500`) now returns the correct results. * You can no longer delete the last remaining page from layers in an Embeddable. * Experiments now filter out time periods when they were paused from the displayed results. </Update> <Update label="17 June 2025"> ### 🔥 Features and Updates * 🛡️ **Builder Safe Mode** – Launch the Builder in safe mode using `?embeddables_safe_mode=true` to prevent actions and computed fields from running. Great for troubleshooting, or reverting some accidental code that managed to break the Builder! * 🖼️ **Dynamic Button Image URLs** – The Image URL field for buttons now supports computed field values like `{{example_value}}` for dynamic image switching. ### ⚡️ Improvements * Open Builder from Experiments – Added a quick-access button to open the Builder directly from the experiments page. * Experiment Variant Preview Links – You can now preview variants of draft experiments with a dedicated link. * CMS Setup UI Improvements – Enhanced the Builder's CMS configuration with a clearer interface and new options, making content management more intuitive. </Update> <Update label="03 June 2025"> ### 🔥 Features and Updates * 👥 **Persona Preview Dropdown** – Added a "Personas" dropdown in Preview mode so you can add, edit or switch between personas more easily. * 📎 **File Type Options Expanded** – Added "All types" and "Images only" options to the File Upload component's existing type selection. More flexibility for your file uploads! ### ⚡️ Improvements * Experiment View Calculation Fix – Improved percentage view to properly calculate all bars relative to the tallest bar of each type, ensuring accurate visual comparisons. * Cleaner Breadcrumb Navigation – Current page is now hidden from breadcrumbs when selecting global components, making navigation clearer and more intuitive. ### 🐞 Bug Fixes * Missing Experiments Display – New experiments now properly appear in the Embeddable overview's experiments tab. * Stripe Checkout Phone Collection – Phone numbers are now correctly collected during Stripe Checkout process. * Better Breadcrumb Text Wrapping – Long names no longer cause awkward wrapping in breadcrumb navigation. * Firefox Breadcrumb Visibility – Breadcrumbs no longer get buried at the bottom of the page in Firefox. -️ File Upload Type Warning – Adding a file upload component that accepts all file types no longer triggers incorrect type warnings. </Update> <Update label="19 May 2025"> ### 🔥 Features and Updates * ☁️ **Flexible Field Storage** – Choose where your users' input is saved: in the cloud, locally, or not at all! No more worrying about lost data or privacy — just pick your preference right from the sidebar. * 📈 **Funnel Y-Axis Toggle** – Instantly switch between absolute values and percentages on your funnel charts. See your data the way you want! * 🧑‍🎨 **Persona Management** – Update or remove existing personas directly in the Builder, including renaming them. No more deleting and recreating just to make a change! ### ⚡️ Improvements * Published URL Editing – Change your Embeddable’s published URL anytime from the General Information settings (after publishing). * Helpful HIPAA Tooltips – Added tooltips for "Health Data", "Personally Identifiable Information", and "Contact Info" in the Data Schema table. No more guessing! * Auto-Prefilled Conversion Values – Creating a conversion now uses a dropdown of available options instead of a text box. No more typos or broken analytics! * Improved Page Selection Clarity – It's now easier to tell when a page is selected in the Builder's page tab, helping you quickly identify what you're working on. ### 🐞 Bug Fixes * Experiment Variant Switching Fixed – You can now switch between experiment variants as many times as you like. * Region Dropdown Search – Added a search bar to the region selection dropdown so you can quickly find what you need. </Update> <Update label="14 May 2025"> ### 🔥 Features and Updates * 🚀 **Engine v4**: * 🧼 Removed all hidden internal styles – Fully redesigned style generation with clean component templates. * 🖥️ Smart breakpoint ordering - Styles will automatically apply in order of screen width, so tablet styles will never get overwritten by mobile styles. * 🔗 Simplified container referencing – Components now rely solely on parent id, removing parent key usage to prevent duplicate rendering in Engine v4. * 📊 **Funnel Drop-off Percentages** – Funnel charts now display both absolute and percentage amounts. * 🧩 **Re-order Conversion Events** – Drag-and-drop conversion events directly in the Overview page or via Settings. No more deleting and re-adding just to rearrange! * 👥 **Remove Users from Projects** – Admins can now remove members from projects directly in the Users table, with built-in safeguards. * 🧠 **Real-time Schema Enforcement** – The Builder now displays warnings, hints and automatic fixes in places like the Layers sidebar, whenever there are issues with the schema the need to be fixed. * 📦 **DropdownMain Container Selector Added** – Properly includes the DropdownMain container in advanced dropdown selectors for consistent styling and behavior. </Update> <Update label="9 May 2025"> ### 🔥 Features and Updates * 📋 **Trigger List for Actions** – View all available triggers per action with a single click. Instantly auto-fill the search bar and jump to the Triggers tab for faster configuration. * 🌐 **CMS Options in the Builder** – Easily configure and connect content directly from your CMS for more dynamic page building. * 🎭 **Enter & Exit Transitions UI** – Define entry and exit animations using a new visual interface for smoother experiences. * 💳 **Stripe Metadata Controls in Options** – Add or edit metadata fields directly from the configuration panel for Stripe components. * 🧪 **New Experiments from Web App** – Experiments can now be fully configured and managed directly within the web app interface. ### ⚡️ Improvements * Breadcrumb Size Reduced – A cleaner workspace with more screen real estate and less visual clutter. ### 🐞 Bug Fixes * Firefox Overview Page Fix – Resolved an issue where the Web App Overview Page was broken on Firefox. </Update> <Update label="29 April 2025"> ### 🔥 Features and Updates * 🧪 **Customer journey QA tool** - Test, validate, and debug flows with a new internal quality tool. * 🚫 **Exit Page flag** - Flag pages as an 'Exit page' to prevent users who are shown that page from progressing past it - perfect for disqualification pages like 'We don't cover your US state'. * 📐 **Collapsible left sidebar** - More room to build! * 🔄 **Additional page transition options** - Smooth and customized flow transitions for better UX (an easy UI for editing this is coming very soon!). * 🎯 **Custom style selectors** - Define and apply custom CSS selectors directly inside your components. * 🔌 **New Action configurations** - Added support for Airtable, HubSpot, Google Sheets, and Webhooks. ### 🐞 Bug Fixes * Fixed Preview Mode DOM Interactions - Interactive elements now behave as expected during preview. * Fixed Unintended Edits on Stripe Component Selection - Selecting a Stripe component no longer triggers an edit in the history log unless a real change is made. * Fixed Stripe Checkout Sync Behavior - Improved sync logic to prevent unnecessary updates and errors when editing Checkout options. * Improved Condition Key Filtering - Duplicate keys no longer break the search bar. * Trigger Sync on Button Key Changes - Key changes now instantly reflect in logic without needing tab switches. * Global Images Render Correctly - Visual assets now load instantly after changing them. </Update> <Update label="17 April 2025"> ### 🔥 Features and Updates * 🎛️ **Advanced component options** - Added advanced configuration support for components like Calendly, HubSpot, Lottie, Media Embeds, File Uploads, Rive, PayPal, and Progress Bars. * 💳 **Trigger Actions from Stripe events** - Added support for triggering workflows based on specific Stripe events: payment attempt, failure, or completion. ### ⚡️ Improvements * Discard Changes now clears drafts - No more persistent reload notifications! * Warnings about other people’s edits before saving - If you try to save while there are edits from someone else, a confirmation is required. A tooltip now shows who made the external changes. ### 🐞 Bug Fixes * Fixed loading Builder edit mode access - Now automatically enters edit mode as expected * Blank Embeddables no longer miss parent\_id - New creations are properly linked from the start </Update> <Update label="8 April 2025"> ### 🔥 Features and Updates <Frame label="Stripe Checkout 2.0"> <img /> </Frame> * 🚨 **Live Version Alerts** - Get notified when other users save a version, edit the version you're on, or create new drafts. Stay in sync with your team! * 💳 **Brand new Stripe Checkout 2.0** - Brand new design, much more flexibility, and better handling of events and errors. * 🏷️ **Conditional Tags** are now officially live — tag smarter, not harder. * 📄 **Brand New Pages Grid** - A full-screen view of all your pages in a grid format, with more features coming soon to this view. ### 🐞 Bug Fixes * Lowercase email comparison is fixed — no more invite issues with uppercase emails. * Conditions text now looks clean and doesn't overflow — plus some nice improvements to make editing smoother ✏️ </Update> <Update label="3 April 2025"> <Frame> <img /> </Frame> ### 🔥 Features and Updates * 🔍 **Zoom controls** are now available for the Embeddable preview—adjust it right from the top bar! * 🏷️ **Recently used tags** are back! Say hello to faster tagging 🎉 ### ⚡️ Improvements * The Open Old Builder button now takes you to the old Builder in the same tab, with an easy way to switch back to the new one. ### 🐞 Bug Fixes * Conditions that were very long were breaking in a way that you couldn't see the button to edit component conditions - that's now fixed. * The Add Page button was appearing on top of the CSS editor and could cause issues when manually editing CSS. * Toast alerts are now fully interactive—you can click or close them even with a popover or modal open. </Update> <Update label="31 March 2025"> ### 🔥 Features and Updates - 📖 **Docs Relaunched** - We've completely rebuilt the Embeddables Docs to make it easier to find what you need. - Now, our docs are organized into: - [Features](/features) - Overviews of each major feature in Embeddables. - [Guides & Best Practices](/guides) - More in-depth explanations of how things work, and how to get the most out of the platform. * [How-To's](/how-to) - Step-by-step guides to achieve specific tasks. - \[API - Reference]\(/api-reference) - Technical reference for the Embeddables API, as well as a glossary and this changelog! - We're still improving it and will be adding many more docs over time. - Let us know what docs you'd like to see! - 🎥 **Onboarding Tour Video** - Along with the launch of our brand new docs, we've also created an onboarding tour video to show you (nearly) everything you can do in the Builder. - [Watch it here](/builder/overview) and be fully ramped up in the Builder in just over 5 minutes. </Update> <Update label="15 March 2024"> ### 🔥 Features and Updates * 👨‍👩‍👧 **Multi-player on Embeddables** has been a little underwhelming so we're working on it. Starting today you'll seen an alert in the top left if someone else is working at the same time as you and your versions could result in a conflict. * 🚀 **Push-to-Prod/Staging Logs** are now in the version control card so you can see who pushed what version to prod and staging and when. No more back and forth Slack messages or scrolling through the version card for 5 minutes. ### ⚡️ Improvements * We've made it easier to select layers in the layer card as the hit area was a little too small for our liking. * The Builder and more specifically the Logic card should now feel more snappy as we found more ways to squeeze the juice out of them. ### 🐞 Bug fixes * When you typed code in the code editor and click 'Save' and close the card before clicking 'Apply Changes' your code could be lost. That's now been improved so your code is no longer lost. </Update> <Update label="8 March 2024"> ### 🐞 Bug fixes - The rich text component would periodically decide to become 500px tall whilst you were editing inside of it in the Builder. It's now no longer doing that party trick. - Updating the key name in the options card sometimes caused an alert that said 'Undefined'. This was unhelpful and annoying and is now fixed. - Charts now load more reliably and don't go blank when you change a filter. - Copy and Paste keyboard shortcuts stopped working on the Layers card. Now they're working again. </Update> <Update label="6 March 2024"> ### 🔥 Features and Updates * 👥 **Updated filter component:** We re-built the filter component as it was a little clunky and unreliable. It's now simpler (we think) and more reliable. * 🪄 **Split test individual components:** You can now experiment with hiding/showing components within a test. Unsure if the social proof container increases click-throughs? Now you can test just showing/hiding it without duplicating the entire page and running an A/B split on the page. * 📞 **Brand New US Phone Number formatting**: You can now specify formatting for US phone number inputs including the visual display and what formats are accepted by validation. We're looking at you, "+1". ### ⚡️ Improvements * The key names on the layers tab sometimes get a little long. Now they have tooltips for when they do. * Scrolling within the UI of the Builder felt a little off. We've made some design changes to the scroll design and action to make it feel more complete. * You can now duplicate an existing Computed Field. Apparently that didn't exist and this product manager is finding out for the first time. ### 🐞 Bug fixes * Occasionally the 'Today' filter wouldn't apply and you'd be stuck sitting wondering what was going on. That's now back to being its useful self. * On the Options card it randomly scrolled you when you selected a tab. That's now behaving itself. * Updating the key of a computed field (or add a new one) it doesn't overwrite the name of the input or trigger keys. * Split testing requires snake\_case and occasionally you could set the key without it being snake\_case and the test would fail to run. That's now fixed. * The layers card now stays in place and doesn't jump around the screen every time you open it. </Update> <Update label="23 February 2024"> ### 🔥 Features and Updates * 🏃🏼 **Saving your Embeddable is now 40% faster.** We know watching Embeddablob is fun but waiting to get something saved took a little too long. This update improved that speed. * ♿️ **Accessibility controls** for Embeddables launched in Alpha. You can now navigate through the Embeddable using the 'tab' button to move between components and the 'Enter' key to move to the next page. To get access reach out to the team and we'll enable it. ### ⚡️ Improvements * The 'Buttons' tab in the 'Options' card was too hard to access so it's been brought forward and is now available without scrolling. * You can now access the Custom HTML Editor in a full screen experience rather than a tiny little window. It's like the IMAX of code editors now. * You can now specify the maximum selection in a multi-select button e.g. up to 3 responses. This should have been added sooner but it's finally off the backlog. * Deleting is scary but occasionally you make a mistake. We've made it easier to delete an Option Button without clicking 4 times. * We increased the accuracy of click submissions across a wider range of browser types so you can trust your funnel charts even more. * You can now filter to the highest page a user reached. Perfect for seeing which users got to say the email page but no further in live session replay and their submission data. * You can now see the number of events a user had in the users table. Great for seeing at a glance if they did a little or a lot in your Embeddable. ### 🐞 Bug fixes * Occasionally discarding changes could cause the page to temporarily appear blank. That scared you (and us) and won't happen any longer. * Archived Embeddables sometimes remained archived even when you clicked 'Un-Archive'. We had a word with the archives and now they're back to behaving themselves. Sorry about that. * API Fix for User Submissions. Occasionally the query slug would be slow but it's now faster. </Update> <Update label="16 February 2024"> ### 🔥 Features and Updates - Accessibility features: alt-text is not just for images! It's now easy to add alt text and equivalent meta descriptions and labels in Buttons, Options Buttons, Dropdowns and more. ### ⚡️ Improvements - Various performance updates on the Web App mean that it's now significantly faster! - Stability, logging and performance updates on the Builder. ### 🐞 Bug fixes - Hitting `ESC` inside an input or inside a Code Editor, while the text editor is in focus, will no longer close the card lose your un-applied edits. - The Assets card, when first opened, no longer fails to replace images with a click. </Update> <Update label="12 February 2024"> ### 🔥 Features and Updates * Submission Table & Export CSVs * Long-awaited and much-requested, you can now easily view and download your submissions from a particular Embeddable. * Head to the page of a particular Embeddable in the Web App and you will now see a spreadsheet-like table of all submissions (or 'entries') for that Embeddable. * You can also hit the Download button to export a CSV of the last 1,000, 10,000 or 100,000 submissions, to analyze further in your software of choice. * Preset option collections\*\* for Option Buttons and Dropdowns: select from presets like 'Countries' and 'US States' to save typing them all o * Button tooltips\*\*: give your users more info in case they need it, with easy-to-add tooltips on buttons. <Frame> <img /> </Frame> </Update> <Update label="8 February 2024"> ### 🔥 Features and Updates - **Version Sizes** - Ever struggled to find that version from weeks ago where you made a huge number of edits? - You can now see the number of edits made in each saved version of an Embeddable (applies to versions saved from Feb 8 onwards). - US Phone number formatting\*\*: Our brand new US phone number formatting features are now available in `BETA` and in [`JSON-Only`](/reference/glossary/glossary#json-only-feature). Contact us if you want to try them o - Compare Unsaved JSON Changes\*\*: The versions diff now has a Current JSON option, so you can review your draft updates before hitting sa ### ⚡️ Improvements - Our 'Auto-Preload Next Page Images' feature now applies to images inside buttons. </Update> <Update label="1 February 2024"> ### 🔥 Features and Updates - You can now find the split test that you're looking for by filtering in the search bar of the Experiments card. - Button components now have a 'Text Wrapper' sub-element (which contains the 'Text' and 'Description' elements) available to be styled in the Designer. - You can now edit Logic items (Actions and Computed Fields) by directly editing the JSON. ### ⚡️ Improvements - Several aspects of the product now load faster, including various analytics features in the Web App, and the initial load of the Builder. - Undo/redo functionality is now visually synced with the Split Test card. - The versions card now shows the exact date + time of each save, rather than simply "5 days ago". ### 🐞 Bug fixes - Non-images no-longer have broken thumbnails in the Assets card. </Update> <Update label="29 January 2024"> ### 🔥 Features and Updates * 🧪 **Split Testing v2** This complete rebuild of our Experiments feature makes it easy and simple to do the following: * Create new experiments. * Pause experiments, selecting a temporary winner\*. * Complete experiments, choosing a winner\* - this will automatically remove conditions as well as losing pages and components that are no longer used. * Add variants and edit variant weights. * Easily add experiment-related conditions to components. * Get a preview link for your experiment with the variant you choose, so you can test out that variant as a user. * View which pages + components are related to a particular experiment variant - these are color-coded, both in the preview and in the Layers card. * See a list of past completed experiments, when they were completed, and (for new experiments\*) which variant won. <Info> \*Note that this only applies to experiments created in Split Testing v2. Older experiments can't be paused or completed in the same way. </Info> </Update> <Update label="25 January 2024"> ### 🔥 Features and Updates * When you add a new component, it will now be automatically selected. * When building with ChatMessages components, you can now select which GPT model to use. (Currently [`JSON-Only`](/reference/glossary/glossary#json-only-feature) for now) ### ⚡️ Improvements * Re-rendering of pages and components is more efficient, making the Builder faster to work with. * Initial rendering of Embeddables now fills templates with data (e.g. default userData & external content) for certain component types, which should lead to faster load times and better Google Lighthouse scores. ### 🐞 Bug fixes * The Pages card no longer sometimes plays videos or loads iframes in the background. * The ChatMessages component no longer fails to work if "Handle AI Messages" is not checked. </Update> <Update label="22 January 2024"> ### 🔥 Features and Updates * **Conditional Tags** are now live in \`BET * Add a condition to a tag so that it is only added to a page/component/element if that condition is met. * Perfect for styles that you want to add/remove dynamically. <Frame> <img /> </Frame> </Update> <Update label="18 January 2024"> ### 🔥 Features and Updates * You can now **Copy/Paste components and pages between different Embeddables**! * Use `Ctrl/Cmd + C` to copy a component, then `Ctrl/Cmd + V` to paste it in another Embeddable. * This works with containers - it will copy all child components along with it. * You can also use the Copy button in the Options card. * This will copy across the structure **without styles**. * You can also **copy with styles included** using the "Copy with Styles" button in the Options card. * This will bring all the styles associated with the applied tags. * If any conflict occurs you will be asked to choose whether to replace tag styles or keep them. <Frame> <img /> </Frame> </Update> <Update label="11 January 2024"> ### 🔥 Features and Updates - Keyboard shortcuts for selecting different component - You can now change the selection using the arrow keys. - Up and Down for siblings components. - Left and Right for entering or exiting a container - Quick access button to open **list of component triggers** - Click the icon above the border of a selected component to quickly view all triggers associated with that component. - Easily see a **list of triggers for an Action** by hovering over it. - Debounce option\*\* for InputBox componen - Default User Data\*\* setting available in Whole Embeddable options ### ⚡️ Improvements - Easier login/logout UX. ### 🐞 Bug fixes - Editing text/descriptions of OptionsSelector/CustomButtons with Custom HTML no longer breaks the HTML. - Dragging global components on the layers no longer breaks the entire Embeddable JSON. - Builder no longer allows setting keys starting with numbers. - Building repeatable containers is no longer buggy. - When dragging a container into a global position, it now brings the child components too. </Update> # CLI Changelog Source: https://docs.embeddables.com/reference/changelog/cli-changelog Version history for the Embeddables CLI (@embeddables/cli) Install or upgrade: `npm install -g @embeddables/cli` Check your version: `embeddables -v` *** <Update label="v0.16.7 — 27 May 2026"> ### Features * **Workbench: page thumbnail strip** — A new toggleable thumbnail row below the Workbench shows a preview of every page in the Embeddable, with the current page highlighted and auto-centered. Click a thumbnail to jump via `Savvy.goToPage`; drag the divider to resize the strip. Thumbnails use the Engine's static HTML when available and derive their viewport from the Flow's breakpoints, and pages hidden or gated by unmet conditions or experiments are dimmed in both the page navigator dropdown and the thumbnail row. Strip visibility and height persist in `WorkbenchPrefs` (localStorage), the list is virtualized with `@tanstack/react-virtual`, and the strip is hidden by default. * **Workbench: action-debug toast toggle** — A new bolt button in the Workbench toggles action-debug toasts on/off and syncs the `savvy_debug` URL param (preserving `history.state` and removing the param on toggle-off so a reload reflects the new state). Includes `aria-label`/`aria-pressed` for accessibility. ### Chores * **AI prompt rules: Actions and multilingual guidance** — Consumer AI context now documents the full `output()` second-arg helper API (`setUserData`, `goToPage`, `goToNextPage`, `trackCustomEvent`) with destructuring examples, clarifies that computed fields typically only use `trackCustomEvent`, and adds Flow-level `languages` config plus end-user runtime language behavior (browser-locale scope in published runtime, exact vs regional key matching, opt-in `outputs_onloadflow` defaults). The global language picker is reframed as a stakeholder-driven, design-agnostic pattern (any UI that sets `embeddable_language`; `OptionSelector` shown as one common example). * **doc-templates AI README: Markdown and JSX typography** — Internal doc-templates AI context clarifies Markdown vs JSX usage in typography: documents automatic class application for standard HTML headings/paragraphs and the explicit JSX requirements for custom styling, with an improved table layout. </Update> <Update label="v0.16.6 — 19 May 2026"> ### Bug Fixes * **Global component insert positions in JSON diff → edit history** — Fixed `src/helpers/json.ts` so that insert anchors for components added in a diff are scoped by their `_location` (global slot vs. page-level bucket). Sibling ordering now uses only true ordering peers (same parent or same `_location`), the `isGlobalComponents` flag is passed for root globals, and `insertedComponentId` is excluded from anchor resolution so a component can never anchor against itself. Move-command logic was also tightened to preserve component hierarchy across global slots. </Update> <Update label="v0.16.3 — 18 May 2026"> ### Chores * **PR template: Decisions section** — The repo's pull request template now leads with a `Decisions` section capturing approach, tradeoffs, and alternatives so reviewers see key decisions at the top of the PR. Contributor-facing only. </Update> <Update label="v0.16.2 — 6 May 2026"> ### Improvements * **Per-operation edit history on save and publish** — `embeddables save` and `embeddables dangerously-publish` now emit per-change `editHistoryDescriptions` matching the Builder's format (e.g. `Set component login_btn [id: comp_123] property text to "Continue"`, `Add component cta_button in page welcome`, `Move page checkout`, `Remove tag draft from Page welcome`). Previously each save/publish recorded a single generic `Saved version from CLI` entry. Descriptions are capped at 100 entries with an overflow marker, mirroring the Builder. The `trigger.editor` field on save/publish now records the actual user ID instead of the literal string `"CLI"`. ### Chores * **AI-README: PR title workflow for Linear issues** — Internal contributor guide updated with the PR title format used for Linear-tracked work. </Update> <Update label="v0.16.1 — 23 April 2026"> ### Features * **`disable_button_until_page_valid` for `StripeCheckout2`** — `StripeCheckout2` now accepts a `disable_button_until_page_valid` boolean prop. When set to `true`, the payment button stays disabled until all page-level validation passes, preventing premature payment attempts before required fields are complete. ### Bug Fixes * **Workbench: removed nonexistent `setContentLanguage` call** — The Workbench language switcher was calling `window.Savvy.setContentLanguage()`, which does not exist in the engine. The no-op call has been removed; language switching continues to work correctly via `setUserData` with the language key. ### Chores * **AI prompt rules: `window.Embeddables.trackCustomEvent`** — Consumer AI context updated to reference `window.Embeddables.trackCustomEvent()` for host-page custom event tracking, replacing the legacy `window.Savvy.trackCustomEvent()` reference. * **AI prompt rules: legacy `experiments` array prohibited** — Consumer AI context now explicitly prohibits writing the top-level `experiments` array on a Flow. `connected_experiments` is the only field AI agents may write for experiment connections, preventing experiments from disappearing from the Experiments UI. </Update> <Update label="v0.16.0 — 3 April 2026"> ### Features * **`embeddables experiments create`** — New command that creates an experiment for one or more Embeddables. Accepts `-i, --id <id>` (Embeddable ID), `-n, --name <name>` (experiment name), `-k, --experiment-key <key>`, `-p, --project-id <id>`, `--variants <list>` (comma-separated `key:weight` pairs; defaults to `control:50,treatment:50`), `--status <status>` (defaults to `draft`), and `--auto-adjust` (automatically scale variant weights to 100% without an interactive prompt when weights don't sum to 100). * **`GEMINI.md` emitted on `init` / `upgrade`** — `embeddables init` and `embeddables upgrade` now also write a `GEMINI.md` file at the project root, following the same short-pointer convention as `AGENTS.md`. Gemini CLI will automatically load the shared `AI-README.md` development guide. * **`languages` property support** — The top-level `languages` array is now recognised in Flow types. A new `EmbeddableLanguage` type (`{ key: string; name: string }`) is exported from `.types/`, and `infobox_use_dialog_element` is also added to the Flow type to align with the Builder. ### Bug Fixes * **Stripe/PayPal payment trigger output props** — `on_payment_complete_outputs`, `on_payment_failed_outputs`, and `on_payment_attempted_outputs` on `StripeCheckout`, `StripeCheckout2`, and `PaypalCheckout` components now render as first-class named JSX props in the reverse compiler instead of falling into the `props={}` catch-all. * **`InputBox.max_length` type** — The `max_length` prop on `InputBox` is now typed as `number` (matching the Builder), not `string`. * **`embeddables init` spinner** — The "Fetching projects…" spinner is now cleared before the interactive project selection prompt is shown, preventing the spinner text from persisting below the list during the entire selection interaction. * **`embeddables pull` fails on missing Embeddable** — `pull` now exits with a clear error when the remote Embeddable is missing (empty-flow fallback response or 404), rather than silently writing a broken local repo. ### Improvements * **`embeddables diff` shows item names** — Changes to `computedFields` and `actions` (dataOutputs) in diff output now list each affected item by name/key/id (`- removed`, `+ added`, `~ modified`) instead of a generic `[N items] → [M items]` summary. ### Chores * **AI prompt rules updated** — Internal `.prompts` context updated: `track_clicks` added to `CustomButton` guidance; `languages` property clarification added; payment component trigger-type table now correctly includes legacy `StripeCheckout` alongside `StripeCheckout2` and `PaypalCheckout`. </Update> <Update label="v0.15.1-beta.0 — 27 March 2026"> ### Features * **Workbench: Feedback Panel** — A new Feedback panel in the Workbench lets you submit annotated feedback on an Embeddable directly from the preview. Supports image paste (including on Windows), session upload management, tagging, and a shared session welcome modal. Feedback is stored with the project ID from `embeddables.json`, injected automatically in dev mode. * **`embeddables dangerously-publish`** — New command that promotes an existing saved version to staging or production without a full rebuild. Requires exactly one of `--staging` or `--prod`. Use `--via-staging` with `--prod` if the version is not yet on staging. Pass `--save` to upload a new saved version first, or `--publish-version <number>` to target a specific version instead of reading from `config.json`. * **`embeddables inspect`: fixable failures shown as warnings with fix-and-retry prompt** — When `inspect` encounters component-level issues that can be auto-fixed (e.g. missing required props), it now displays a warning and prompts you to retry with `--fix` rather than exiting immediately. The prompt defaults to yes, preserving the previous auto-fix feel. Matches the existing behaviour in `embeddables pull`. * **`OptionSelector` placeholder prop** — `OptionSelector` now accepts a first-class `placeholder` prop with full multi-language translation support. * **Workbench: Reset User Data toast** — The Workbench now shows a toast confirmation when user data is reset. ### Bug Fixes * **Workbench: image paste on Windows** — Pasted images are now accepted when the clipboard item's MIME type is empty, which occurs in some Windows browsers and clipboard sources. * **`embeddables save`: 500 error response body surfaced** — When the server returns a 500 during `save`, the response body is now parsed and included in the displayed error message. * **`embeddables init` / `upgrade`: single Claude rule file** — `embeddables init` and `upgrade` now write `.claude/CLAUDE.md` only; the legacy `.claude/embeddables-cli.md` file is removed on upgrade. Aligns with Claude Code conventions and avoids duplicate context. * **Workbench: toast notification fixes** — Toast is now positioned above the Workbench bar (outside the panel), aligned to the right, with improved contrast (solid success/error colors), correct state reset when the Embeddable ID changes, and proper timer cleanup on unmount. </Update> <Update label="v0.15.0 — 23 March 2026"> ### Features * **Shared `AI-README.md` + short pointer rule files** — `embeddables init` now creates a shared `AI-README.md` at the project root containing the full Embeddables CLI development guide. The AI editor rule files for Cursor (`.cursor/rules/embeddables-cli.md`), Claude (`.claude/embeddables-cli.md`), Codex (`AGENTS.md`), and Antigravity (`.agent/rules/embeddables-cli.md`) are now concise pointer files that reference `AI-README.md`, rather than each duplicating the full content. This keeps rule files small and ensures they stay in sync. * **`embeddables upgrade` refreshes AI context files and type stubs** — Running `embeddables upgrade` now also regenerates AI rule files and `.types/` type stubs in the project to match the newly installed CLI version, so they stay up to date automatically without needing to re-run `embeddables init`. * **`preserve_json_in_cli` for `CustomHTML`** — `CustomHTML` components now accept a `preserve_json_in_cli` boolean prop. When set, the HTML content is preserved as-is during CLI round-trips, bypassing HTML reformatting. Useful for raw embeds that should not be touched by the compiler. ### Bug Fixes * **Workbench: `OptionSelector` dropdown autofill** — Dropdowns in the Workbench field editor now autofill correctly. Fixed by removing an empty-buttons guard and switching to `setUserData` for consistent state propagation. * **`embeddables branch`: soft-deleted branches hidden** — Soft-deleted branches are now filtered out of the interactive branch selection prompt, keeping the list clean. * **`CustomHTML`: whitespace trimming and hyphenated SVG attributes** — Whitespace in `CustomHTML` text content is now trimmed correctly; hyphenated SVG attributes (e.g. `stroke-width`, `fill-rule`, `stroke-linecap`) are preserved through round-trips. * **`embeddables pull`: reduced style-only diffs** — Fixed an issue where pulling could introduce unwanted whitespace changes in `CustomHTML` serialization, causing spurious diffs. * **Edit history: array value diffs** — Array-type values in edit history now diff correctly when saving, preventing spurious change records. * **Page-level trigger output types** — Page-level outputs now correctly identify trigger output types, improving type safety. ### Improvements * **Computed field and action function signatures** — `result()` and `output()` now accept the full three-argument form (`userData`, `helperFunctions`, `triggerContext`). Trailing unused parameters can be omitted, keeping simple computed fields concise. ### Chores * **AI prompt guidance for disabled CTAs** — Internal AI context updated with the `needs_validation_passed` + `validation_show_state` pattern for implementing disabled CTA button states. </Update> <Update label="v0.14.4 — 9 March 2026"> ### Improvements * **`addTaskBranch` now stores `project_id`** — When a branch is associated with a task, the CLI now correctly includes `project_id` in the `task_branches` database record alongside `task_id` and `branch_id`. A debug log line is also emitted during the insertion, improving visibility for branch operations in development. </Update> <Update label="v0.14.3 — 9 March 2026"> ### Improvements * **`branches create` success message shows branch ID** — After successfully creating a branch, the CLI now displays both the branch name and the branch ID (as returned by the server) in the success box, making it easier to reference the new branch in subsequent commands such as `embeddables pull -b <id>` or `embeddables save -b <id>`. ### Chores * **CLI option formatting** — Internal option definitions in the CLI entry point have been reformatted for consistency and readability; no user-facing changes. </Update> <Update label="v0.14.2 — 9 March 2026"> ### Features * **`embeddables branches create`** — New command that creates a new branch from the current Embeddable state. Resolves the Embeddable from `--id`, the current working directory, or an interactive prompt; reads the origin version and branch from `config.json`. Use `-n, --name <name>` to set the branch name (required). Requires login. * **`embeddables branch --branch` option** — `embeddables branch` now accepts `-b, --branch <branch_id>` to switch directly to a known branch ID or name without going through the interactive selection list. ### Chores * **`branches` recognised as a utility command** — The `branches` subcommand group is now listed in the utility commands set, so branch management commands are correctly grouped in `embeddables --help` output. </Update> <Update label="v0.14.1 — 9 March 2026"> ### Features * **`embeddables tasks`** — New command group for managing project tasks directly from the terminal. All subcommands require login and resolve the project from `embeddables.json` or an interactive prompt: * **`tasks list`** — Print a compact table of all tasks for the current project (`-p, --project-id <id>` to skip the prompt). * **`tasks get -i <id>`** — Show full details of a single task by ID. * **`tasks update-status -i <id> -s <status>`** — Change a task's status. Valid values: `to_do`, `scoping`, `in_progress`, `feedback_cycle`, `qa`, `blocked`, `completed`, `cancelled`. * **`tasks assign -i <id>`** — Reassign a task. Pass `--assignee-id <user-id>` to assign to a specific user or `--assign-to-owner` to assign back to the task owner. * **`tasks comment -i <id> -m <message>`** — Add a comment to a task. * **`tasks add-branch -i <id> -b <branch-id>`** — Link an Embeddable branch to a task. * **`embeddables branches create`** — New subcommand that creates a new Embeddable branch from the current local state. Pass `-n, --name <name>` for the branch name; the origin version and branch are read automatically from `config.json`. Requires login. ### Chores * **Expanded CLI help and README** — `embeddables --help` output and the package README now include full option documentation for `inspect`, `diff`, `feedback`, `builder open`, `experiments connect`, `assets upload`, `assets sync`, and all task management commands. </Update> <Update label="v0.14.0 — 9 March 2026"> ### Features * **Non-interactive flags for all commands** — Every interactive prompt in the CLI (excluding `login`) now has a corresponding flag, making all commands fully scriptable for use in CI pipelines, AI-driven workflows, and automation: * **`embeddables branch`**: `--branch <id|name>` skips the branch selection prompt. * **`embeddables save`**: `--project-id <id>` skips the project selection prompt; `--force` skips all confirmation prompts (version conflicts and other users' drafts). * **`embeddables pull`**: `--project-id <id>` skips the project selection prompt. * **`embeddables builder open`**: `--project-id <id>` skips the project selection prompt. * **`embeddables experiments connect`**: `--project-id <id>` skips the project selection prompt. </Update> <Update label="v0.13.0 — 9 March 2026"> ### Features * **`embeddables tasks` command group** — New commands for managing project tasks directly from the CLI. All subcommands require login and resolve the project from `embeddables.json` or prompt interactively. * **`tasks list`** — Display a table of all tasks for the current project (`-p, --project-id <id>` to target a specific project). * **`tasks get`** — Fetch and display full details for a single task (`-i, --id <id>`): status, priority, type, assignee, dates, body, and effort. * **`tasks update-status`** — Change a task's status (`-i, --id <id>`, `-s, --status <status>`). Valid values: `to_do`, `scoping`, `in_progress`, `feedback_cycle`, `qa`, `blocked`, `completed`, `cancelled`. * **`tasks assign`** — Change a task's assignee (`-i, --id <id>`, then either `--assignee-id <userId>` or `--assign-to-owner` to assign to the task's owner). * **`tasks comment`** — Add a comment to a task (`-i, --id <id>`, `-m, --message <content>`). * **`tasks add-branch`** — Link an Embeddables branch to a task (`-i, --id <id>`, `-b, --branch-id <id>`). * **`embeddables login --token <token>`** — New `-t, --token` option for non-interactive, token-based login. Pass a JWT directly instead of going through the OTP email flow — ideal for CI pipelines and cloud containers where interactive prompts are unavailable. * **`--bypass-auth` flag for `pull`, `dev`, and `build`** — Hidden flag that skips the login check, allowing these commands to run without a stored auth token. Intended for CI environments and automated tooling. (`save` continues to require login.) </Update> <Update label="v0.12.1 — 5 March 2026"> ### Improvements * **Workbench: in-app modal dialogs in User Data panel** — The User Data panel in the Workbench now uses proper in-app modal dialogs instead of native browser `window.prompt()` calls. Both the "Save Persona" and "Go To Page" actions open a focused modal with a text input, inline error display, and keyboard shortcuts (Enter to confirm, Escape to dismiss). This also fixes the Workbench on Cursor Cloud, where native browser dialogs are not supported. ### Chores * **AI development guidelines added to internal dev context** — `AI-README.md` now includes an "AI Development Guidelines" section covering code comment conventions, testing requirements, and an end-of-workflow inconsistency check. `CLAUDE.md` and `AGENTS.md` tables-of-contents were updated to reference the new section. * **Cursor Cloud instructions in developer docs** — `AI-README.md` and `AGENTS.md` now include specific setup and usage instructions for working within Cursor Cloud, including notes on CLI-only commands and test video recording guidelines. * **Developer guide centralized in `AI-README.md`** — `AGENTS.md`, `CLAUDE.md`, and `.cursor/rules/embeddables-dev.md` now point to `AI-README.md` as the single source of truth for the internal development guide. </Update> <Update label="v0.12.0 — 4 March 2026"> ### Features * **Antigravity IDE support in `embeddables init`** — `embeddables init` now creates `.agent/rules/embeddables-cli.md` for [Antigravity](https://antigravity.dev), joining Cursor (`.cursor/rules/`), Claude (`.claude/`), and Codex (`AGENTS.md`) as a supported AI editor. The file is generated with the correct Antigravity frontmatter (`globs`, `alwaysOn`) and the same full Embeddables CLI context injected into other editors. * **`_docs` property** — Embeddables now support a `_docs` property for documenting non-obvious aspects of your configuration. It can be set at the top level, on individual pages, computed fields, and data outputs. The value is preserved through pull and save round-trips unchanged, making it safe to add inline documentation to your `config.json` without it being stripped. ### Bug Fixes * **Video mute/play state lost on `pull`** — Fixed two root causes that caused `<video muted autoplay loop>` and related MediaEmbed properties to disappear after `embeddables pull`. First, several `MediaEmbed` component-specific props (`silentAutoplayPreview`, `hide_controls`, `central_play_button`, `roundedCorners`, `embed_code`, `placeholder_image_url`, `embed_code_raw`) were missing from `COMPONENT_SPECIFIC_PROPS` and are now correctly included. Second, the HTML-to-JSX parser in `CustomHTML` was silently dropping boolean HTML attributes (those without `=` signs) — `muted`, `autoplay`, `loop`, etc. now survive as proper JSX boolean attributes. ### Improvements * **Richer AI prompt guidance for actions and events** — The Embeddables CLI context file (`.cursor/rules/embeddables-cli.md`, `.claude/embeddables-cli.md`, `AGENTS.md`, `.agent/rules/embeddables-cli.md`) now includes detailed guidance on: wiring action triggers, programmatically triggering actions, using `triggerContext` to pass data between actions and computed fields, and tracking custom events. TypeScript types are emphasised as the primary source of truth for valid prop values. ### Chores * **JSDoc documentation for `cli.ts`** — Internal JSDoc comments added to `src/cli.ts` to improve developer experience when navigating the CLI entry point. * **`.cursorindexingignore` added** — SpecStory AI session files are now excluded from Cursor's indexing to keep the index lean. </Update> <Update label="v0.11.0 — 27 February 2026"> ### Features * **`embeddables assets upload`** — New command that uploads a local asset folder to the Embeddables asset store. Point it at a directory (`--dir <path>`, default: `assets/`) and it validates, then uploads all supported files (images, fonts, documents, etc.) with a concurrent worker pool. Key options: `--dry-run` (preview what would upload without uploading), `--recursive` (traverse subdirectories), `--include` / `--exclude` (glob filters), `--group-id` (target a specific asset group), and `--workers <n>` (concurrency, default 5). Prints a summary of discovered/uploaded/skipped/failed files and exits non-zero on any failure. * **`embeddables assets sync`** — New companion command that syncs project asset metadata from the cloud into a local `assets.json` file. `assets.json` becomes the source of truth for referencing uploaded asset URLs in your Embeddable (e.g. images, downloadable files). Run after uploading to keep your local manifest up to date. * **`default-styles.css` created by `init`** — `embeddables init` now writes a `default-styles.css` file to the project root when it doesn't already exist. This file provides baseline styles for standard HTML tags (buttons, inputs, links, etc.) so that Embeddables using unscoped tag selectors render consistently out of the box. ### Bug Fixes * **`experiments connect` falsely marking experiments as already connected** — Fixed a bug where experiments targeting the same Embeddable were incorrectly filtered out and shown as already connected, even though they hadn't been added to the local `config.json`. The command now checks `connected_experiments` in `config.json` as the source of truth for local connections, rather than the server-side `flow_id`. * **TypeScript/React typing errors in pulled Embeddables** — `embeddables pull` now automatically generates `embeddables/tsconfig.json` (with correct `baseUrl` and path mappings to `.types/`) when it doesn't already exist. This eliminates `React refers to a UMD global` and `Cannot find module '@embeddables/cli/components'` errors that appeared in editors after pulling without first running `embeddables init`. ### Improvements * **Workbench: language switcher in User Data panel** — For multi-language Embeddables, the Workbench now shows a language selector dropdown next to the User Data heading. Switching languages updates all field labels and OptionSelector button texts in the Field Editor to show the localised values, and calls `window.Savvy.setContentLanguage()` to update the live Embeddable rendering. ### Chores * **Workbench build script moved to `src/workbench/build.ts`** — The internal `build-workbench` script is no longer in `src/commands/`; it has moved to `src/workbench/build.ts` to better reflect that it is not a public CLI command. Invoked via `npm run build-workbench` as before. </Update> <Update label="v0.10.0 — 26 February 2026"> ### Features * **`embeddables diff`** — New command that compares two versions of an Embeddable and reports what changed. By default it compares `latest` (the last saved cloud version) against `local` (your current working files), showing which pages and components were added, removed, or modified. Use `--depth props` to drill into individual prop-level changes. Filter results with `--page <key>` and `--component <key>` (comma-separated). Accepts any combination of version numbers, `latest`, `staging`, `prod`, or `local` for `--from` and `--to`. * **`embeddables feedback [message]`** — Send feedback directly from the CLI without leaving your terminal. Run with a message (`embeddables feedback "Great workflow!"`) or without arguments for an interactive prompt that asks for your message, sentiment (👍 positive / 👎 negative), and area (CLI, AI prompts, compiler, or other). No login required. * **`branch@version` version specifiers** — `embeddables diff` `--from` and `--to` options now accept `branch@version` syntax (e.g. `main@latest`, `feature-branch@47`) to fetch a specific version from a specific branch, independently of the current working branch. ### Bug Fixes * **Diff: current branch auto-detected** — `embeddables diff` now reads the active branch from `config.json` automatically, so you no longer need to pass `--branch` explicitly when working on a non-default branch. * **Diff: key-order-independent comparison** — Property insertion order in JSON objects no longer causes false positives; objects are compared semantically so only real content differences are reported. ### Chores * **CompileErrors reported as Sentry warnings** — Internal compile errors are now captured as Sentry warnings (with code frame context and file/line metadata) rather than being silently dropped or escalated as exceptions. Lint errors remain excluded from exception reporting. </Update> <Update label="v0.9.6 — 24 February 2026"> ### Bug Fixes * **Custom validation functions support `userData` parameter** — Input Box `custom_validation_function` now accepts an optional second parameter `userData` (`Record<string, any>`), giving your validation logic access to the full current user data. Example: `function validate(value, userData) { return userData.country === 'US' ? /^\d{5}$/.test(value) : true; }`. </Update> <Update label="v0.9.3 — 24 February 2026"> ### Improvements * **Richer error output with code frames** — Compile errors in `build`, `dev`, `inspect`, and `save` now display a highlighted code frame showing the surrounding source lines, a caret pointing to the exact column, and the file path and line number. Errors are written to `stderr` for consistent terminal integration. </Update> <Update label="v0.9.0 — 24 February 2026"> ### Features * **`embeddables inspect --preserve`** — The `inspect` command now accepts a `--preserve` flag to maintain component order during forward compilation. Previously `inspect` always preserved order; it now defaults to `false` (standard compilation order), and you can opt in with `--preserve` when you want to keep existing order for comparison purposes. ### Improvements * **Embeddable context info box across all commands** — `build`, `dev`, `pull`, and `save` now display a consistent info box showing the Embeddable title, ID, branch, and version at the start of each command, replacing the previous plain `ID: …` line in `dev` and varying formats in other commands. * **CLI help output overhauled** — `embeddables --help` now uses Shopify CLI-style formatting with color, icons, and grouped sections for a cleaner, more readable experience. * **`embeddables init` output polished** — The initialization header, success messages, and next-steps box have been updated: the success box now shows project title and ID; the next-steps sequence is now Pull → Run locally → Save (the Login step was removed since login is required before `init`); a prominent yellow beta warning box is shown at the end; and the docs link is included in the next-steps box. * **Trailing gap after all commands** — All commands (`login`, `logout`, `build`, `pull`, `save`, `inspect`, `builder open`, `experiments connect`) now consistently print a trailing blank line for cleaner terminal output. * **Sentry exception enrichment** — Error reports now include descriptive labels, `CompileError` file/line/column metadata, and additional tags (`versionNumber`, `cliVersion`, entity name tags alongside IDs) for faster debugging of production issues. </Update> <Update label="v0.8.3 — 23 February 2026"> ### Bug Fixes * **Automatic Embeddable detection no longer triggers falsely in directories named `embeddables/`** — `inferEmbeddableFromCwd()` previously matched any directory whose parent was named `embeddables`, causing false positives when users kept projects in a folder called `embeddables/` (e.g. `~/embeddables/cursor-telehealth/`). The function now checks for an `embeddables.json` config file before treating a directory as an Embeddable project root. </Update> <Update label="v0.8.2 — 23 February 2026"> ### Bug Fixes — Fixed an infinite loop in the HTML→JSX conversion function triggered when component HTML content begins with a declaration tag such as `<!DOCTYPE html>` or a processing instruction like `<?xml version="1.0"?>`. These non-element tags are now skipped cleanly. A secondary guard was also added so the parser always advances past any `<` character it cannot match, preventing the loop from stalling regardless of the malformed input. </Update> <Update label="v0.8.1 — 23 February 2026"> ### Features * **Delightful CLI output overhaul** — All commands now use animated spinners (via [ora](https://github.com/sindresorhus/ora)), compact tabular output, and concise formatting. Success moments display green boxed messages; grey info boxes show context (Embeddable ID, branch, version). Verbose file listings are consolidated into summary counts. Warnings and errors now use symbols (⚠ / ✖) instead of text badges. ### Bug Fixes * **ESC key now consistently quits all CLI prompts** — Previously, pressing ESC during interactive prompts (e.g. the email input in `embeddables login`, the OTP code entry, or the lint-fix confirmation in `embeddables build`) would silently skip the prompt and continue execution. ESC now reliably exits the process across all prompt types, including auto-complete prompts that previously resolved with the highlighted value instead of aborting. ### Chores * **`prepublishOnly` build script** — `npm publish` now automatically runs the TypeScript build step before publishing, preventing accidental releases with stale `dist/` output. </Update> <Update label="v0.7.19 — 23 February 2026"> ### Bug Fixes * **`embeddables dev` no longer loops when fixing duplicate option IDs** — Fixed an infinite rebuild loop in dev mode triggered when an `OptionSelector` had duplicate button IDs. The fix routine now uses targeted text replacements instead of full Babel code generation, so files are only written when content actually changes. Duplicate ID detection also now covers const-referenced button arrays (e.g. `buttons={someButtons}`), not just inline arrays. * **TypeScript: `props` property now included in generated component types** — The `Props<T>` type emitted into `.types/components.d.ts` by `embeddables init` was missing a `props` field, causing errors like `Property 'props' does not exist on type 'Props<FileUpload>'`. A `props?: Record<string, any>` property is now included. * **`embeddables init` no longer duplicates `.gitignore` entries** — Running `embeddables init` more than once used to append a new `# Embeddables` block each time. It now replaces the existing block in-place. The generated block also adds `node_modules/` and no longer unnecessarily includes `**/.types/`. </Update> <Update label="v0.7.18 — 22 February 2026"> ### Features * **Multi-language support** — The CLI now correctly handles Embeddables with multi-language content, converting between `data-<language>-<property>` in the JSON and the `languages` property in React. </Update> <Update label="v0.7.17 — 22 February 2026"> ### Improvements * **`embeddables pull` defaults to latest version** — Running `embeddables pull` without `--version` now pulls the latest version automatically, skipping the version-selection prompt. To choose a version interactively, use `embeddables pull --version`; to pull a specific version, pass a number: `embeddables pull --version 47`. </Update> <Update label="v0.7.16 — 22 February 2026"> ### Bug Fixes * **Reverse compiler: global component cross-location `parent_id`** — Fixed a round-trip bug where a global component with `_location` pointing to one group (e.g. `before_components`) but `parent_id` pointing to a component in a different location group (e.g. `before_page`) was orphaned during reverse compilation. Components are now grouped by following their parent chain rather than their own `_location`, keeping the component tree intact in a single TSX file. </Update> <Update label="v0.7.15 — 22 February 2026"> ### Features * **`embeddables builder open`** — Opens the Embeddables Builder in your default browser for the selected Embeddable. Resolves the project from `embeddables.json` (or prompts to select one if not configured), then navigates directly to the edit URL. Pass `-i <id>` to skip the interactive prompt, or run from inside `embeddables/<id>/` to infer the Embeddable automatically. * **`embeddables inspect`** — New debugging command that fetches an Embeddable by ID from the engine, reverse-compiles it to React/CSS/JS files, rebuilds it back to JSON, and reports which top-level keys differ between the original and rebuilt output. Designed for diagnosing round-trip compiler bugs. ### Chores * **CI workflow improvements** — Internal documentation update workflow enhanced with version input, updated permission modes, and improved branch/trigger configuration. </Update> <Update label="v0.7.14 — 22 February 2026"> ### Features * **Codex (AGENTS.md) support** — `embeddables init` now generates an `AGENTS.md` file at the project root, following the OpenAI Codex convention. This gives Codex the same Embeddables CLI context that Cursor and Claude already receive. ### Bug Fixes * **Condition `id` now required** — The `id` field on conditions is now required (previously optional). Use the `cond_` prefix + 10 digits format (e.g. `cond_1234567890`), consistent with other ID conventions. Existing conditions without IDs will need one added. * **Case-insensitive sorting in CLI lists** — Lists of embeddables, projects, and experiments shown in interactive prompts now sort case-insensitively using `localeCompare`. Previously, uppercase names (e.g. "CLI") could sort before lowercase ones (e.g. "count"). </Update> <Update label="v0.7.13 — 22 February 2026"> ### Bug Fixes * **Compiler: dataOutputs no longer dropped** — Fixed a bug where `dataOutputs` (including webhook actions) were removed from the compiled output when the `actions/` directory didn't exist or contained no JS files. The compiler now preserves webhook actions from the config even when there are no local JS action files. * **Compiler: improved JSX string escaping** — Strings containing backslashes, newlines, and double quotes are now escaped correctly when generating JSX attributes, preventing double-escaping and malformed output during round-trips. </Update> <Update label="v0.7.12 — 20 February 2026"> ### Improvements * **Archived Embeddables excluded from lists** — Archived Embeddables no longer appear in the interactive selection prompt when running `embeddables pull` or `embeddables dev`, keeping your lists clean and focused. </Update> <Update label="v0.7.10 — 20 February 2026"> ### Features * **Local Embeddables prioritized in selection** — When choosing an Embeddable to work with, locally-pulled Embeddables are now shown at the top of the list. * **`embeddables init` cleans up `.claudefiles`** — If an old `.claudefiles` directory exists from a previous version of the CLI, `init` will now remove it and replace it with the correctly named `.claude/` directory. </Update> <Update label="v0.7.9 — 20 February 2026"> ### Features * **`pull --version <number>`** — You can now pull a specific saved version of an Embeddable: `embeddables pull --version 47`. Useful for checking out an older version or recovering from a bad save. </Update> <Update label="v0.7.7 — 19 February 2026"> ### Features * **Login required for all commands** — The CLI now enforces authentication before running commands that require it (pull, dev, save, etc.). You'll see a clear prompt to log in (`embeddables login`) rather than a cryptic API error. </Update> <Update label="v0.7.6 — 19 February 2026"> ### Bug Fixes * **CMS dev mode: `content_sources` omitted** — Fixed a proxy issue where `content_sources` was incorrectly included in dev mode for CMS-based Embeddables, causing unexpected behaviour. It is now correctly omitted during local development. </Update> <Update label="v0.7.5 — 19 February 2026"> ### Bug Fixes * **Workbench: page selector fixed** — The page selector in the Workbench panel was not working; it is now functional again. </Update> <Update label="v0.7.4 — 19 February 2026"> ### Improvements * **Workbench keyboard shortcut updated** — The Workbench toggle shortcut is now `⌥K` (Option+K on Mac). </Update> <Update label="v0.7.3 — 19 February 2026"> ### Bug Fixes * **Reverse compiler: `CustomHTML` uses `class` not `className`** — When reverse-compiling an Embeddable to TSX, `CustomHTML` components now correctly generate `class` (HTML attribute) instead of `className` (React attribute) in their raw HTML content. </Update> <Update label="v0.7.0 — 19 February 2026"> ### Features * **Structured error logging via Sentry** — The CLI now reports errors to Sentry for improved diagnostics. Logs include context about which command was run, making it easier to debug failures. * **`.claude/` directory** — The Claude project context directory is now named `.claude/` (previously `.claudefiles/`). Running `embeddables init` will create the correct directory. If you have an old `.claudefiles/` directory, run `embeddables init` to migrate. ### Bug Fixes * **Build errors handled gracefully** — The CLI now exits cleanly with a helpful message on certain build errors instead of crashing with an unhandled exception. </Update> <Update label="v0.6.11 — 18 February 2026"> ### Improvements * **Branch state persisted from config** — `embeddables pull` now reads and restores the active branch from `config.json`, so the CLI stays on the correct branch across sessions without needing `-b` each time. </Update> <Update label="v0.6.9 — 18 February 2026"> ### Features * **Workbench shown/hidden via query parameter** — The Workbench debugging panel can now be toggled with `?workbench=true` / `?workbench=false` in the preview URL, without restarting the dev server. * **Custom validation functions in compiled output** — Components with a `custom_validation_function` now compile that logic correctly into the output, based on `validation_formula` presence. * **Edit history tracking in save** — `embeddables save` now includes edit history metadata when uploading, improving version tracking in the Builder. ### Bug Fixes * **`class` vs `className` in CustomHTML** — Fixed a reverse-compiler bug where `class` attributes inside `CustomHTML` were converted to `className`. Raw HTML attributes in custom HTML blocks are now left as-is. </Update> <Update label="v0.6.8 — 18 February 2026"> ### Features * **`pull --preserve`** — New flag to keep the existing component order in `config.json` when pulling. Most first-pull "changes" are React reordering components within containers—`--preserve` prevents that noise so you can focus on real differences. See the [CLI reference](/cli#pull-preserve) for details. * **Metadata-only actions preserved** — Actions that carry only metadata (no JS logic) are now correctly included in the compiled output. </Update> <Update label="v0.6.5 – v0.6.7 — 17–18 February 2026"> ### Features * **`embeddables upgrade` command** — New command to update the CLI to the latest stable version: `embeddables upgrade`. No need to remember the full `npm install -g @embeddables/cli` command. * **`embeddables -v` shows package version** — The version flag now reads directly from `package.json`, so it always reflects the installed package version. * **Custom validation functions compiled to TypeScript** — Components with custom validation logic are now reverse-compiled into typed TypeScript functions, improving editor support and type safety. ### Bug Fixes * **Template literal escaping in button text** — Strings containing `${{ ... }}` (Embeddables template syntax) in button text are no longer accidentally treated as JavaScript template literal interpolations during reverse compilation. </Update> <Update label="v0.6.1 — 16 February 2026"> ### Features * **`--fix` flag for lint auto-correction** — Pass `--fix` to `embeddables build`, `embeddables dev`, or `embeddables pull` to automatically fix common issues: duplicate IDs, duplicate keys, and keys that start with a digit. ### Bug Fixes * **Proxy error handling improved** — The dev proxy server now handles connection errors more gracefully, with clearer error messages when the Engine is unreachable. </Update> <Update label="v0.6.0 — 15 February 2026"> ### Features * **Branch management in pull and save** — `embeddables pull` and `embeddables save` now handle branch metadata automatically, reading and writing the active branch to `config.json`. * **`parent_key` deprecation handled** — The reverse compiler now detects and strips deprecated `parent_key` usage, replacing it with the correct `parent_id`-based approach. </Update> # Glossary Source: https://docs.embeddables.com/reference/glossary/glossary Key terms that we use here at Embeddables #### Contact See: [End-User](#end-user) *** #### Embeddable and Embeddables Embeddables (capital E) is the name of our company and product. We always capitalize the E in Embeddable or Embeddables when referring to the things you ***build*** in our platform. So you build, embed and push live an Embeddable. And you do so using the product Embeddables. Enjoy building Embeddables in Embeddables! *** #### End-User *Note: Also known as a [Contact](#contact).* This is a real life person who is currently viewing/completing, or has previously viewed/completed, one of your Embeddables. This is distinct from an Embeddables User, which is someone who builds Embeddables in our builder - like you! *** #### Entry See: [User Data](#user-data) *** #### Group A Group corresponds to a billing account with Embeddables. If you are a customer of Embeddables that you would typically have a single Group in our system. Groups can contain multiple [Projects](#project). *** #### JSON-Only Feature These are new features in the Builder, which are available for use but don't yet have buttons/inputs in the Builder cards to be able to add/modify them. **Advanced users only:** If you want to use these features in the meantime, you can edit the JSON of the component/page/Embeddable to add/update the feature manually. *** #### Project Each project is a collection of Embeddables, to help you organize them into different... well, projects! Your various Projects are all associated with your [Group](#group). If an [End-User](#end-user) who visits multiple Embeddables that are all within the same Project, they will have multiple Entries grouped within a single Contact. On the other hand, if an End User visits multiple Embeddables that span several Projects (even if they are in the same Group), this will create multiple Contacts. *** #### User Data *Note: Also known as an [Entry](#entry).* This is the set of data that an [end-user](#end-user) of an Embeddable has at any time, for example their email address or the answer they gave to a question. It is stored in `JSON` format and the schema is dependent on the Embeddable that they are using. A user that has taken multiple Embeddables will have a different User Data for each one. In our database, this is known as one [Contact](#contact) corresponding to multiple [Entries](#entry). <Note> This page is incomplete, and will be expanded soon - watch this space! </Note> # Embeddables Web App Source: https://docs.embeddables.com/webapp/overview Manage projects, experiments, entries, and analytics in the Embeddables Web App The **Web App** is your dashboard for managing Embeddables projects, viewing submissions, running experiments, and analyzing performance. ## Common Web App tasks <CardGroup> <Card title="User Roles & Permissions" icon="lock" href="/guides/user-roles-permissions"> Understand workspace roles and what each user can access. </Card> <Card title="Analytics" icon="chart-line-up" href="/features/analytics"> Capture and analyze user data from your Embeddables. </Card> <Card title="Split Testing" icon="flask" href="/features/split-testing"> Configure and manage experiments from the Web App. </Card> <Card title="Dashboard" icon="arrow-up-right-from-square" href="https://app.embeddables.com"> Open the Embeddables Web App. </Card> </CardGroup> # Welcome to the Embeddables Docs Source: https://docs.embeddables.com/welcome You've reached the brand new home for the Embeddables documentation <img alt="Embeddables library blob illustration" /> <img alt="Embeddables library blob illustration" /> ## Learn the key features of Embeddables To go deeper than the Onboarding Tour, we recommend reading through the following sections, which go into how each feature works in more detail. <CardGroup> <Card title="User Data" icon="database" href="/features/user-data"> How data is stored </Card> <Card title="Conditions" icon="split" href="/features/conditions"> Show/hide content </Card> <Card title="Split Testing" icon="flask" href="/features/split-testing"> Test different versions </Card> <Card title="Computed Fields" icon="brackets-curly" href="/features/computed-fields"> Compute new values from users' data </Card> <Card title="Actions" icon="code" href="/features/actions"> Transform and send data to other services </Card> <Card title="Analytics" icon="chart-line-up" href="/features/analytics"> Track your users and measure insights </Card> <Card title="Version Control" icon="floppy-disk" href="/features/saving-and-version-control"> Drafts, saves & versions </Card> <Card title="URL Params" icon="vial" href="/features/url-params"> Pass data from the URL </Card> <Card title="Going live" icon="rocket-launch" href="/features/publish-to-your-site"> Publish to your website </Card> </CardGroup> *** ## Best practices Click here to dive deeper into how to get the most out of the platform: <Card title="Best Practices" icon="lightbulb" href="/guides/naming-keys"> Our top tips and recommendations for how to get the most out of Embeddables. </Card> *** ## How-To guides Finally, our longer list of How-To guides are where you can find the answers to specific questions or or tasks that you're trying to achieve. <Card title="How-To guides" icon="code" href="/how-to/display-answers-back-to-user"> Our full list of How-To guides, from sending data in and out of Embeddables, to all the different ways you can add an Embeddable to your site. </Card>