# Build an India Gold Price App with JavaScript

An India gold app needs a different default from a generic XAU/USD ticker. Users expect rupees per gram, 24K and 22K side by side, Indian number formatting, an observation time, and a clear line between spot-derived metal value and the amount a jeweler will charge.

This guide builds that data path with one free endpoint and a small Node server. The result is suitable for a rate card, savings tracker, or jewelry estimator. It is not a dealer quote or appraisal.

## Use the INR carat endpoint

The free `/v1/carat` endpoint returns per-gram prices for eight purities. No API key is required:

```text
GET https://api.goldprice.dev/v1/carat?currency=INR
```

The response has this shape:

```json
{
  "currency": "INR",
  "timestamp": "2026-07-19T08:30:00Z",
  "price_gram_24k": "13250.00",
  "price_gram_22k": "12145.83",
  "price_gram_21k": "11593.75",
  "price_gram_20k": "11041.67",
  "price_gram_18k": "9937.50",
  "price_gram_16k": "8833.33",
  "price_gram_14k": "7729.17",
  "price_gram_10k": "5520.83"
}
```

The numbers above illustrate the response format; fetch the endpoint for the current values. Prices are decimal strings so the transmitted value is not changed by JSON floating-point conversion.

These are spot-derived reference prices in INR. For actual domestic dealer quotes and premium over spot, use the [India physical-price endpoint](/physical/in).

## Proxy and cache it on your server

The API intentionally is not a browser CORS surface. Call it from your server, cache the shared response for 60 seconds, and let browsers call your own route.

```js
import { createServer } from "node:http";

const SOURCE = "https://api.goldprice.dev/v1/carat?currency=INR";
const TTL_MS = 60_000;
let cache = null;

async function getIndiaRates() {
  const now = Date.now();
  if (cache && now - cache.fetchedAt < TTL_MS) return cache.data;

  try {
    const response = await fetch(SOURCE, {
      signal: AbortSignal.timeout(5_000),
    });
    if (!response.ok) throw new Error(`upstream ${response.status}`);

    const data = await response.json();
    if (
      data.currency !== "INR" ||
      typeof data.price_gram_24k !== "string" ||
      typeof data.price_gram_22k !== "string" ||
      typeof data.timestamp !== "string"
    ) {
      throw new Error("invalid rate response");
    }

    cache = { data, fetchedAt: now };
    return data;
  } catch (error) {
    if (cache) return cache.data; // stale-if-error
    throw error;
  }
}

createServer(async (request, response) => {
  const url = new URL(request.url, "http://localhost");
  if (url.pathname !== "/api/india-gold") {
    response.writeHead(404).end("not found");
    return;
  }

  try {
    const data = await getIndiaRates();
    response.writeHead(200, {
      "content-type": "application/json",
      "cache-control": "public, max-age=30",
    });
    response.end(JSON.stringify(data));
  } catch {
    response.writeHead(503, { "content-type": "application/json" });
    response.end(JSON.stringify({ error: "rate_unavailable" }));
  }
}).listen(3001);
```

The stale fallback preserves the source timestamp. Do not replace it with the time your server sent the response; users need to know when the market value was observed.

## Format rupees and calculate common weights

Convert the decimal strings only at the calculation boundary and format with the Indian grouping system:

```js
const formatINR = new Intl.NumberFormat("en-IN", {
  style: "currency",
  currency: "INR",
  maximumFractionDigits: 0,
});

function rateCard(data, grams = 10) {
  const price24k = Number(data.price_gram_24k);
  const price22k = Number(data.price_gram_22k);

  if (!Number.isFinite(price24k) || !Number.isFinite(price22k)) {
    throw new Error("invalid numeric rate");
  }

  return {
    perGram24k: formatINR.format(price24k),
    perGram22k: formatINR.format(price22k),
    tenGram24k: formatINR.format(price24k * grams),
    tenGram22k: formatINR.format(price22k * grams),
    observedAt: new Date(data.timestamp).toLocaleString("en-IN", {
      timeZone: "Asia/Kolkata",
      dateStyle: "medium",
      timeStyle: "short",
    }),
  };
}
```

Call your route from the browser:

```js
const response = await fetch("/api/india-gold");
if (!response.ok) throw new Error("rate unavailable");

const card = rateCard(await response.json());
document.querySelector("#rate-24k").textContent = card.perGram24k;
document.querySelector("#rate-22k").textContent = card.perGram22k;
document.querySelector("#observed-at").textContent = card.observedAt;
```

For money collected from users, keep decimal arithmetic in integer paise or use a decimal library. `Number` is acceptable for display-only estimates but not for invoices or accounting.

## Add a GST estimate without calling it a quote

For a finished jewelry sale, the official CBIC jewelry FAQ generally applies 3% GST to the total transaction value, including making charges. Keep each part visible:

```js
function estimateJewelryInvoice({ grams, perGram, makingCharge }) {
  const metalValue = grams * perGram;
  const subtotal = metalValue + makingCharge;
  const gst = subtotal * 0.03;

  return {
    metalValue,
    makingCharge,
    gst,
    estimatedTotal: subtotal + gst,
  };
}
```

Label the result “indicative estimate.” The spot-derived rate does not include a local dealer premium, stones, wastage, hallmarking fees, discounts, or buyback terms. Tax treatment also depends on the transaction. The [India gold-tax guide](/blog/gold-tax-india) explains the distinction between retail GST, import duty, and capital gains.

## Production checks

Before shipping, handle non-200 responses, set a request timeout, validate the response fields, cache by currency, and display the source timestamp. Log failures without substituting zero. Keep the purity label next to every figure: a 22K rate without “22K” is easy to mistake for 24K.

If the product promises a price a customer can transact at, replace the spot-derived estimate with a dealer quote from `GET /v1/physical/IN` (Physical tier or higher) and disclose the dealer, product, sell or buyback side, and update time. Start with the [live India gold-rate page](/gold/inr) to compare how the two views differ.
