Skip to content

Embedding

Quickstart

A working chart on your page, then the one thing that decides whether it draws anything. About two minutes, no account, no key.

What you are about to add

An iframe served from our origin. It is not a script bundle: nothing of ours runs in your page’s realm, your CSP gains one frame-src entry, and a bug of ours cannot break your page. The chart itself is what you see below.

A real embed, running on this page. Drag it, zoom it, this is the same document your snippet frames.

1. Paste the snippet

A placeholder plus the loader. The loader's only job is to build the frame and keep its height in step with the chart, data-datafeed names the object you are about to write.

On your page
<div class="vp-chart"
     data-symbol="BTCUSDT"
     data-datafeed="myfeed"
     data-height="480"></div>
<script src="https://view.verticalprop.com/embed.js"></script>

2. Give it data, this is the step that matters

A chart on your site draws YOUR bars, not ours. We ship the charting engine; the market data is yours. An embed on a site that is not ours which names no datafeed does not quietly fall back to our feed, it renders an explanation and a link. That is the one thing worth knowing before you paste anything.

Here is a complete working feed against Binance’s public host, keyless, CORS-open, and runnable from your page on the first try. Copy it as-is and the chart above is what you get.

Complete, working, copy as-is
<div class="vp-chart"
     data-symbol="BTCUSDT"
     data-datafeed="binance"
     data-height="480"></div>
<script src="https://view.verticalprop.com/embed.js"></script>

<script>
  // Chart resolutions → Binance intervals.
  var STEP = {
    '1': '1m', '5': '5m', '15': '15m', '30': '30m',
    '60': '1h', '240': '4h', '1D': '1d', '1W': '1w', '1M': '1M'
  };
  var sockets = {};

  vpEmbed.datafeed('binance', {
    onReady: function () {
      return { supported_resolutions: Object.keys(STEP) };
    },

    resolveSymbol: function (name) {
      return {
        name: name,
        description: name,
        session: '24x7',            // crypto never closes
        timezone: 'Etc/UTC',
        pricescale: 100,            // BTCUSDT quotes to 2 decimal places
        minmov: 1,
        has_intraday: true,
        supported_resolutions: Object.keys(STEP),
        data_status: 'streaming',
        provider: 'Binance'         // shown as attribution on the chart
      };
    },

    getBars: function (symbol, resolution, period) {
      var url = new URL('https://data-api.binance.vision/api/v3/klines');
      url.searchParams.set('symbol', symbol);
      url.searchParams.set('interval', STEP[resolution] || '1d');
      // endTime, NOT startTime: the chart needs `countBack` bars ENDING at
      // `to`, and asking from `from` returns too few whenever the window
      // holds less history than the chart asked for.
      url.searchParams.set('endTime', String(period.to * 1000));
      url.searchParams.set('limit', String(Math.min(1000, period.countBack)));

      return fetch(url).then(function (r) { return r.json(); }).then(function (rows) {
        // Binance already stamps kline open times in MILLISECONDS, which is
        // what a bar wants — even though period.from/to arrived in seconds.
        return rows.map(function (k) {
          return {
            time: k[0],
            open: Number(k[1]), high: Number(k[2]),
            low: Number(k[3]), close: Number(k[4]), volume: Number(k[5])
          };
        });
      });
      // Past the listing date Binance returns [], which is exactly how the
      // chart learns to stop asking for older history.
    },

    subscribeBars: function (symbol, resolution, uid, push) {
      var stream = symbol.toLowerCase() + '@kline_' + (STEP[resolution] || '1d');
      var ws = new WebSocket('wss://data-stream.binance.vision/ws/' + stream);
      ws.onmessage = function (ev) {
        var k = JSON.parse(ev.data).k;
        push({
          time: k.t,
          open: Number(k.o), high: Number(k.h),
          low: Number(k.l), close: Number(k.c), volume: Number(k.v)
        });
      };
      sockets[uid] = ws;
    },

    unsubscribeBars: function (uid) {
      if (sockets[uid]) { sockets[uid].close(); delete sockets[uid]; }
    }
  });
</script>
These exact bytes are injected into a real host page and driven against a real chart by our browser suite on every change, so this cannot rot into an example that no longer works. The three comments inside it name the three ways this integration usually goes wrong.
The loader tag here has no async, and that is deliberate. This snippet calls vpEmbed.datafeed(...) from an inline script, and embed.js assigns that global when it executes. With async the inline script wins the race, throws, and the chart sits on “Loading market data…” forever, which reads exactly like the venue being down. Snippets that only place a <div> keep async and are correct.

3. Then one of these

If you want to…Go to
Understand the datafeed properly, streaming, search, symbol metadataUse your own data
Avoid loading a third-party script at allWithout our script
Draw orders, stops, targets and positions on the chartOrders and positions
Change theme, interval, chart type or hide the chromeURL parameters
Use React, Vue, Svelte, WordPress or a CMS that strips scriptsFramework recipes
Answer a security reviewSecurity model

If nothing drew

Four causes account for nearly all of it, in order of likelihood.

What you seeAlmost always
An explanation naming data-datafeedYou are on your own site with no feed. That is the BYOD rule, step 2.
&ldquo;Loading market data…&rdquo; foreverasync on the loader beside an inline vpEmbed call, or your getBars never resolves.
An empty plot in the window you asked aboutbar.time in seconds. It is MILLISECONDS; period.from/to are seconds.
Nothing at all, no frameYour CMS stripped the <script> tag. Use the plain iframe form.

The full list, with what each one looks like, is on Troubleshooting.