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%
Todos os artigos
TutorialE-commerceTypeScriptPython

Crie uma calculadora de preços de joias com cotações do ouro ao vivo

Crie no servidor uma calculadora de preços de joias em TypeScript e Python, com ouro por grama ao vivo, quilates, mão de obra, IVA e cache dentro da sua cota.

Por MetalAPI Team··10 min de leitura

O que a calculadora faz

O preço de uma joia normalmente parte de quatro dados: o peso da peça, o teor do ouro, a cotação atual do ouro e a mão de obra do joalheiro, com os impostos por cima. Uma calculadora que faça isso para cada página de produto ou pedido de orçamento precisa do preço do ouro por grama na moeda da loja. E precisa desse preço rapidamente, dentro da cota da API e sem expor a chave de API ao navegador.

Este tutorial cria um pequeno serviço no servidor em TypeScript (Node.js) e em Python. O navegador, ou o backend da sua loja, envia um peso e um quilate para o seu próprio endpoint. O seu servidor combina esses dados com uma cotação do ouro da MetalAPI mantida em cache e retorna o detalhamento completo do preço. A fórmula:

Fórmula de preço
valor do metal = peso (g) × preço do ouro por grama (24K) × teor
mão de obra    = por grama, um percentual do valor do metal ou um valor fixo
preço líquido  = valor do metal + mão de obra
IVA            = preço líquido × alíquota do IVA
total          = preço líquido + IVA

O ouro à vista é um preço de referência do mercado atacadista. Os preços de varejo também refletem o design, as pedras, as perdas de fabricação e a margem, e os preços de recompra costumam ficar abaixo do preço à vista. Trate esta calculadora como a parte do seu preço baseada no metal e acrescente as suas próprias regras de negócio.

Ouro por grama na sua moeda

O endpoint /latest retorna preços em qualquer moeda base e, para metais, em qualquer unidade de peso. Uma única requisição fornece o preço de um grama de ouro puro em euros:

bash
curl "https://metalapi.com/api/v1/latest?base=EUR&currencies=XAU&unit=gram" \
  -H "X-API-Key: YOUR_API_KEY"
Resposta (valores ilustrativos)
{
  "success": true,
  "timestamp": 1792834892,
  "date": "2026-10-24",
  "base": "EUR",
  "unit": "gram",
  "rates": {
    "XAU": 0.0090909091,
    "EURXAU": 110
  },
  "rate_limit": { "limit": 10000, "remaining": 9874, "...": "..." }
}

rates.EURXAU é o preço de uma unidade de XAU, aqui um grama, na moeda base. Esse é o número de que a calculadora precisa. rates.XAU é o inverso: gramas de ouro por euro. A resposta inclui unit sempre que a unidade não é a onça troy padrão, e timestamp é o horário dos dados de preço. Exiba-o ao lado de cada orçamento.

A moeda base e a unidade funcionam em todos os planos, inclusive no Free, e a mesma requisição pode incluir XAG, XPT e XPD se você também vende peças de prata ou platina. Os planos pagos também podem usar o endpoint de quilates, que retorna diretamente preços por quilate como XAU22K, ou todos os quilates de uma vez com carat=all. Este tutorial aplica o teor por conta própria para que o código funcione em qualquer plano e possa usar os valores de teor das marcações oficiais, que é o próximo assunto.

Teor, mão de obra e IVA

O quilate expressa o teor de ouro em 24 avos, então o fator de pureza é karat / 24, que é a convenção usada pelo endpoint /carat. Muitos mercados, porém, marcam o ouro pelo teor em milésimos, e para alguns quilates os dois valores diferem ligeiramente:

Quilatequilate / 24Marcação usual
24K1,0000999
22K0,9167916
21K0,8750875
18K0,7500750
14K0,5833585
9K0,3750375

Para um anel de 14K com 5,2 g, a um preço ilustrativo de € 110 por grama, 14/24 resulta em um valor do metal de € 333,67 e 0.585 resulta em € 334,62. Nenhum dos dois está errado. Use a convenção das suas marcações e notas fiscais; é por isso que o código abaixo recebe um fator de pureza em vez de um quilate. O guia de preços por quilate aprofunda esse contexto.

