MetalAPI
XAU3,997.89−1.54%XAG53.936−1.51%XPT1,554.47−0.83%XPD1,047.03−0.44%XCU6.5080+2.29%WTI103.27+3.82%BRENT111.70−1.30%EUR1.120400−0.01%GBP1.337500−0.01%JPY0.0063761+0.14%CHF1.213445−0.25%TRY0.020125−0.12%BTC80,329.75−0.55%ETH2,329.36−1.13%
All articles
GoldSilverUnits

Troy Ounces, Grams and Kilograms: Converting Precious Metal Prices Correctly

Troy vs avoirdupois ounces, exact gram and kilogram factors, the API's unit parameter, rounding rules and the bugs that put metal prices off by almost 10%.

By MetalAPI Team··9 min read

Why the unit matters

Precious metals are quoted per troy ounce almost everywhere: in wholesale markets, in bullion dealer price lists and in price APIs. The people who buy and sell metal, on the other hand, think in grams, kilograms, tolas or taels, depending on where they live and what they trade. Any product that shows a metal price to people therefore contains at least one unit conversion, and that conversion is a common source of quiet, plausible-looking errors.

The classic mistake is treating a troy ounce as an ordinary ounce. The two differ by almost 10%, which is large enough to matter on every invoice but small enough that nobody notices it in a demo. This article covers the exact definitions, the conversion factors, how to let the API convert for you, how to round and display the results, and a list of bugs worth writing tests for.

Troy ounce vs avoirdupois ounce

Both ounces are defined in grains, and the grain has an exact metric definition: 1 grain = 64.79891 milligrams. Everything else follows from that:

  • A troy ounce is 480 grains, exactly 31.1034768 grams. It is the unit gold, silver, platinum and palladium are priced in.
  • An avoirdupois ounce, the everyday ounce for food and parcels in the US and UK, is 437.5 grains, exactly 28.349523125 grams.
  • A troy pound has 12 troy ounces (373.2417216 g), while the avoirdupois pound has 16 avoirdupois ounces (453.59237 g). So the troy ounce is the heavier ounce, but the troy pound is the lighter pound.
UnitGrainsGrams (exact)Troy ounces
Grain10.064798910.0020833…
Pennyweight (dwt)241.555173840.05
Avoirdupois ounce437.528.3495231250.9114583…
Troy ounce48031.10347681
Troy pound5,760373.241721612
Avoirdupois pound7,000453.5923714.5833…

A troy ounce is about 9.7% heavier than an avoirdupois ounce (480 ÷ 437.5 ≈ 1.0971). That ratio is exactly the size of the error you get when the two are mixed up.

Unless a source says otherwise, the ounce in a precious metal price is a troy ounce, often written oz t or ozt. In MetalAPI the default unit is troy_oz, and every metal price is per troy ounce unless you ask for something else.

Exact conversion factors

Because all of these units are defined exactly, conversions need no approximations. To turn a price per troy ounce into a price per another unit, multiply by the number of troy ounces in that unit:

price per unit = price per troy ounce × grams per unit ÷ 31.1034768

Price perGrams in one unitMultiply the troy ounce price by
Gram10.0321507466
Kilogram1,00032.1507466
Avoirdupois ounce28.3495231250.9114583333
Avoirdupois pound453.5923714.5833333
Metric ton1,000,00032,150.7466
Short ton (2,000 lb)907,184.7429,166.6667
Tola11.66380380.375
Pennyweight1.555173840.05

The tola, still widely used for gold in South Asia and the Gulf, is defined as 180 grains: exactly 11.6638038 g, or three eighths of a troy ounce. The tael is less tidy, because its weight depends on the market: about 37.4 g in the Hong Kong gold trade, 37.5 g in Taiwan and 50 g in mainland China. If you support taels, make the gram weight a configuration value and label which tael you mean.

Kilogram bars make a handy sanity check. A kilogram is about 32.15 troy ounces, so a kilo price should always be roughly 32 times the ounce price. If it is closer to 35 times, someone used the avoirdupois factor.

Converting by hand

If you already have a price per troy ounce, converting it yourself is one multiplication. Keep the exact constants in one module and convert through grams, so every pair of units works the same way:

units.mjs
// Grams in one of each unit. All exact: 1 grain = 64.79891 mg.
const GRAMS_PER_UNIT = {
  troy_oz: 31.1034768, // 480 grains
  gram: 1,
  kilogram: 1000,
  ounce: 28.349523125, // avoirdupois, 437.5 grains
  pound: 453.59237, // avoirdupois, 7,000 grains
  metric_ton: 1_000_000,
  short_ton: 907_184.74, // 2,000 lb
  tola: 11.6638038, // 180 grains
  pennyweight: 1.55517384, // 24 grains
};

