How to integrate a Cryptocurrency API in Webflow Cloud and show live prices without exposing your key

How to integrate a Cryptocurrency API in Webflow Cloud and show live prices without exposing your key

Learn how to build a Webflow Cloud route that hides your CoinGecko API key, caches prices in Key Value Store, and powers a live ticker on your Webflow site.

How to integrate a Cryptocurrency API in Webflow Cloud and show live prices without exposing your key

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

A Webflow Cloud route can keep your crypto API key off the page and serve cached prices that stay inside a free plan's quota.

Most crypto data providers meter every request against an API key. Put that key in a page script, and anyone can copy it from the page source, while every visitor's browser spends your quota directly.

Webflow Cloud runs a Next.js app at a path on your Webflow site, so a server route can hold the key while the browser only talks to your own domain.

In this build, we’ll create a Route Handler that fetches prices from CoinGecko's free Demo plan, caches them in Webflow Cloud's Key Value Store, and serves them to a ticker on your page.

What do you need to integrate a cryptocurrency API in Webflow?

You need four things:

  • A CoinGecko Demo API key: Free from CoinGecko's API pricing page, which lists 10,000 calls a month and requires attribution on the page that shows the data.
  • Node.js 22 and npm: Webflow's bring-your-own-app page states that "Currently, Webflow Cloud supports only the npm package manager."
  • Next.js 15: Pinned when you scaffold, so the project starts on the release line Webflow Cloud supports.
  • A Webflow Cloud project: Connected to your site, with a mount path chosen for the app.

With these prerequisites ready, you can set up the project and integrate the API.

5 steps to integrate a cryptocurrency API in Webflow Cloud

The build is a Next.js app with one cached Route Handler and one client component. The browser never sees the key or the provider's response format.

1. Scaffold the Next.js app with npm

Create the app with npm and pin Next.js 15, then add the two packages the cache needs: the OpenNext Cloudflare helper that exposes Webflow Cloud's storage bindings, and the Workers type definitions.

Run these commands from the folder where you keep projects:

npx create-next-app@15 crypto-prices --ts --app --use-npm --yes
cd crypto-prices
npm install @opennextjs/cloudflare
npm install -D @cloudflare/workers-types

Pinning create-next-app@15 installs the newest Next.js 15 release, which meets Webflow Cloud's version floor without pulling in Next.js 16. The --yes flag accepts the defaults, including the App Router, so the project is ready for a Route Handler.

The OpenNext helper only reads the storage binding; Webflow Cloud supplies the adapter during build, so there is no adapter to configure.

2. Store the key and declare the Key Value Store

Put the key in environment variables, locally in .env.local and in the Webflow Cloud environment for deploys:

  • COINGECKO_API_KEY: Your Demo key. Mark it secret in Webflow Cloud, and never give it the NEXT_PUBLIC_ prefix, because Next.js ships those values to the browser.
  • NEXT_PUBLIC_BASE_PATH: The mount path, such as /prices. Webflow Cloud does not create this variable, and the client component needs it. Leave it empty locally.

CoinGecko still answers without a key, but keyless calls are rate-limited by IP address and share that limit with every other app on the same network, so a missing or misspelled key quietly degrades instead of failing.

Then declare a Key Value Store binding by adding wrangler.json to the project root.

Webflow Cloud assigns the real namespace ID when it deploys:

{
  "kv_namespaces": [{ "binding": "PRICES_KV", "id": "replace-after-deployment" }]
}

3. Build the cached price Route Handler

The route reads one cached snapshot from the Key Value Store and only calls CoinGecko when the snapshot has expired.

Webflow Cloud replaces any Cache-Control header a route sets with private, no-cache, so browser caching can't protect the quota; this server cache keeps the provider to one request per cache window, no matter how many visitors poll.

CoinGecko's simple price endpoint takes symbols and a required vs_currencies, and returns prices keyed by lowercase symbol.

Route implementation

Create app/api/prices/route.ts:

// app/api/prices/route.ts
import { NextResponse } from 'next/server';
import { getCloudflareContext } from '@opennextjs/cloudflare';
import type { KVNamespace } from '@cloudflare/workers-types';

const ALLOWED_SYMBOLS = ['BTC', 'ETH', 'SOL'];
const CACHE_KEY = 'prices:usd';
const CACHE_TTL_SECONDS = 300; // about 8,640 calls a month, inside the Demo plan

type Snapshot = { prices: Record<string, number>; fetchedAt: string };