A mão de obra costuma assumir uma de três formas, e a versão em TypeScript aceita todas elas:

  • Por grama: um valor fixo para cada grama da peça, por exemplo € 12/g.
  • Percentual: uma parcela do valor do metal, por exemplo 8%. Ela sobe e desce junto com o preço do ouro.
  • Fixa: uma taxa fixa por peça, comum em itens pequenos e trabalhos sob encomenda.

O IVA ou imposto sobre vendas vem por último. A alíquota, e aquilo sobre o que ela incide, depende do país: alguns tributam o preço integral, outros tratam o ouro para investimento de forma diferente das joias, e outros tributam apenas a mão de obra. Mantenha a alíquota e a base de cálculo na configuração, confirme-as com o seu contador e arredonde cada linha para centavos, para que as linhas de uma nota fiscal somem exatamente o total.

Cache para ficar dentro da sua cota

Cada plano atualiza os preços em um intervalo fixo, e as atualizações caem em múltiplos desse intervalo contados a partir de 00:00 UTC: às :00, :10, :20 e assim por diante no plano Standard, e uma vez por dia à meia-noite UTC no Free. Todas as requisições entre dois desses limites retornam os mesmos preços, então o cache ideal é renovado uma vez por limite. Renovar a cada limite, 24 horas por dia, custa isto em um mês de 31 dias:

Plano (cota mensal)Intervalo de atualizaçãoRequisições a cada limiteDentro da cota?
Free (100)Diário31Sim
Lite (1.000)30 min1.488Não: renove a cada hora (744)
Standard (10.000)10 min4.464Sim
Advanced (50.000)1 min44.640Sim, com menos folga para outras chamadas

Mais duas regras deixam o cache robusto. Use requisição única (single flight): quando o cache expira sob carga, a primeira requisição busca um novo preço e as requisições simultâneas aguardam esse resultado, em vez de cada uma chamar a API. E sirva o último preço em caso de erro (serve stale on error): se a API não puder ser acessada, continue cotando com o último preço válido por algumas horas e tente de novo a cada minuto, em vez de derrubar cada finalização de compra. Requisições que falham com erro do servidor não são cobradas, mas ainda assim não há motivo para disparar uma rajada delas durante uma indisponibilidade.

O cache abaixo fica na memória do processo, o que é ideal para um único servidor. Em plataformas serverless ou com várias instâncias, cada instância mantém a sua própria cópia. Guarde o preço em um armazenamento compartilhado, como o Redis, ou use o cache de dados do seu framework com a mesma expiração.

A implementação em TypeScript

O módulo abaixo roda no Node.js 18 ou mais recente e não tem dependências. Ele busca o preço em euros por grama, mantém-no em cache até o próximo limite do plano e transforma um peso, um fator de pureza e uma mão de obra em um detalhamento de preço:

gold-quote.ts
// gold-quote.ts — Node 18+. A chave de API fica no servidor.
const API_KEY = process.env.METALAPI_KEY!;
const CURRENCY = "EUR";
const PLAN_INTERVAL_S = 600; // Plano Standard: os preços são atualizados a cada 10 minutos
const MIN_TTL_S = 0;         // Plano Lite: use 3600 para ficar abaixo de 1.000 requisições por mês

interface GoldPrice {
  perGram: number;   // ouro puro (24K), em CURRENCY por grama
  timestamp: number; // horário dos dados de preço (segundos Unix)
  fetchedAt: number;
  expiresAt: number;
}

let cached: GoldPrice | null = null;
let inflight: Promise<GoldPrice> | null = null;

// Os preços só mudam em múltiplos do intervalo do plano (contados a partir de 00:00 UTC).
function nextBoundary(ms: number): number {
  const step = PLAN_INTERVAL_S * 1000;
  return Math.floor(ms / step) * step + step;
}