/** Convert a price per `from` unit into a price per `to` unit. */
export function convertPrice(price, from, to) {
  const a = GRAMS_PER_UNIT[from];
  const b = GRAMS_PER_UNIT[to];
  if (a === undefined || b === undefined) throw new Error(`Unknown unit: ${a === undefined ? from : to}`);
  return (price / a) * b;
}

console.log(convertPrice(3997.89, "troy_oz", "gram")); // 128.535148…
console.log(convertPrice(3997.89, "troy_oz", "kilogram")); // 128535.148…
console.log(convertPrice(3997.89, "troy_oz", "tola")); // 1499.20875

In Python, use Decimal when the result ends up on an invoice or in a ledger. requests passes parse_float through to the JSON parser, so prices never go through a binary float:

per_gram.py
import os
from decimal import Decimal, ROUND_HALF_UP

import requests

GRAMS_PER_TROY_OZ = Decimal("31.1034768")
CENT = Decimal("0.01")

r = requests.get(
    "https://metalapi.com/api/v1/latest",
    params={"currencies": "XAU"},
    headers={"X-API-Key": os.environ["METALAPI_KEY"]},
    timeout=10,
)
data = r.json(parse_float=Decimal)
if not data["success"]:
    raise RuntimeError(data["error"])

per_oz = data["rates"]["USDXAU"]  # e.g. Decimal("3997.89")
per_gram = per_oz / GRAMS_PER_TROY_OZ
per_kg = per_gram * 1000

print(per_gram.quantize(CENT, rounding=ROUND_HALF_UP))  # 128.54 at the example price
print(per_kg.quantize(CENT, rounding=ROUND_HALF_UP))  # 128535.15

Inverse rates need the opposite operation. rates.XAU is troy ounces per one unit of the base currency, so grams per unit of currency is rates.XAU × 31.1034768, not ÷. That trap is one more reason to work with the prefixed price keys such as USDXAU and leave the inverse keys alone.

Letting the API convert: the unit parameter

You can also ask MetalAPI for the unit you need. /latest, the historical endpoints, /timeframe, /ohlc, /convert and /carat accept a unit parameter: troy_oz (default), gram, kilogram, ounce, pound, metric_ton or short_ton. This request returns gold and silver in euros per gram:

bash
curl "https://metalapi.com/api/v1/latest?base=EUR&currencies=XAU,XAG&unit=gram" \
  -H "X-API-Key: $METALAPI_KEY"

When the unit is not the default, the response repeats it in a top-level unit field. The values below are an example from the docs, not live prices:

Example response
{
  "success": true,
  "timestamp": 1792765800,
  "date": "2026-10-23",
  "base": "EUR",
  "unit": "gram",
  "rates": {
    "XAU": 0.009057444725,
    "EURXAU": 110.4064149,
    "XAG": 0.6713138244,
    "EURXAG": 1.489616277
  }
}

The API uses the same exact factors as the tables above, and the inverse key follows the unit too: with unit=gram, rates.XAU is grams of gold per euro. A few rules worth knowing:

  • Short forms are accepted, such as g, kg, lb and tonne. oz means the avoirdupois ounce, so never use it as shorthand for troy ounces.
  • An unknown unit returns error 209 (invalid_unit).
  • unit applies to precious and industrial metals only. Energy stays per barrel, MMBtu or gallon, and currencies and crypto are unaffected, so mixing symbols in one request is safe.
  • Industrial metals are also quoted per troy ounce by default, so all metals compare directly. Pass unit=pound for copper, unit=metric_ton for aluminium and zinc, and unit=short_ton for steel to get the units those markets trade in.

/convert accepts unit as well, which turns a weight into money in one step. This values 250 grams of gold in US dollars:

Value 250 g of gold
curl "https://metalapi.com/api/v1/convert?from=XAU&to=USD&amount=250&unit=gram" \
  -H "X-API-Key: $METALAPI_KEY"

info.rate is the price of one gram in US dollars and result is the value of the whole amount. With the example gold price, the response looks like this:

Example response
{
  "success": true,
  "query": { "from": "XAU", "to": "USD", "amount": 250, "unit": "gram" },
  "info": { "timestamp": 1792834892, "rate": 128.5351482, "quote": 128.5351482 },
  "date": "2026-10-24",
  "result": 32133.78705
}

Rounding and display