async function fetchUpstream(): Promise<Snapshot> {
  const url = new URL('https://api.coingecko.com/api/v3/simple/price');
  url.searchParams.set('symbols', ALLOWED_SYMBOLS.join(',').toLowerCase());
  url.searchParams.set('vs_currencies', 'usd');

  const res = await fetch(url, {
    headers: {
      'x-cg-demo-api-key': process.env.COINGECKO_API_KEY ?? '',
      accept: 'application/json',
    },
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) throw new Error(`Upstream returned ${res.status}`);

  const body = (await res.json()) as Record<string, { usd?: number }>;
  const prices: Record<string, number> = {};
  for (const symbol of ALLOWED_SYMBOLS) {
    const usd = body[symbol.toLowerCase()]?.usd;
    if (typeof usd === 'number') prices[symbol] = usd;
  }
  return { prices, fetchedAt: new Date().toISOString() };
}

function getKv(): KVNamespace | undefined {
  try {
    const env = getCloudflareContext().env as unknown as { PRICES_KV?: KVNamespace };
    return env.PRICES_KV;
  } catch {
    return undefined; // no bindings under plain `next dev`
  }
}

export async function GET(request: Request) {
  if (!process.env.COINGECKO_API_KEY) {
    return NextResponse.json({ error: 'Price service is not configured' }, { status: 500 });
  }

  const requested = [
    ...new Set(
      (new URL(request.url).searchParams.get('symbols') ?? '')
        .split(',')
        .map((s) => s.trim().toUpperCase())
        .filter((s) => ALLOWED_SYMBOLS.includes(s)),
    ),
  ];
  if (requested.length === 0) {
    return NextResponse.json({ error: 'No supported symbols requested' }, { status: 400 });
  }

  const kv = getKv();
  let snapshot = kv ? await kv.get<Snapshot>(CACHE_KEY, 'json') : null;
  if (!snapshot) {
    try {
      snapshot = await fetchUpstream();
      await kv?.put(CACHE_KEY, JSON.stringify(snapshot), { expirationTtl: CACHE_TTL_SECONDS });
    } catch (err) {
      const message = err instanceof Error ? err.message : 'Upstream request failed';
      return NextResponse.json({ error: message }, { status: 502 });
    }
  }

  const prices = Object.fromEntries(requested.map((s) => [s, snapshot.prices[s] ?? null]));
  return NextResponse.json({ symbols: requested, prices, fetchedAt: snapshot.fetchedAt });
}

The route is public because a price ticker has no signed-in user, and the allowlist means a visitor can only request the coins you choose. All coins share one cache key, so a busy page costs one Key Value Store write per cache window rather than one per coin.

Leave export const runtime = 'edge' out of the file: the bring-your-own-app page still suggests it for API routes, but the OpenNext Cloudflare adapter behind Webflow Cloud does not support that Next.js runtime target.

Run npm run dev and request http://localhost:3000/api/prices?symbols=BTC,ETH. Under next dev, there is no Key Value Store, so each request reaches CoinGecko; the JSON should list a price for each coin and a fetchedAt timestamp.

4. Render live prices in a client component

The ticker polls the route every two minutes. CoinGecko's Demo data refreshes about once a minute, and the route caches for five minutes, so polling faster would only repeat the same snapshot.

The bring-your-own-app page says "Client-side fetch calls must manually include the base path to correctly reach your endpoints," so the component prefixes every request with the mount path.

Create app/components/PriceTicker.tsx:

// app/components/PriceTicker.tsx
'use client';

import { useEffect, useState } from 'react';

type PriceResponse = {
  symbols: string[];
  prices: Record<string, number | null>;
  fetchedAt: string;
};

const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
const REFRESH_MS = 120_000;

const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2 });
const usdSmall = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumSignificantDigits: 4 });

function formatValue(value: number | null | undefined): string {
  if (typeof value !== 'number') return 'n/a';
  return value >= 1 ? usd.format(value) : usdSmall.format(value);
}

export default function PriceTicker({ symbols }: { symbols: string[] }) {
  const key = symbols.join(',');
  const [data, setData] = useState<PriceResponse | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function load() {
      try {
        const res = await fetch(`${BASE_PATH}/api/prices?symbols=${encodeURIComponent(key)}`);
        const body = await res.json();
        if (!res.ok) throw new Error(body.error ?? `Request failed with ${res.status}`);
        if (!cancelled) {
          setData(body as PriceResponse);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) setError(err instanceof Error ? err.message : 'Unknown error');
      }
    }

    load();
    const interval = setInterval(load, REFRESH_MS);
    return () => {
      cancelled = true;
      clearInterval(interval);
    };
  }, [key]);

  if (error) return <p role="alert">Prices unavailable: {error}</p>;
  if (!data) return <p>Loading prices…</p>;

  return (
    <div>
      <ul>
        {data.symbols.map((symbol) => (
          <li key={symbol}>
            <strong>{symbol}</strong>: {formatValue(data.prices[symbol])}
          </li>
        ))}
      </ul>
      <small>
        Updated {new Date(data.fetchedAt).toLocaleTimeString()}. Data provided by{' '}
        <a href="https://www.coingecko.com/en/api">CoinGecko</a>. Prices may be delayed and are not
        financial advice.
      </small>
    </div>
  );
}

Prices under a dollar use significant digits, so a coin worth less than a cent shows its real value instead of rounding to zero. The attribution line meets CoinGecko's requirement for the free plan, and the note about delays keeps the ticker from reading as a trading quote.

Replace the starter page at app/page.tsx so it renders the ticker:

// app/page.tsx
import PriceTicker from './components/PriceTicker';