async function fetchGold(): Promise<GoldPrice> {
  const url = `https://metalapi.com/api/v1/latest?base=${CURRENCY}&currencies=XAU&unit=gram`;
  const res = await fetch(url, { headers: { "X-API-Key": API_KEY }, signal: AbortSignal.timeout(10_000) });
  const body = await res.json();
  if (!body.success) throw new Error(`MetalAPI ${body.error.code}: ${body.error.info}`);
  const now = Date.now();
  return {
    perGram: body.rates[`${CURRENCY}XAU`],
    timestamp: body.timestamp,
    fetchedAt: now,
    expiresAt: nextBoundary(now + MIN_TTL_S * 1000) + 5_000, // alguns segundos de folga
  };
}

export function goldPerGram(): Promise<GoldPrice> {
  if (cached && Date.now() < cached.expiresAt) return Promise.resolve(cached);
  // Requisição única: requisições simultâneas compartilham uma só chamada à API.
  inflight ??= fetchGold()
    .then((fresh) => (cached = fresh))
    .catch((err) => {
      // Continua cotando com o último preço válido por até 6 horas; tenta de novo em um minuto.
      if (cached && Date.now() - cached.fetchedAt < 6 * 3600_000) {
        cached.expiresAt = Date.now() + 60_000;
        return cached;
      }
      throw err;
    })
    .finally(() => (inflight = null));
  return inflight;
}

export type MakingCharge =
  | { type: "per_gram"; amount: number }
  | { type: "percent"; rate: number } // 0.08 = 8% do valor do metal
  | { type: "flat"; amount: number };

const money = (n: number) => Math.round(n * 100 + Number.EPSILON) / 100;

export async function quote(grams: number, purity: number, making: MakingCharge, vatRate: number) {
  const gold = await goldPerGram();
  const metal = money(grams * gold.perGram * purity);
  const labour = money(
    making.type === "per_gram" ? grams * making.amount
      : making.type === "percent" ? metal * making.rate
      : making.amount,
  );
  const net = money(metal + labour);
  const vat = money(net * vatRate);
  return {
    currency: CURRENCY,
    grams,
    purity,
    gold_per_gram_24k: gold.perGram,
    price_time: new Date(gold.timestamp * 1000).toISOString(),
    metal,
    making: labour,
    net,
    vat,
    total: money(net + vat),
  };
}

Algumas linhas merecem um olhar mais atento. nextBoundary() arredonda para cima até o próximo múltiplo do intervalo do plano, então no Standard um preço buscado às 10:03 expira às 10:10:05, alguns segundos depois da próxima atualização, e não às 10:13. Com MIN_TTL_S definido como 3600 no Lite, a próxima renovação espera pelo primeiro limite que esteja a pelo menos uma hora de distância, o que limita o uso a 24 requisições por dia. O ramo .catch é a regra de servir o último preço: ele retorna o último preço válido e agenda a próxima tentativa para um minuto depois, de modo que uma indisponibilidade custa no máximo uma requisição com falha por minuto, e não uma por visitante.

Exponha o módulo por meio de um pequeno endpoint HTTP. Valide a entrada com rigor: aceite apenas os teores que você vende e uma faixa de peso razoável. A mão de obra e a alíquota do IVA são configurações do servidor; nunca as aceite do cliente.

server.ts
// server.ts — npm install express, depois: METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { quote } from "./gold-quote";

// Fatores de pureza conforme marcados nas suas peças (veja a tabela de quilates acima).
const PURITY: Record<string, number> = { "24": 0.999, "22": 0.916, "21": 0.875, "18": 0.75, "14": 0.585, "9": 0.375 };
const app = express();

app.get("/api/jewelry-quote", async (req, res) => {
  const grams = Number(req.query.grams);
  const purity = PURITY[String(req.query.karat)];
  if (!purity || !(grams > 0 && grams <= 2000)) {
    res.status(400).json({ error: "Use karat 24, 22, 21, 18, 14 or 9 and grams between 0 and 2000." });
    return;
  }
  try {
    // Mão de obra e IVA são configurações do servidor, nunca dados enviados pelo cliente.
    res.json(await quote(grams, purity, { type: "per_gram", amount: 12 }, 0.2));
  } catch (err) {
    console.error(err);
    res.status(503).json({ error: "Gold price temporarily unavailable." });
  }
});

