
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:
GET https://api.goldprice.dev/v1/carat?currency=INRThe response has this shape:
{ "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. If your app also displays the USD/INR conversion rate, fetch that separately from exchangerate.dev and retain its observation timestamp. These figures are spot-derived; get actual domestic dealer prices from the dealers themselves.
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.
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:
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:
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:
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 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 quote from the dealer the customer will buy from. Disclose the dealer, product, sell or buyback side, and update time. Start with the live India gold-rate page to compare how the two views differ.
Live gold prices, historical OHLC, and multi-source aggregation, available via REST and SSE.