Get started
Build a host page with your own chart, then load the iC Candle embed as an iframe.
Step 1 — Mount the iframe
<iframe
id="iccandle"
src="https://embed-iccandle-app.iccandle.ai/en?theme=light&header=false"
title="iC Candle"
allow="clipboard-write; payment"
style="width: 100%; height: 100%; border: 0;"
></iframe>
Use a locale path (/en, /zh, …), optional theme (light | dark | system), and header=false when you want to hide the embed app header.
For the full scanner query-param reference, see Search params.
Do not sandbox the iframe in a way that blocks popups (Google / Cognito sign-in) or top-level navigation (Stripe Checkout).
Step 2 — Send parent origin (billing)
Your page and the iframe are cross-origin. They cannot share memory or app state; they communicate with window.postMessage.
On iframe load, tell the embed:
- Which parent origin hosts it (always).
- Optionally that a Stripe checkout just succeeded (
?payment=successon your host URL). - Then clean Stripe-related query params from the parent URL.
const EMBED_ORIGIN = "https://embed-iccandle-app.iccandle.ai";
const iframe = document.getElementById("iccandle");
iframe.addEventListener("load", () => {
const win = iframe.contentWindow;
if (!win) return;
// (1) parent-origin — always
win.postMessage(
{ type: "parent-origin", origin: window.location.origin },
EMBED_ORIGIN,
);
// (2) payment-success — only if ?payment=success
const params = new URLSearchParams(window.location.search);
if (params.get("payment") === "success") {
const post = () =>
win.postMessage({ type: "payment-success" }, EMBED_ORIGIN);
// Retry so the embed is ready to receive the message
[0, 600, 1500].forEach((delay) => window.setTimeout(post, delay));
// (3) strip query params
const url = new URL(window.location.href);
["payment", "session_id", "lookup_key"].forEach((p) =>
url.searchParams.delete(p),
);
window.history.replaceState({}, "", url);
}
});
| Step | Message / action | When |
|---|---|---|
| Parent origin | { type: "parent-origin", origin } | Every iframe load — so Stripe checkout can return to your domain |
| Payment success | { type: "payment-success" } | Only when the parent URL has ?payment=success (retried at 0 / 600 / 1500 ms) |
| URL cleanup | Delete payment, session_id, lookup_key | After posting payment success, via history.replaceState |
Some hosts stringify the payload (JSON.stringify({ type: "parent-origin", … })); the embed accepts both objects and JSON strings. See Stripe return.
Step 3 — Listen for embed messages
Accept messages only from the embed origin. Payloads may arrive as plain objects or JSON strings, so normalize event.data first.
For the full message catalog and payload shapes, see Window messages and Data types.
For iframe hosts, the current embed contract is mostly a namespaced envelope:
type EmbedMessage = {
name?: string;
data?: unknown;
type?: string;
pattern?: unknown;
};
Most outbound messages use name values such as auth.signIn, selector.loading, chart.play, news.eventClicked, and nav.click.
Some pattern-selection flows still send direct objects instead of the name envelope, so your host should inspect both message.name and message.type.
Shared types
/** OHLC bar — same shape as cacheCandle and replay payloads */
interface Candle {
o: number; // open
h: number; // high
l: number; // low
c: number; // close
timestamp: number; // bar time, unix seconds
}
/** Calendar / economic event (news flow) */
interface NewsEvent {
id: string;
timestamp: number; // unix ms
event_name: string;
metric: string;
forecast: string;
actual: string;
previous: string;
currency: string;
}
Handler skeleton
window.addEventListener("message", (event) => {
if (event.origin !== EMBED_ORIGIN) return;
let message;
try {
message =
typeof event.data === "string" ? JSON.parse(event.data) : event.data;
} catch {
return;
}
switch (message?.name) {
case "auth.signIn":
localStorage.setItem("iccandle_token", message.data.idToken);
break;
case "selector.loading":
toggleHostLoading(message.data.isLoading); // boolean
break;
case "chart.play":
onChartReplay(message.data);
break;
case "chart.stop":
clearReplayAndOverlays();
break;
case "selector.closeResult":
clearReplayAndOverlays();
break;
case "nav.click":
onEmbedNav(message.data.href); // string
break;
case "news.eventClicked":
onNewsEvent(message.data.event, message.data.similarDetails);
break;
case "news.selectedEventPayloads":
persistSelectedNewsEvents(message.data); // NewsEvent[]
break;
case "news.back":
case "news.backToSimilarEvents":
clearNewsReplayAndEventLine();
break;
case "news.iframeReady":
onEmbedReady();
break;
case "news.analyzeImpact":
onNewsAnalyzeImpact();
break;
case "pattern.classicPatternSelected":
drawPatternRange(message.data?.pattern);
break;
case "pattern.clearClassicPatternSelected":
clearPatternRange();
break;
case "pattern.custom_pattern_selected":
drawTrackedPatternRange(message.data?.pattern);
break;
case "pattern.clear_custom_pattern_selected":
clearTrackedPatternRange();
break;
}
});
name messages (embed → parent)
name | When it fires | Payload | Host action |
|---|---|---|---|
auth.signIn | Sign-in completes inside the iframe (email, password, or OAuth) | { name: "auth.signIn", data: { idToken: string } } | Store data.idToken as iccandle_token for Bearer API calls |
selector.loading | Scan or AI prep starts or finishes | { name: "selector.loading", data: { isLoading: boolean } } | Show / hide loading UI on your chart or page |
chart.play | User starts replay from news or chart analysis flows | { name: "chart.play", data: { isReplay: true, predictCandles?: Candle[] | null, playEndTimestamp: number | null, selectedCandles: Candle[] | null } } | Inject replay bars / generated candles and block live ticks until playEndTimestamp |
chart.stop | Replay is cleared or closed | { name: "chart.stop", data: { isReplay: false, predictCandles?: Candle[] | null, playEndTimestamp: null, selectedCandles: null } } or { name: "chart.stop", data: null } | Clear replay candles and replay-specific overlays |
selector.closeResult | User closes the active result / analysis panel | { name: "selector.closeResult", data: null } | Clear replay and result-specific overlays |
nav.click | User clicks header navigation inside the embed | { name: "nav.click", data: { href: string } } | Optional — react to tab changes (for example, hide chart chrome on /news) |
news.eventClicked | User selects a calendar / economic event | { name: "news.eventClicked", data: { event: NewsEvent, similarDetails: object | null } } | Center chart on the event, draw a vertical line, and highlight the timescale mark |
news.selectedEventPayloads | User selects scanner news events | { name: "news.selectedEventPayloads", data: NewsEvent[] } | Persist for timescale marks if your chart supports them |
news.back | User leaves news detail or similar-events view | { name: "news.back", data: null } | Remove event line, marks, and news replay overlays |
news.backToSimilarEvents | User navigates back within the news flow | { name: "news.backToSimilarEvents", data: null } | Same as news.back — clear event-specific chart decorations |
news.iframeReady | Similar-events iframe finishes initial load | { name: "news.iframeReady", data: null } | Optional lifecycle hook for host orchestration |
news.analyzeImpact | User requests news impact analysis | { name: "news.analyzeImpact", data: null } | Optional — hook into analysis-specific host UI if needed |
pattern.classicPatternSelected | User selects a classic/common pattern match | { name: "pattern.classicPatternSelected", data: { pattern: object } } | Highlight the compared pattern date range |
pattern.clearClassicPatternSelected | User clears that classic/common selection | { name: "pattern.clearClassicPatternSelected", data: null } | Remove the pattern highlight |
pattern.custom_pattern_selected | User selects a tracked pattern | { name: "pattern.custom_pattern_selected", data: { pattern: object } } | Highlight the tracked pattern range |
pattern.clear_custom_pattern_selected | User clears tracked pattern selection | { name: "pattern.clear_custom_pattern_selected", data: null } | Remove the tracked-pattern highlight |
playEndTimestamp is unix seconds — the bar time where replay should stop.
pattern objects come from scan / pattern-tracker results. Your host only needs the start / end timestamps (or equivalent range fields) to draw a highlight, so inspect one live message in devtools and map those fields to your chart API.
Step 4 — Authenticate
App routes require a session. Opening the embed without cookies redirects to /{locale}/sign-in.
- Email / password and OAuth complete inside the iframe (OAuth uses a popup because providers deny framing).
- On success the embed posts
auth.signInwith{ data: { idToken } }. - Store that token as
iccandle_tokenif your host will callcacheCandleor other APIs.
See the Auth workflow sequence diagram on the introduction page.
Step 5 — Run a scan
5a — Cache candles
POST https://scan-service.iccandle.ai/cacheCandle with the selected window:
Authorization: Bearer <iccandle_token>
Content-Type: application/json
{
"candles": [
{ "o": 1.08, "h": 1.09, "l": 1.07, "c": 1.085, "timestamp": 1719878400 }
]
}
| Field | Notes |
|---|---|
o, h, l, c | Open / high / low / close |
timestamp | Unix seconds (bar time) |
Response includes an id — use it as cid in the iframe URL.
5b — Navigate the iframe
const params = new URLSearchParams({
tk: "10", // top_k (default 10)
symbol: "EURUSD",
res: "60", // match resolution
ref_res: "60", // reference resolution for the cached window
cid: candleId, // from cacheCandle
from: String(startTimestampSec * 1000), // ms — news range helper
to: String(endTimestampSec * 1000), // ms — news range helper
theme: "light",
header: "false",
model: "light", // AI results model: light | pro
temperature: "1", // AI results temperature: 0.5 | 1 | 1.5
});
// optional: &fs=EURUSD,GBPUSD &period=<unixSec>
iframe.src = `${EMBED_ORIGIN}/en?${params}`;
Required for a home scan: symbol, res, ref_res, cid. Without them the embed skips /search.
See Search params for every supported query param, defaults, and route-specific notes.
Step 6 — Drive your chart from messages
| Message | Host responsibility |
|---|---|
chart.play / chart.stop | Inject or clear replay candles / generated candles on your chart |
pattern.classicPatternSelected / pattern.custom_pattern_selected | Highlight the compared pattern range |
pattern.clearClassicPatternSelected / pattern.clear_custom_pattern_selected | Remove that range |
news.eventClicked / news.selectedEventPayloads / news.back | News marks, vertical lines, selected-event state, event replay cleanup |
nav.click | Optional — react when the user switches embed tabs (data.href). Not emitted when header=false. |
You own the chart library; the embed only signals intent via postMessage.
Minimal layout
Place your chart and the iframe side by side (or stacked on small screens). Give the iframe a fixed height or a flex child so the results UI can scroll inside the frame.
Prefer header=false in partner embeds so the host chrome stays primary and the iframe focuses on scan results, pattern tracker, and news flows.
Next
See Search params for the scanner URL contract, Window messages for the full message catalog, Data types for shared payloads, and API reference for routes and protocol details.