app.listen(3000, () => console.log("Try http://localhost:3000/api/jewelry-quote?karat=18&grams=5.2"));

Uma requisição a /api/jewelry-quote?karat=18&grams=5.2 retorna um detalhamento como este (preço do ouro ilustrativo):

json
{
  "currency": "EUR",
  "grams": 5.2,
  "purity": 0.75,
  "gold_per_gram_24k": 110,
  "price_time": "2026-10-24T09:41:32.000Z",
  "metal": 429,
  "making": 62.4,
  "net": 491.4,
  "vat": 98.28,
  "total": 589.68
}

A implementação em Python

A versão em Python segue o mesmo desenho. Ela usa Decimal para valores monetários, de modo que os valores nunca acumulam erros de ponto flutuante binário, e um lock para que requisições simultâneas em um servidor com threads compartilhem uma só chamada à API. Para manter o código curto, ela aceita apenas mão de obra por grama:

gold_quote.py
# gold_quote.py — Python 3.9+, pip install requests
import math
import os
import threading
import time
from decimal import ROUND_HALF_UP, Decimal

import requests

API_KEY = os.environ["METALAPI_KEY"]
CURRENCY = "EUR"
PLAN_INTERVAL_S = 600  # Plano Standard: os preços são atualizados a cada 10 minutos
MIN_TTL_S = 0          # Plano Lite: use 3600 para ficar abaixo de 1.000 requisições por mês

_lock = threading.Lock()
_cache = {"per_gram": None, "timestamp": 0, "fetched": 0.0, "expires": 0.0}


def _next_boundary(t: float) -> float:
    return (math.floor(t / PLAN_INTERVAL_S) + 1) * PLAN_INTERVAL_S


def gold_per_gram() -> tuple[Decimal, int]:
    """Ouro puro (24K) em CURRENCY por grama, e o horário Unix desse preço."""
    with _lock:  # uma chamada à API por vez; as threads em espera reutilizam o resultado
        now = time.time()
        if _cache["per_gram"] is not None and now < _cache["expires"]:
            return _cache["per_gram"], _cache["timestamp"]
        try:
            r = requests.get(
                "https://metalapi.com/api/v1/latest",
                params={"base": CURRENCY, "currencies": "XAU", "unit": "gram"},
                headers={"X-API-Key": API_KEY},
                timeout=10,
            )
            body = r.json()
            if not body.get("success"):
                raise RuntimeError(f"MetalAPI {body['error']['code']}: {body['error']['info']}")
        except Exception:
            if _cache["per_gram"] is not None and now - _cache["fetched"] < 6 * 3600:
                _cache["expires"] = now + 60  # serve o último preço válido, tenta de novo em um minuto
                return _cache["per_gram"], _cache["timestamp"]
            raise
        _cache.update(
            per_gram=Decimal(str(body["rates"][f"{CURRENCY}XAU"])),
            timestamp=body["timestamp"],
            fetched=now,
            expires=_next_boundary(now + MIN_TTL_S) + 5,
        )
        return _cache["per_gram"], _cache["timestamp"]


def money(x: Decimal) -> Decimal:
    return x.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)


def quote(grams: Decimal, purity: Decimal, making_per_gram: Decimal, vat_rate: Decimal) -> dict:
    per_gram, ts = gold_per_gram()
    metal = money(grams * per_gram * purity)
    making = money(grams * making_per_gram)
    net = metal + making
    vat = money(net * vat_rate)
    return {
        "currency": CURRENCY,
        "gold_per_gram_24k": str(per_gram),
        "price_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(ts)),
        "metal": str(metal),
        "making": str(making),
        "net": str(net),
        "vat": str(vat),
        "total": str(net + vat),
    }

