Feed updates
Equity.US.NET/USDwent liveCrypto.XBTC/USDwent liveEquity.US.TMF/USDaddedEquity.US.MSTX/USDaddedInterestRate.10YZ6removedInterestRate.10YU6removedInterestRate.3MSZ6removedInterestRate.3MSU6removedInterestRate.3MSM6removedCommodities.CLLZ6/USDaddedCommodities.CLLX6/USDaddedCommodities.CLLV6/USDaddedCommodities.CLLU6/USDaddedCommodities.CAN6/USDremoved
View all
Developer Hub

Frontend Authentication

Use short-lived tokens to call Pyth Pro from browser apps

This guide explains how to call Pyth Pro from browser and frontend apps without shipping your PRO_API_KEY to users. Anything in a browser bundle is public, so your backend mints a short-lived JWT with POST /auth/token and hands it to the browser. The browser then presents that JWT in place of the API key — as an Authorization: Bearer header on the History and REST APIs, and through the pyth-lazer-auth subprotocol on a WebSocket connection.

Replacing the Proxy (Beta) API?

This is the replacement. The beta Proxy (pyth-lazer-proxy-{1,2,3}) gave unauthenticated frontend access; now your backend mints a short-lived JWT and the browser uses it over the WebSocket, REST, and History APIs — the raw API key stays server-side.

When to use which pattern

ClientPattern
Server-to-serverPresent the long-lived Pro API key directly as Authorization: Bearer <PRO_API_KEY>. Simplest — prefer this whenever the client can hold a secret.
Browser / frontendYour backend mints a JWT via POST /auth/token and returns it to the browser, which presents it in place of the API key: Authorization: Bearer <JWT> on the History and REST APIs, or the pyth-lazer-auth WebSocket subprotocol for streaming.

Never expose the long-lived PRO_API_KEY in browser code, mobile apps, or any distributed binary. Anything in the client bundle is public — treat it as leaked the moment you ship it.

The flow

  1. The browser client asks its own backend for a Pyth JWT.
  2. The backend calls POST https://pyth.dourolabs.app/auth/token with Authorization: Bearer <PRO_API_KEY>.
  3. The backend returns the JWT (and its expires_at) to the browser.
  4. The browser presents the JWT in place of the API key until it nears expiry, then repeats from step 1.

POST /auth/token reference

  • Base URL: https://pyth.dourolabs.app
  • Method / path: POST /auth/token
  • Auth: Authorization: Bearer <PRO_API_KEY> (the long-lived Pro API key)
  • Content-Type: application/json

Request body (optional)

FieldTypeRequiredDescription
ttl_secondsinteger | nullNoHow long the token should live, in seconds. The server caps it at its maximum; omit it (or send null) to get that maximum.

Response 200

FieldTypeDescription
access_tokenstringShort-lived JWT to present as Authorization: Bearer <access_token>.
expires_atstringRFC 3339 datetime at which the token expires.
token_typestringAlways "Bearer".

Errors

StatusMeaning
401Missing or malformed API key.
404Token broker is not enabled on this deployment.

Backend example

Add a token endpoint to your own backend. Never send the raw PRO_API_KEY to the browser — hand out only the short-lived JWT.

import express from "express";

const app = express();
app.use(express.json());

// Requires PRO_API_KEY in the backend's environment.
// Protect this route with your own auth — see "Protecting the token endpoint" below.
app.post("/pyth-token", async (req, res) => {
  const body: Record<string, unknown> = {};
  if (typeof req.body?.ttl_seconds === "number") {
    // Optional pass-through: let callers request a shorter TTL than the server default.
    body.ttl_seconds = req.body.ttl_seconds;
  }

  const response = await fetch("https://pyth.dourolabs.app/auth/token", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PRO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) {
    res.status(response.status).send(await response.text());
    return;
  }

  // { access_token, expires_at, token_type }
  res.json(await response.json());
});

Do not cache the minted JWT on the server across users unless you scope each token to one user. A JWT is a bearer token: whoever holds it can spend your quota until it expires.

Frontend example

Cache the JWT in memory, refresh it a few seconds before expires_at, and send it as a bearer token on each request.

type PythToken = { access_token: string; expires_at: string };

let cached: PythToken | undefined;

async function getPythToken(): Promise<string> {
  // Refresh 30s before the token actually expires so an in-flight request
  // doesn't race the expiry.
  const safetyMarginMs = 30_000;
  if (cached && Date.parse(cached.expires_at) - Date.now() > safetyMarginMs) {
    return cached.access_token;
  }

  const response = await fetch("/pyth-token", { method: "POST" });
  if (!response.ok) throw new Error(`Failed to mint Pyth token: ${response.status}`);
  cached = (await response.json()) as PythToken;
  return cached.access_token;
}

// Call the History API from the browser using the short-lived JWT.
const jwt = await getPythToken();
const res = await fetch(
  "https://pyth.dourolabs.app/v1/fixed_rate@200ms/price?ids=1&timestamp=1704067200000000",
  { headers: { Authorization: `Bearer ${jwt}` } },
);
console.log(await res.json());

The same JWT works against the REST API on https://pyth-lazer.dourolabs.app — send it as Authorization: Bearer <JWT>, exactly as on the History API.

For the WebSocket API, a browser cannot set request headers on the connection, so pass the JWT through the subprotocol list instead. Offer the marker pyth-lazer-auth immediately followed by the token:

const ws = new WebSocket(
  "wss://pyth-lazer-0.dourolabs.app/v1/stream",
  ["pyth-lazer-auth", jwt],
);

The server reads the token from that value, authenticates, and completes the handshake by echoing back the pyth-lazer-auth subprotocol — your client accepts it and does nothing with it. The rest of the subscribe flow is unchanged.

Protecting the token endpoint

  • Require your own auth on POST /pyth-token (session cookie, first-party JWT, etc.). Anyone who can reach this endpoint can spend your Pro API-key quota.
  • Rate-limit it. Without a limit, the endpoint is easy to abuse.
  • Quota accounting: calls made with a JWT count against the quota of the API key that minted it.

On this page