Academy · Developer Guide

Build Crypto Price Webhooks on the CMC WebSocket API

CoinMarketCap APIUpdated 2 September 2026 · 11 min read
Build Crypto Price Webhooks on the CMC WebSocket API, shown as a space shuttle on a cobalt grid beside live percentage-change readouts.

Key Takeaways

  • The CoinMarketCap API delivers real-time prices over a WebSocket stream. If your architecture expects webhooks, the pattern is a short bridge service that turns stream messages into HTTP deliveries on your own infrastructure.
  • The bridge is about fifty lines of Node.js: one socket in, one filter, one HTTP POST out. Because you own that layer, the payload shape, the retry policy and the alert thresholds are all yours to define.
  • Messages bill at 0.025 credits each. A single top-500 asset streaming continuously costs about 12,960 credits a month, so a Startup plan's 450,000 credits carries roughly 34 of them, or about 100 lower-ranked assets on the slower cadence.
  • The WebSocket API is a beta feature and carries no SLA cover for now, which makes reconnection handling part of the first version rather than a later addition.

Webhooks are popular because the receiving side is so little work: no connection to hold open, no client to supervise, just an endpoint that gets called when something happens. Real-time market data moves in the other direction, over a persistent stream, because that is what keeps latency low across a wide watchlist. Bridging the two is a small service, and building it yourself has a real advantage: you decide what the payload looks like, when it fires and how retries behave.

How the CoinMarketCap Stream Works

Real-time prices are delivered by the CoinMarketCap WebSocket API, added in version 3.0.3 on June 5, 2026. You open one persistent connection, subscribe to the assets you care about, and updates are pushed to you as they land. There is no callback URL to register, which is the one architectural difference to plan around: the stream supplies the events, and the delivery layer sits on your side.

Three properties of the stream shape everything downstream:

  • Access starts at Startup. The stream is available on Startup, Growth, Professional and Enterprise plans. Startup is $95 a month billed monthly, or $79 a month billed annually, and includes 450,000 credits.
  • Billing is per message received. Each message costs 0.025 credits as it arrives, and your filter runs after that point. So the monthly total follows what you subscribe to rather than how many alerts you end up sending.
  • Beta, with no SLA cover. The stream is a beta feature, which places it outside the Service Level Agreement for now.

Cadence

Market updates arrive roughly every 5 seconds for the top 500 cryptocurrencies by rank, and roughly every 15 seconds for everything else. The split affects both how quickly an alert can fire and what a given watchlist costs to run.

What You Are Building

One Node.js process that holds a WebSocket connection to the CoinMarketCap stream, watches the assets you subscribe to, and POSTs a JSON payload to any URL you configure whenever a price crosses a threshold you set. The threshold logic is the part most teams customize: per-asset percentages, absolute price levels, volume moves read off the same message, or anything else the payload carries. The connection and delivery skeleton does not change when you do.

Setup

You need Node.js 18 or newer, one dependency, and an API key on a plan that includes WebSocket access.

bash
mkdir price-webhook-bridge && cd price-webhook-bridge
npm init -y && npm pkg set type=module
npm install ws

export CMC_API_KEY="your-api-key"
export WEBHOOK_URL="https://your-service.example/hooks/price"

Connect and Authenticate

Every channel, market and on-chain alike, shares the one endpoint. Authentication uses the same X-CMC_PRO_API_KEY header as the REST API, passed when the connection opens.

javascript
import WebSocket from 'ws';

const ws = new WebSocket('wss://pro-stream.coinmarketcap.com/v1', {
  headers: { 'X-CMC_PRO_API_KEY': process.env.CMC_API_KEY }
});

ws.on('open',  () => console.log('stream connected'));
ws.on('error', (err) => console.error('socket error', err.message));
ws.on('close', (code) => {
  console.error('socket closed', code);
  process.exit(1); // let the supervisor restart it
});

Why this runs server-side

The browser WebSocket constructor cannot set request headers, so a header-authenticated connection cannot be opened from front-end code. The stronger reason to keep the bridge on a server you control is the API key: anything shipped to a browser is readable by whoever loads the page.

Subscribe to the Price Channel

Market prices come from the market@crypto_latest_price channel. One subscribe frame names the channel and the assets you want, and the stream starts pushing updates for them. Assets are identified by numeric CoinMarketCap ID: Bitcoin is 1, Ethereum is 1027.

The frame carries crypto_ids inside a params object, as an array of numbers. Four client methods are available: subscribe, unsubscribe, unsubscribe_all and ping. The keepalive interval is read from the session rather than hardcoded: the bridge takes it from the welcome frame that opens the connection and falls back to 10 seconds if no interval is supplied, so it keeps working whichever value the stream sends.