export default function Home() {
  return (
    <main>
      <h1>Live crypto prices</h1>
      <PriceTicker symbols={['BTC', 'ETH', 'SOL']} />
    </main>
  );
}

Reload http://localhost:3000 and you should see one row per coin with a formatted price and the attribution line.

5. Deploy to Webflow Cloud and mount the app on your site

Create a Webflow Cloud project connected to your repository and choose a mount path, following Webflow's bring-your-own-app guide. Add both variables from step 2 to the Webflow Cloud environment, with the key marked secret and NEXT_PUBLIC_BASE_PATH set to the exact mount path.

Webflow's environment variables page states that "Both secret and non-secret environment variables are available to your application's build process and to the deployed application at runtime," so Next.js can inline the public value during the build.

Deploy after the variables are saved. Webflow Cloud creates the Key Value Store namespace from wrangler.json on the first deploy. In the Designer, add a navbar or footer link to the mount path so visitors can reach the ticker from your existing pages.

Open your site at the mount path, and you should see the ticker. In the browser's network tab, the request to the mount path plus /api/prices should return 200, and repeated requests within five minutes should return the same fetchedAt value, which confirms the cache is working.

What causes a Webflow Cloud cryptocurrency API integration to fail?

Most failures come from the build or the mount path; the rest come from the quota. The fetchedAt value in the route's response is the quickest way to tell whether the cache is doing its job.

The build fails after adding export const runtime = 'edge' to the route

Cause: Webflow Cloud deploys Next.js through the OpenNext Cloudflare adapter, which does not support the Next.js edge runtime target. The bring-your-own-app page describes Webflow Cloud as deploying "using the Edge runtime," meaning the Cloudflare Workers platform, and still tells you to add the directive to API routes.

Fix: Delete the runtime export from every route file; the route already runs on Workers. If you add middleware later, keep it in middleware.ts, because Webflow's framework customization docs state that "Node.js runtime middleware isn't supported. Only Edge runtime middleware works on the Workers runtime."

The ticker shows a 404 or a JSON parse error in production but works on localhost

Cause: The browser is requesting /api/prices at your site root, but the app is mounted under a path. The response is the Webflow site's HTML 404 page, which the component then fails to parse as JSON.

Fix: Check the failing request URL in the network tab. If the mount path is missing, set NEXT_PUBLIC_BASE_PATH to the exact mount path in the Webflow Cloud environment and redeploy, since Next.js inlines public values at build time.

The route returns "Price service is not configured" after deployment

Cause: COINGECKO_API_KEY exists in .env.local but not in the Webflow Cloud environment you deployed to.

Fix: Add the key to that environment as a secret, then redeploy. Secret values are redacted from build logs, so confirm the value by pasting it again rather than by reading a log.

The ticker shows "Prices unavailable: Upstream returned 429"

Cause: CoinGecko is rate-limiting the key, and the cache isn't absorbing traffic. Usually wrangler.json is missing, or its binding name differs from PRICES_KV, so getKv finds no store, and every poll from every open tab reaches the provider.

Fix: Check that wrangler.json sits in the project root with the PRICES_KV binding and redeploy. Repeated requests should then return the same fetchedAt. If the cache works and you still hit the limit, raise CACHE_TTL_SECONDS to fit the monthly allowance.

What you can build next with a cryptocurrency API and Webflow

A second Route Handler can return historical prices for a chart, using its own cache key so it doesn't share the ticker's window. The slower content around the numbers, such as coin profiles and market commentary, can live in the Webflow CMS, where editors publish without touching the app.

For receiving events from other services in the same app, the webhooks guide covers signature checks on Webflow Cloud.

For the storage options this route builds on, Webflow's developer docs cover Key Value Store and SQLite on Webflow Cloud.

Frequently asked questions

Can I build this with Astro or Vite instead of Next.js?

Yes, with Astro, which Webflow Cloud supports from version 5 and which reads bindings through locals instead of getCloudflareContext. The approach is the same: keep the key in a server endpoint and have the browser call only that endpoint. A Vite single-page app has no server routes, so it would need a separate endpoint to hold the key.

Can I store prices in the Webflow CMS instead of the app?

You can write prices to the CMS through Webflow's REST API, but prices that change every minute would mean constant writes and republishing. I keep live quotes in the Webflow Cloud app and use the CMS for slower content such as coin profiles.

Can I use CoinMarketCap instead of CoinGecko?

Yes. Change fetchUpstream to call CoinMarketCap's quotes endpoint with the X-CMC_PRO_API_KEY header, and map its response into the same { prices, fetchedAt } snapshot. Because the route normalizes the response, the ticker component needs no change.

Can I load coin metadata from a local JSON file with node:fs?

No. Webflow Cloud's Workers runtime has no file system access, as its Node.js compatibility page notes. Import the JSON file as a module instead, so the bundler includes it at build time and the route reads it like any other import.


Last Updated
October 24, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo

Get started for free

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.

Get started — it’s free
Watch demo

Try Webflow for as long as you like with our free Starter plan. Purchase a paid Site plan to publish, host, and unlock additional features.