Web Integration
Embed NeoCharts into a web app via a pre-built, compiled bundle.
Embed NeoCharts into an existing web app — React, Vue, or plain HTML/JS, any stack that
can load a <script> tag.
The SDK is distributed as a pre-built, compiled bundle — your page only implements the
window.NxtChartHost JS object that supplies data and receives trading callbacks, and
dispatches CustomEvents to push live updates.
This is the complete integration — the full method set, alerts, and OI analysis, all inside the Terminal.
How the bundle loads into your page
The bundle is a compiled Flutter web app — flutter.js bootstraps a Flutter engine into the
host <div> you provide, the same mechanism any Flutter web app uses to embed into a page.
If step 4 below is unfamiliar, Flutter's own Embedding a Flutter app into a web
page explains the
general mechanism this bundle relies on — loadEntrypoint, the host element, and the engine
lifecycle.
1. Receive the bundle
Access
NeoCharts is distributed directly to licensed partners, not published to a public CDN. Your
IOURING team sends you the compiled web bundle as a .tar.xz archive — unpack it
(tar -xJf neochart-web-<version>.tar.xz) into a directory containing main.dart.js,
flutter.js, assets/, nxt-chart-host.d.ts, and (self-contained variant only)
canvaskit/. No toolchain of any kind needed on your end; it's already built.
TypeScript
nxt-chart-host.d.ts types window.NxtChartHost and window._flutter for you — add it to
your tsconfig.json's include, or reference it directly
(/// <reference path="./build_web/nxt-chart-host.d.ts" />). Without it, TypeScript reports
Property 'NxtChartHost' does not exist on type 'Window & typeof globalThis' the first time
you assign to window.NxtChartHost.
If your ESLint config enforces no-underscore-dangle, access the global via bracket notation
— window['_flutter'] — instead of window._flutter. It's still fully typed against the
.d.ts above; only the access syntax changes.
Every release ships as two archives — ask your IOURING contact for the one that matches your deployment:
| Archive | Contains canvaskit/? | Typical size | Use when |
|---|---|---|---|
neochart-web-<version>.tar.xz | Yes, bundled locally | ~3.6 MB | Default choice. Works regardless of your network policy — nothing to configure or allowlist. |
neochart-web-<version>-cdn.tar.xz | No | ~1 MB | Only if your deployment's browsers have confirmed, unrestricted outbound access to gstatic.com. |
The -cdn variant depends on Google's CDN at runtime
canvaskit/ is the compiled Skia rendering engine the chart draws with — it isn't optional
code, the chart cannot render without it. The -cdn archive omits the local copy and instead
has the compiled app fetch it from https://www.gstatic.com/flutter-canvaskit/ the first time
it loads in a browser.
If that request is blocked — a corporate firewall, an intranet/air-gapped deployment, or any
network policy that doesn't allowlist Google's CDN — the chart fails silently: no error
dialog, just a blank screen or a stuck loading spinner. Use the plain (self-contained)
archive unless you've explicitly verified gstatic.com is reachable from where the chart
will run.
Add your license key file
Put the neocharts-key.json IOURING sends you at <bundle>/assets/neocharts-key.json, and
keep <bundle>/assets/neocharts_core.wasm from the bundle — it checks the license. Replace the
key file whenever IOURING sends a new one.
Every domain the page runs on must be in your license. *.acme.com covers subdomains only, not
acme.com itself; localhost and 127.0.0.1 always work. If your page sets a
Content-Security-Policy, script-src must allow 'wasm-unsafe-eval'. See
Licensing for details.
2. Serve the bundle
Place the bundle directory somewhere your own web server serves as static files, at a stable
URL your page can load scripts from — e.g. copy it into your app's public//static/
assets directory as part of your build pipeline, or point your server/CDN config at it
directly. The examples below assume it's reachable at /build_web/; use whatever path your
setup actually serves it at.
Don't rely on a relative filesystem path (../build_web) to reach it — most dev servers and
CDNs resolve script src against your page's own served root, not your project's folder
layout, so a relative path that works when you open the file directly can 404 once it's
actually served.
Self-contained bundle: canvaskit/ must also be served at your site root
This only applies to the plain (self-contained) archive, not the -cdn one.
The compiled app looks for its local CanvasKit copy at a path relative to your page's own
URL — not relative to wherever you put the rest of the bundle. Concretely: if your page is
at https://yourapp.com/dashboard, the engine requests https://yourapp.com/canvaskit/...,
regardless of whether the rest of the bundle lives at /build_web/.
So alongside serving the bundle at whatever path you chose, also make canvaskit/ (the
subfolder inside the bundle) reachable at your site's root canvaskit/ path — copy it there
as an extra step in your build pipeline, or add a server/CDN rule that serves it from root.
Skipping this produces a blank chart with 404s for canvaskit.js/canvaskit.wasm in your
browser console — the chart itself gives no error dialog.
3. Implement window.NxtChartHost
Set this up before loading the bundle (step 4) — the engine reads it as soon as it boots.
yourApi, yourBroker, and yourTickFeed below (e.g. yourApi.fetchOHLCV(...)) are
placeholder names standing in for your own broker/market-data code — not real functions this
SDK provides. Replace each with whatever already fetches that data in your app.
window.NxtChartHost = {
// ── Session-constant getters — read once at startup ──────────────────
get symbolInfo() {
return JSON.stringify({
id: 'RELIANCE',
name: 'Reliance Industries',
lotSize: 1,
precision: 2,
tickSize: 0.05,
exchange: 'NSE',
expiry: '', // e.g. '2025-03-27' for a derivative; empty for this equity
strike: '', // e.g. '23000' for an option; empty for this equity
optType: '', // e.g. 'CE' or 'PE' for an option; empty for this equity
weekly: '', // e.g. 'Y' marks a weekly expiry; empty for this equity
undID: '', // e.g. 'NIFTY' for a derivative; empty for this equity
});
},
get underlyingSymbolInfo() {
return ''; // empty for equities
},
get optionSymbols() {
return '[]';
},
get futureSymbols() {
return '[]';
},
get indexSymbols() {
return '[]';
},
get marketTimings() {
// Keyed by exchange code -- add one entry per exchange any symbol
// reachable this session might resolve to (e.g. also 'NFO' once
// options are in play; see the marketTimings reference doc).
return JSON.stringify({
NSE: {
timezone: 'Asia/Kolkata',
sessions: [
['0915-1530'], // Monday
['0915-1530'], // Tuesday
['0915-1530'], // Wednesday
['0915-1530'], // Thursday
['0915-1530'], // Friday
[], // Saturday
[], // Sunday
],
holidays: [],
special: {},
},
});
},
get hasOCO() {
return true;
},
get isMarketOrderSupported() {
return true;
},
get storageKey() {
return `user_${currentUser.id}`; // stable, user-scoped key
},
// Optional theming getters: see /docs/charting/theming
// ── Request/response methods ──────────────────────────────────────────
loadData(symbolId, from, to, intervalSeconds, requiredBars) {
return yourApi
.fetchOHLCV({ symbolId, from, to, interval: intervalSeconds })
.then(JSON.stringify);
},
fetchOptionDetails(underlyingSymbolId) {
return yourApi.fetchOptionChain(underlyingSymbolId);
},
chartTopOptions() {
return yourApi.fetchTopOptions();
},
atmSymbols() {
return Promise.resolve(
JSON.stringify(['NIFTY24DEC24000CE', 'NIFTY24DEC24000PE']),
);
},
searchSymbols(query) {
return yourApi.searchSymbols(query);
},
fetchOIAnalysis(underlyingSymbolId, expiry, timeFrom, timeTo) {
return yourApi.fetchOIAnalysis(underlyingSymbolId, expiry, timeFrom, timeTo);
},
fetchOIChange(underlyingSymbolId, expiries, timeFrom, timeTo) {
return yourApi.fetchOIChange(underlyingSymbolId, expiries, timeFrom, timeTo);
},
fetchOI(underlyingSymbolId, expiries) {
return yourApi.fetchOI(underlyingSymbolId, expiries);
},
fetchPcrIntraday() {
return yourApi.fetchPcrIntraday();
},
fetchAtmStraddleIntraday() {
return yourApi.fetchAtmStraddleIntraday();
},
fetchAtmIvIntraday() {
return yourApi.fetchAtmIvIntraday();
},
fundsData() {
return yourApi.fetchFunds();
},
// ── Parameterized replacements (xxxFor) ─────────────────────────────────
// Scoped to an explicit id instead of "whatever's currently charted."
// `underlyingSymbolInfoFor` is required — the SDK calls it on every chart
// load. The other 7 aren't called yet (see the reference table below),
// but still need a real body: `implements ChartInterface` on every
// platform won't compile without one.
underlyingSymbolInfoFor(symbolId) {
return yourApi.fetchUnderlyingSymbolInfo(symbolId); // nullable
},
optionSymbolsFor(underlyingId) {
return yourApi.fetchOptionSymbols(underlyingId);
},
futureSymbolsFor(underlyingId) {
return yourApi.fetchFutureSymbols(underlyingId); // nullable
},
atmSymbolsFor(underlyingId) {
return yourApi.fetchAtmSymbols(underlyingId); // nullable
},
chartTopOptionsFor(underlyingId) {
return yourApi.fetchTopOptions(underlyingId);
},
fetchPcrIntradayFor(underlyingId) {
return yourApi.fetchPcrIntraday(underlyingId);
},
fetchAtmStraddleIntradayFor(underlyingId) {
return yourApi.fetchAtmStraddleIntraday(underlyingId);
},
fetchAtmIvIntradayFor(underlyingId) {
return yourApi.fetchAtmIvIntraday(underlyingId);
},
// ── Trading actions & lifecycle — fire-and-forget ──────────────────────
placeOrder(params) {
yourBroker.placeOrder(JSON.parse(params));
},
modifyOrder(params) {
yourBroker.modifyOrder(JSON.parse(params));
},
cancelOrder(orderID) {
yourBroker.cancelOrder(orderID);
},
placeOCOOrder(params) {
yourBroker.placeOCOOrder(JSON.parse(params));
},
modifyOCOOrder(params) {
yourBroker.modifyOCOOrder(JSON.parse(params));
},
cancelOCOOrder(groupId) {
yourBroker.cancelOCOOrder(groupId);
},
groupAdjustOrders(params) {
yourBroker.groupAdjustOrders(JSON.parse(params));
},
createAlert(params) {
yourBroker.createAlert(JSON.parse(params));
},
modifyAlert(params) {
yourBroker.modifyAlert(JSON.parse(params));
},
deleteAlert(alertId) {
yourBroker.deleteAlert(alertId);
},
closeRequested() {
// Called when the chart wants to be dismissed (e.g. an in-chart close
// action) — decide what that means for your page (unmount, navigate
// away, etc.)
yourApp.closeChart();
},
// ── Market data subscription control ────────────────────────────────────
subscribeMarketData(symbolsJson) {
yourTickFeed.subscribe(JSON.parse(symbolsJson));
},
unsubscribeMarketData() {
yourTickFeed.unsubscribeAll();
},
};The tables below are the full member reference — types, exact shapes, and links to the wire-format pages.
Session-constant getters (read once at startup)
null is accepted anywhere you see string | null below, but always return the empty form
shown in the example above instead — see Always return empty, never
null for why.
| Getter | Type | Shape |
|---|---|---|
symbolInfo | string | JSON — see symbolInfo |
underlyingSymbolInfo | string | null | same shape as symbolInfo |
optionSymbols | string | JSON array of symbolInfo-shaped objects |
futureSymbols | string | null | JSON array |
indexSymbols | string | null | JSON array — populating this hides the Volume indicator for symbols in the list (spot indices have no traded volume) |
marketTimings | string | JSON — see marketTimings |
hasOCO | boolean | |
isMarketOrderSupported | boolean | |
storageKey | string | stable, user-scoped — see Chart Persistence for what's stored against it and how |
Optional theming getters
chartColors, primaryColor, onPrimaryColor and forcedBrightness — see
Theming.
Request/response methods
| Method | Returns |
|---|---|
loadData(symbolId, from, to, intervalSeconds, requiredBars) | Promise<string> (JSON bars, oldest-first) |
fetchOptionDetails(underlyingSymbolId) | Promise<string> (JSON array) |
chartTopOptions() | Promise<string> (JSON array) |
atmSymbols() | Promise<string | null> |
searchSymbols(query) | Promise<string> (JSON array) |
fetchOIAnalysis(underlyingSymbolId, expiry, timeFrom, timeTo) | Promise<string | null> |
fetchOIChange(underlyingSymbolId, expiries, timeFrom, timeTo) | Promise<string | null> |
fetchOI(underlyingSymbolId, expiries) | Promise<string | null> |
fetchPcrIntraday() | Promise<string> (JSON array) |
fetchAtmStraddleIntraday() | Promise<string> (JSON) |
fetchAtmIvIntraday() | Promise<string> (JSON array) |
fundsData() | Promise<string> (JSON — see fundsData) |
Parameterized replacements (xxxFor)
Scoped to an explicit id instead of the ambient members above. underlyingSymbolInfoFor is
called on every chart load — implement it before shipping. The other 7 aren't called by the
SDK yet, but still need a real body to compile; see each method's own page for the "not yet
called" detail.
| Method | Returns | Docs |
|---|---|---|
underlyingSymbolInfoFor(symbolId) | Promise<string | null> | underlyingSymbolInfoFor |
optionSymbolsFor(underlyingId) | Promise<string> (JSON array) | optionSymbolsFor |
futureSymbolsFor(underlyingId) | Promise<string | null> | futureSymbolsFor |
atmSymbolsFor(underlyingId) | Promise<string | null> | atmSymbolsFor |
chartTopOptionsFor(underlyingId) | Promise<string> (JSON array) | chartTopOptionsFor |
fetchPcrIntradayFor(underlyingId) | Promise<string> (JSON array) | fetchPcrIntradayFor |
fetchAtmStraddleIntradayFor(underlyingId) | Promise<string> (JSON) | same as above |
fetchAtmIvIntradayFor(underlyingId) | Promise<string> (JSON array) | same as above |
Trading actions & lifecycle (fire-and-forget)
| Method | Params |
|---|---|
placeOrder(params) | JSON string — see placeOrder params |
modifyOrder(params) | JSON string — same shape as placeOrder |
cancelOrder(orderID) | string |
placeOCOOrder(params) | JSON string — see placeOCOOrder params |
modifyOCOOrder(params) | JSON string — same shape as placeOCOOrder |
cancelOCOOrder(groupId) | string |
groupAdjustOrders(params) | JSON string — see groupAdjustOrders params |
createAlert(params) | JSON string — see createAlert params |
modifyAlert(params) | JSON string — same shape as createAlert |
deleteAlert(alertId) | string |
closeRequested() | — called when the chart wants to be dismissed (e.g. an in-chart close action); decide what that means for your page (unmount, navigate away, etc.) |
Market data subscription control
subscribeMarketData(symbolsJson) / unsubscribeMarketData() — called when the chart starts
or stops needing live ticks for the given symbol(s). subscribeMarketData fires with the
complete current set every time it changes (tile switches, new positions, indicator
settings, etc.), not just once at session start — implement it as a replace, never an
accumulate, or you'll leak stale symbols. unsubscribeMarketData means "no longer need any
live data at all" (session teardown), not "the set changed."
4. Load the bundle
<div id="nxtchart-mount" style="width: 100%; height: 100%;"></div>
<script src="/build_web/flutter.js" defer></script>
<script>
window.addEventListener('load', function () {
_flutter.loader.loadEntrypoint({
entrypointUrl: '/build_web/main.dart.js',
onEntrypointLoaded: async function (engineInitializer) {
const appRunner = await engineInitializer.initializeEngine({
hostElement: document.querySelector('#nxtchart-mount'),
assetBase: '/build_web/',
});
await appRunner.runApp();
},
});
});
</script>In a component-based framework (React shown), you generally can't drop a raw <script> tag
inside a component and expect it to behave the same way. Add flutter.js once to your app's
HTML shell instead — public/index.html for Create React App, app/layout.tsx or
pages/_document.tsx for Next.js — then boot the engine once and give each mount of your
component its own chart view, using Flutter's multi-view embedding:
import { useEffect, useRef } from 'react';
// The engine can only be booted once per page, so every mount shares this promise.
let chartApp = null;
function bootChartEngine() {
chartApp ??= new Promise((resolve) => {
// Bracket notation sidesteps `no-underscore-dangle` -- `_flutter` is
// Flutter's own bootstrap global (from flutter.js) and can't be renamed.
window['_flutter'].loader.loadEntrypoint({
entrypointUrl: '/build_web/main.dart.js',
onEntrypointLoaded: async (engineInitializer) => {
const appRunner = await engineInitializer.initializeEngine({
assetBase: '/build_web/',
multiViewEnabled: true,
});
resolve(await appRunner.runApp());
},
});
});
return chartApp;
}
function NeoChart() {
const hostRef = useRef(null);
useEffect(() => {
// Your step 3 implementation, built for the current user's session.
window.NxtChartHost = createNxtChartHost();
let removeView = null;
let unmounted = false;
bootChartEngine().then((app) => {
if (unmounted || !hostRef.current) return;
const viewId = app.addView({ hostElement: hostRef.current });
removeView = () => app.removeView(viewId);
});
return () => {
unmounted = true;
removeView?.();
};
}, []);
return <div ref={hostRef} style={{ width: '100%', height: '100%' }} />;
}One view per session
Each addView builds a fresh chart that reads window.NxtChartHost as the view is created;
removeView disposes that chart along with its subscriptions and state. Mount NeoChart when
a user's session starts and unmount it when the session ends (logout, account switch) — the
next session then gets its own chart instead of the previous session's, without a page reload.
The engine itself stays loaded, so a new view doesn't download anything again.
Keep it mounted while navigating within a session
Unmounting ends that chart — drawings, zoom and open panels are rebuilt from scratch on the
next mount. If your router would otherwise unmount NeoChart for an in-session navigation —
e.g. from /chart to /dashboard — hide it with CSS instead of removing it from the tree.
Keep it at full size while hidden (e.g. position: fixed; inset: 0; opacity: 0; pointer-events: none; z-index: -1); avoid display: none, which collapses the chart's canvas
to 0×0 and can leave it blank until the next repaint once it's shown again.
React 18 Strict Mode double-invokes effects
In development, Strict Mode runs the effect, its cleanup, then the effect again. The shared
chartApp promise keeps the engine from booting twice, and the cleanup removes any view the
first run added, so you end up with exactly one chart.
Define window.NxtChartHost first
Set window.NxtChartHost (step 3) before calling addView (or, with the single-page
snippet above, before loadEntrypoint) — the chart reads its session-constant getters
synchronously as the view is created. If any of these getters depend on data you fetch
asynchronously (e.g. symbolInfo built from an API call), wait for that fetch before adding
the view — don't assign window.NxtChartHost with getters that aren't ready yet.
5. Push live updates
Dispatch a CustomEvent on window whenever new data is available. detail is the JSON
string for the matching schema on the corresponding Trading page.
| Event name | Wire format |
|---|---|
nxtchart:positions | positionsStreamer |
nxtchart:orders | ordersStreamer |
nxtchart:ocoOrders | ocoOrdersStreamer |
nxtchart:actionFeedback | actionFeedbackStreamer |
nxtchart:alerts | alertsStreamer |
nxtchart:marketData | market data ticks — see Live ticks |
window.dispatchEvent(new CustomEvent('nxtchart:orders', { detail: JSON.stringify(orders) }));Troubleshooting
| Symptom | Fix |
|---|---|
Blank page, _flutter is not defined | flutter.js's <script> tag didn't load before loadEntrypoint ran — check the build output path |
| Blank chart — init never completes | Ensure every session-constant getter on window.NxtChartHost returns a value |
| Blank chart — no candles | Verify loadData resolves valid OHLCV JSON ordered oldest-first |
| Orders/positions not shown | Dispatch nxtchart:orders/nxtchart:positions before or shortly after loadData resolves |
| Alerts not shown | Dispatch nxtchart:alerts whenever your alert list changes |
404s for /build_web/* | The bundle isn't actually being served at that URL — check where you copied/pointed it in step 2 |
| No data or live updates after logging out and back in without a page reload | The previous session's chart is still mounted — give each session its own view (addView on login, removeView on logout), see step 4 |
Uncaught error thrown from a getter (e.g. symbolInfo) | A getter depends on async data that hasn't loaded yet — delay loadEntrypoint until your own init finishes |
| "Your NeoCharts license is invalid or has expired." instead of the chart | neocharts-key.json is missing from <bundle>/assets/, was edited (it is signed), the page's domain is not in your license, assets/neocharts_core.wasm wasn't deployed, or your CSP blocks 'wasm-unsafe-eval' — see Licensing |
Blank chart, 404s for canvaskit.js/canvaskit.wasm at your site root (self-contained archive only) | canvaskit/ needs to be served at your site root in addition to wherever the rest of the bundle lives — see the callout in step 2 |
Terminal
Web only
The terminal is only available through the compiled web bundle.
The Terminal is a multi-chart trading layout — the web SDK's landing screen. Every panel below is built from the same screens/blocs the single-chart and scalper views already use — the terminal only changes how they're arranged on screen.
Tile grid
2–8 independently-symboled, resizable chart tiles. Each tile owns its own chart state (bloc, drawing controller, symbol) — changing one tile's symbol, indicators, or drawings never affects the others.
| Action | How |
|---|---|
| Add a tile | The grid's "+" control (up to 8) |
| Remove a tile | Each tile's own close button (down to 2) |
| Activate a tile | Tap/click anywhere on it — the left rail and its drawing tools then apply to that tile |
| Resize | Drag the divider between tiles |
Left rail — drawing tools
A persistent icon rail, grouped into flyouts rather than one long flat list. Applies to whichever tile is currently active.
| Icon | Opens | Tools |
|---|---|---|
| Cursor | Flyout | Cross (deactivates the current drawing tool) |
| Lines & Channels | Flyout | Trend Line, Extended Line, Ray, Arrow, Horizontal Line, Horizontal Ray, Vertical Line, Cross Line, Parallel Channel |
| Fibonacci | Flyout | Fibonacci Retracement |
| Position | Flyout | Long Position, Short Position |
| Shapes | Flyout | Rectangle, Circle, Path |
| Text & Notes | Flyout | Text, Anchored Text |
| Measure | Direct — activates immediately | Measure |
See Indicators & Drawing Tools for what each tool does.
Right dock
A toggle strip beside the tile grid — tapping the active icon again closes the dock.
| Panel | Content |
|---|---|
| Option Chain | Full option chain for the active tile's underlying |
| Top 5 | Top options ranked by volume |
| Analysis | PCR, ATM straddle, and ATM IV intraday views |
Bottom pane — "My Account"
A full-width account panel with its own tab strip, each tab a count-badged table.
| Tab | Columns |
|---|---|
| Positions | Symbol, Qty, Avg, LTP, P&L |
| Orders | Symbol, Qty, Price, Trigger Price, Order Type, Status, Time — OCO orders show a summary row that expands to its Target/Stop Loss legs |
| GTT Orders | Symbol, Qty, Target (trigger + price), Stop Loss (trigger + price), Product |
| Alerts | Symbol, Status, Trigger Price, Created |
GTT Orders surfaces the same OCO (one-cancels-other) data the Orders tab's expandable rows
use — ocoOrdersStreamer — as its own dedicated table.
See Trading for exact NxtChartHost member shapes and wire formats
(including what drives the terminal's own Orders/Positions/Alerts tables), and
How It Works for the data and trade flow.