javascript
const WATCHED = [1, 1027]; // Bitcoin, Ethereum
let pingTimer;

ws.on('open', () => {
  ws.send(JSON.stringify({
    id: 1,
    method: 'subscribe',
    channel: 'market@crypto_latest_price',
    params: { crypto_ids: WATCHED }
  }));
});

ws.on('message', (raw) => {
  let msg;
  try { msg = JSON.parse(raw); } catch { return; }
  if (msg.type !== 'welcome') return;
  clearInterval(pingTimer);
  pingTimer = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ id: 1, method: 'ping' }));
    }
  }, msg.ping_interval_ms ?? 10_000);
});

Filter and Deliver the Webhook

Several frame types share the socket, dispatched on a type field: welcome when the connection is established, ack confirming a subscription, data for updates, plus error and pong. The bridge below acts on data frames and logs error frames, which is what surfaces a rejected subscription rather than leaving it to look like a quiet market.

A data frame nests the payload under data, alongside channel, params and a ts timestamp in epoch milliseconds. Inside it, fourteen fields are documented; the ones this bridge uses are cid for the CoinMarketCap ID and p for price. Also available are vu for volume, mc for market cap, cs for circulating supply, percentage moves over several windows including p24h, and fdv24h.

json
{
  "type": "data",
  "channel": "market@crypto_latest_price",
  "params": { "crypto_ids": 1 },
  "data": {
    "cid": 1,
    "p": 81187.93,
    "vu": 29932025976.79,
    "mc": 1626089237758.01,
    "cs": 20028706,
    "p24h": 0.534
  },
  "ts": 1778663880111
}

The bridge keeps the last price it saw per asset and fires your webhook when a move clears the threshold. The reference price updates on every message rather than only on delivery, so the threshold measures the move between consecutive updates. Anchoring it to a stored baseline instead gives you the move since the last alert, if that is the behavior you want.

javascript
const THRESHOLD = 0.01; // 1% move between updates
const last = new Map();

ws.on('message', async (raw) => {
  let msg;
  try { msg = JSON.parse(raw); } catch { return; }

  if (msg.type === 'error') return console.error('stream error', msg);
  if (msg.type !== 'data') return; // welcome, ack, pong

  const { cid, p: price } = msg.data ?? {};
  if (cid == null || price == null) return;

  const prev = last.get(cid);
  last.set(cid, price);
  if (prev == null) return;

  const moved = (price - prev) / prev;
  if (Math.abs(moved) < THRESHOLD) return;

  await deliver({
    source: 'cmc-price-bridge',
    id: cid,
    price,
    previous: prev,
    moved,
    at: new Date(msg.ts).toISOString()
  });
});

Delivery endpoints are occasionally unavailable, so the version below wraps the POST in a retry with exponential backoff and sends an idempotency key alongside it.

javascript
async function deliver(payload, attempt = 1) {
  try {
    const res = await fetch(process.env.WEBHOOK_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Idempotency-Key': `${payload.id}-${payload.at}`
      },
      body: JSON.stringify(payload)
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
  } catch (err) {
    if (attempt >= 5) return console.error('delivery failed', payload, err.message);
    const wait = 2 ** attempt * 250; // 500ms, 1s, 2s, 4s
    setTimeout(() => deliver(payload, attempt + 1), wait);
  }
}

Keying on (id, at) lets the receiving handler recognize a duplicate, so a retried delivery does not fire the same action twice. Over a few weeks of running, retries are common enough to be worth designing for.

What It Costs to Run

Because billing is per message received rather than per alert delivered, the cost is set by what you subscribe to rather than by how selective your filter is, which makes it straightforward to work out in advance. At 0.025 credits per message, over a 30-day month:

Subscription Messages per month Credits per month Assets within a Startup plan's 450,000
One top-500 asset, ~5s cadence 518,400 12,960 about 34
One lower-ranked asset, ~15s cadence 172,800 4,320 about 100

Two things follow from that. Cost tracks the size of the watchlist rather than market activity, so a quiet week and a volatile one come to the same figure. And for lower-ranked assets the 100-subscriptions-per-connection cap and the credit budget land in roughly the same place, so on that cadence both limits tend to arrive together.

If you only need one slow-moving asset checked occasionally, polling the REST API on a timer is cheaper and simpler. The stream earns its cost when the watchlist widens or the freshness requirement tightens, because you stop paying for requests that return nothing new.

Running It in Production