Rates arrive as JSON numbers with up to 10 significant digits. That precision is there so derived values stay accurate, so keep it until the last possible moment:

  • Convert first, round last. Rounding the gram price to cents and then multiplying by 1,000 for a kilo bar turns a half-cent rounding error into an error of up to five units of currency.
  • Round per currency. Most currencies use two decimals, the Japanese yen uses none and the Kuwaiti dinar three. Let Intl.NumberFormat or your locale library choose the defaults instead of hard-coding two.
  • Allow extra digits for small prices. At the example price of 53.94 USD per troy ounce, silver costs about 1.73 USD per gram, and two decimals hide most of its day-to-day movement.
  • Use decimal types for money. Decimal in Python, BigDecimal in Java, or integer minor units in your database for anything that is invoiced or booked. Binary floats are fine for charts.
  • Label the unit. Put per troy oz, per gram or per kg next to every price. A bare “per ounce” is ambiguous to many readers.

A small helper that takes the currency's standard decimals from Intl and optionally adds a few:

format.mjs
function formatPrice(value, currency, locale = "en-US", extraDigits = 0) {
  const { maximumFractionDigits } = new Intl.NumberFormat(locale, { style: "currency", currency }).resolvedOptions();
  const digits = maximumFractionDigits + extraDigits;
  return new Intl.NumberFormat(locale, {
    style: "currency",
    currency,
    minimumFractionDigits: digits,
    maximumFractionDigits: digits,
  }).format(value);
}

formatPrice(128.53514819925212, "USD"); // "$128.54"   gold per gram
formatPrice(1.7342112699117933, "USD", "en-US", 2); // "$1.7342"   silver per gram
formatPrice(110.4064149, "EUR", "de-DE"); // "110,41 €"

A conversion table at a glance

The table converts the example prices used in the MetalAPI docs, 3,997.89 USD per troy ounce for gold and 53.94 USD per troy ounce for silver. They illustrate the arithmetic and are not current market prices:

Price perGold (USD)Silver (USD)
Troy ounce3,997.8953.94
Gram128.541.7342
Kilogram128,535.151,734.21
Tola (11.6638 g)1,499.2120.23
Pennyweight (1.5552 g)199.892.697
Troy pound (12 oz t)47,974.68647.28
Avoirdupois ounce3,643.9149.16

Look at the last row. If a page labels the avoirdupois figure as the ounce price, it understates the bullion price by almost 9%. The reverse mistake, dividing a troy ounce price by 28.35 to get a gram price, overstates it by about 9.7%: 141.02 instead of 128.54 per gram of gold.

Common conversion bugs and how to test for them

Most unit bugs fall into a handful of patterns:

  • Dividing by 28.35. Using the avoirdupois factor for a troy ounce price. Use 31.1034768.
  • Treating `oz` as troy. In MetalAPI, as in most unit libraries, oz is the avoirdupois ounce. Bullion prices need troy_oz, or no unit at all.
  • Converting the inverse key. rates.XAU is ounces per currency unit, not a price. Converting it to grams means multiplying by 31.1034768, which looks wrong to anyone used to dividing.
  • Converting twice. Requesting unit=gram and then dividing by 31.1034768 again gives a price about 31 times too small. Decide whether conversion happens in the API or in your code, never both.
  • Rounding too early. Round once, at display time.
  • Caching across units. If your cache key ignores the query string, a gram response can be served where an ounce price was expected. Include unit and base in the key.
  • Applying weight units to energy. unit has no effect on oil or gas, and dividing a barrel price by 31.1 makes no sense. Guard conversions by category, which /symbols?details=1 returns for every symbol.

A few property tests catch most of these before they ship. They use Node's built-in test runner and the convertPrice function from above; run them with node --test:

units.test.mjs
import { test } from "node:test";
import assert from "node:assert/strict";
import { convertPrice } from "./units.mjs";

const PER_OZ = 3997.89; // example price, USD per troy ounce

test("a kilogram price is 1,000 gram prices", () => {
  const g = convertPrice(PER_OZ, "troy_oz", "gram");
  const kg = convertPrice(PER_OZ, "troy_oz", "kilogram");
  assert.ok(Math.abs(kg - g * 1000) < 1e-6);
});

test("converting there and back returns the original price", () => {
  const g = convertPrice(PER_OZ, "troy_oz", "gram");
  assert.ok(Math.abs(convertPrice(g, "gram", "troy_oz") - PER_OZ) < 1e-9);
});

test("an avoirdupois ounce costs less than a troy ounce", () => {
  assert.ok(convertPrice(PER_OZ, "troy_oz", "ounce") < PER_OZ);
});

test("a kilogram holds about 32.15 troy ounces", () => {
  const kg = convertPrice(PER_OZ, "troy_oz", "kilogram");
  assert.ok(Math.abs(kg / PER_OZ - 32.1507466) < 1e-6);
});

If you price jewelry, the next step is purity: Gold karat prices explained builds 22K and 18K prices on top of these conversions. The data formats guide and the units section of the API docs document every unit value, and the symbols page shows the unit each market trades in.

Try it with your own data

Create a free account and make your first request in under two minutes. 100 requests a month, no credit card.

Keep reading