Qualquer framework web pode servi-lo. Com o FastAPI, endpoints definidos com def comum rodam em um pool de threads, e é exatamente para isso que serve o lock:

app.py
# app.py — pip install fastapi uvicorn requests, depois: METALAPI_KEY=mk_live_... uvicorn app:app
from decimal import Decimal

from fastapi import FastAPI, HTTPException, Query

from gold_quote import quote

# Fatores de pureza conforme marcados nas suas peças.
PURITY = {24: Decimal("0.999"), 22: Decimal("0.916"), 21: Decimal("0.875"),
          18: Decimal("0.750"), 14: Decimal("0.585"), 9: Decimal("0.375")}
app = FastAPI()


@app.get("/api/jewelry-quote")
def jewelry_quote(karat: int, grams: Decimal = Query(gt=0, le=2000)):
    if karat not in PURITY:
        raise HTTPException(400, "Use karat 24, 22, 21, 18, 14 or 9.")
    try:
        return quote(grams, PURITY[karat], making_per_gram=Decimal("12.00"), vat_rate=Decimal("0.20"))
    except Exception:
        raise HTTPException(503, "Gold price temporarily unavailable.")

A versão em Python retorna os valores como strings, e a versão em TypeScript, como números. Se um orçamento for para um sistema de pedidos, armazene os valores como strings decimais ou centavos inteiros, e não como números de ponto flutuante.

Um exemplo prático

Aqui está o anel de 18K precificado passo a passo, com um preço ilustrativo do ouro de € 110,00 por grama, mão de obra de € 12 por grama e IVA de 20%:

EtapaCálculoValor
Ouro 24K por gramarates.EURXAU€ 110,00
Teor18K, marcação 7500,75
Valor do metal5,2 g × € 110,00 × 0,75€ 429,00
Mão de obra5,2 g × € 12,00€ 62,40
Preço líquido€ 429,00 + € 62,40€ 491,40
IVA€ 491,40 × 20%€ 98,28
Total€ 589,68

Agora, uma corrente de 22K com 10 g e mão de obra de 8%, com o mesmo preço do ouro. Com teor 22/24, o valor do metal é € 1.008,33, a mão de obra é 8% desse valor (€ 80,67), o preço líquido é € 1.089,00 e o total com IVA é € 1.306,80. Com o fator da marcação 916, o valor do metal seria € 1.007,60.

Apenas o valor do metal acompanha o preço do ouro, então o total varia menos que o próprio ouro. Se o ouro subir 1%, para € 111,10 por grama, o valor do metal do anel de 18K passa a € 433,29 e o total a € 594,83, um aumento de € 5,15, ou 0,87%. Com mão de obra percentual, o total acompanharia o preço do ouro mais de perto.

Checklist para produção

  • Mantenha a chave de API em uma variável de ambiente no servidor. O endpoint da calculadora retorna preços, nunca a chave.
  • Exiba price_time em cada orçamento, para que clientes e equipe vejam quão atual é o preço do ouro.
  • Congele o preço quando um pedido for feito: armazene com o pedido o preço do ouro, o timestamp dele, o teor e o valor de cada linha, em vez de recalcular depois.
  • Espere preços estáveis no fim de semana, quando o mercado de ouro está fechado. Isso não é um cache desatualizado.
  • Acompanhe o uso com o endpoint de uso, que não conta para a sua cota, e crie um aviso para você mesmo antes que o mês acabe.
  • Trate o erro 105 (cota mensal esgotada) servindo o preço em cache e alertando a sua equipe. A cota é renovada no dia 1º, às 00:00 UTC, e um upgrade vale imediatamente.
  • Para reagir a grandes movimentos de preço, por exemplo para reprecificar o seu catálogo, use alertas de preço com webhooks em vez de fazer polling com mais frequência.

Compare planos e cotas na página de preços ou crie uma conta grátis para obter uma chave e testar a calculadora com preços reais.

Experimente com seus próprios dados

Crie uma conta grátis e faça sua primeira requisição em menos de dois minutos. 100 requisições por mês, sem cartão de crédito.

Continue lendo