Connection limits. The stream allows up to 10 concurrent connections, each supporting up to 100 subscriptions. Most alerting workloads fit comfortably inside one connection, and the cap is useful to know when you are planning how far a single key needs to scale.

Reconnection. Long-lived connections drop, so the bridge exits on close and lets a supervisor such as systemd or a container runtime restart it. Give the supervisor a short restart delay, for example systemd's RestartSec, so reconnection backs off instead of looping instantly against a stream that may still be unavailable. Subscriptions do not survive a reconnect, which is why the subscribe frame sits inside the open handler and runs again on every connect.

Keepalive. The bridge pings on the interval the welcome frame supplies, falling back to 10 seconds when none is given, and the stream answers each ping with pong. Tracking those replies is what distinguishes a live connection from a half-open one that still reports itself as open, and closing a half-open socket deliberately hands it back to the supervisor.

Alert state. The last map lives in memory, so a restart clears the reference prices and the first message per asset after a restart sets a new baseline rather than firing. That is usually the behavior teams want after a deploy. Persisting the baseline outside the process changes it.

Watching DEX Pools

The same socket and the same delivery half serve on-chain data; only the subscribe frame changes. The documented on-chain channels are onchain@token_agg_event, onchain@transaction, onchain@liquidity_event, onchain@kline, onchain@token_metric, onchain@pool_metric, onchain@unique_trader, onchain@holders_metrics and onchain@holder_wallet_update.

Two of those take a specific identifier rather than an asset ID: onchain@pool_metric requires a pool_address, and onchain@holder_wallet_update requires a wallet_address. Supported chains include Ethereum, BSC, Solana, Base and other EVM-compatible networks, with the two holder channels additionally covering Tron20.

On-chain channels are event-driven rather than periodic, so the cost model changes with them: the total tracks pool activity instead of a fixed cadence. Observed event volume is the figure to budget against here, rather than the per-asset numbers above.

FAQ

Does the CoinMarketCap API support webhooks?

Real-time delivery runs over the WebSocket API rather than HTTP callbacks, so there is no callback URL to register. Teams that want webhook-shaped delivery run the bridge described on this page, which has the side benefit of putting the payload format, the trigger conditions and the retry policy under their own control.

Which plans include WebSocket access?

Startup and above, meaning Startup, Growth, Professional and Enterprise. Startup is $95 a month billed monthly, or $79 a month billed annually, with 450,000 credits included.

What does a price webhook bridge cost to run?

Messages bill at 0.025 credits each. One top-500 asset streaming continuously is about 518,400 messages and 12,960 credits a month, so two assets come to roughly 26,000 credits. Since billing happens on receipt, your filter shapes what you deliver rather than what you spend.

What shape are the subscribe and data frames?

The subscribe frame carries crypto_ids inside a params object, as an array of numbers. Incoming updates nest their payload under data, alongside channel, params and a ts timestamp, so prices are read at msg.data.p. Both shapes are shown in full in the sections above; an ack frame confirms a subscription was accepted, and logging frames where type is error surfaces a rejected one.

Can I open the stream directly from a browser?

The bridge in this guide authenticates with a header, and the browser WebSocket constructor cannot set request headers, so this code runs server-side. That is where you would want it regardless, since it keeps your API key out of anything you ship to a client.

Is the stream covered by an SLA?

Not yet. Beta features sit outside the Service Level Agreement, so the bridge in this guide handles reconnection itself.

Why not just poll the REST API on a timer?

For one asset with a loose freshness requirement, polling is the simpler choice. The stream becomes worthwhile as the watchlist widens or the latency target tightens, because cost tracks updates delivered rather than requests made whether or not anything changed.


This article contains links to third-party websites or other content for information purposes only ("Third-Party Sites"). The Third-Party Sites are not under the control of CoinMarketCap, and CoinMarketCap is not responsible for the content of any Third-Party Site, including without limitation any link contained in a Third-Party Site, or any changes or updates to a Third-Party Site. CoinMarketCap is providing these links to you only as a convenience, and the inclusion of any link does not imply endorsement, approval or recommendation by CoinMarketCap of the site or any association with its operators.

This article is intended to be used and must be used for informational purposes only. It is important to do your own research and analysis before making any material decisions related to any of the products or services described. This article is not intended as, and shall not be construed as, financial advice.

The platforms, protocols, and projects referenced in these articles are used for illustrative purposes only to demonstrate how the CoinMarketCap API can be integrated in practice. CoinMarketCap makes no representation that these projects endorse or are affiliated with CoinMarketCap. If you represent a referenced project and have concerns, please contact us at content@coinmarketcap.com and we will respond promptly.