Show price widgets for FX and gold with caching

To show fresh price widgets in a fintech app, request each value from your server, cache it for a short time per data type and show its timestamp. Narwhal marks out-of-date values with stale and sends Retry-After on a 429.

Request the two widget values

Each widget needs one request. The exchange rate comes from GET /v1/fx/rates/{base}, with the quote currencies in currencies. The gold price comes from the spot endpoint for the commodity and the currency.

curl, the USD to EUR rate
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/fx/rates/USD?currencies=EUR"
Response
{
  "base": "USD",
  "market_session": "open",
  "rates": [
    {"currency": "EUR", "rate": "0.89435", "timestamp": "2026-11-03T02:31:44.626287Z", "stale": false}
  ]
}
curl, the gold spot price
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
  "https://api.narwhalapi.com/v1/commodities/spot/XAU/USD"
Response
{
  "symbol": "XAU-USD",
  "commodity_key": "gold",
  "price": "3946.3299",
  "currency": "USD",
  "unit": "troy_ounce",
  "unit_key": "troy_ounce",
  "timestamp": "2026-11-03T02:31:00Z",
  "stale": false
}

Both carry a timestamp and a stale flag. On the exchange rate, both sit inside each entry of rates. The spot response is a single object, so they sit at the top level.

Cache by data type

The rate and spot price responses both carry Cache-Control: private, no-store, so browsers and proxies keep no copy. The cache is yours to build, and it belongs on your server: a browser that called the API itself would expose the key. Your server fetches, caches and serves the widgets to every visitor from one copy. The lifetimes below are choices for a price widget, not API guarantees.

WidgetTime to liveReason
Exchange rate60 secondsThe rate timestamp includes fractions of a second, and a minute of delay is shown to the user
Gold price60 secondsThe timestamps moved in whole minutes, so a shorter lifetime returns the same value

A cache with a 60-second lifetime sends at most 2 requests a minute for these two widgets however many visitors load the page. That is 2,880 requests over a day of constant traffic, from 2 multiplied by 1,440 minutes. The caching and 429 guide explains how to choose a lifetime for other data.

Serve the widgets from the cache

The script keeps a dictionary of responses with an expiry time. A request is sent only when the stored copy has expired. When Narwhal answers 429, the function returns the last good copy if there is one. Each widget row carries the value, its own timestamp as at, and its stale flag, so the page can show when the number was set.

Python, a price cache that serves two widgets
import json, os, time, urllib.error, urllib.request

API = "https://api.narwhalapi.com"
cache = {}   # path -> (expires_at, data)
requests_sent = 0

def fetch(path, ttl):
    global requests_sent
    hit = cache.get(path)
    if hit and hit[0] > time.time():
        return hit[1]
    req = urllib.request.Request(API + path, headers={"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"})
    try:
        data = json.load(urllib.request.urlopen(req, timeout=10))
        requests_sent += 1
    except urllib.error.HTTPError as err:
        if err.code == 429 and hit:
            return hit[1]   # rate limited: show the last good copy
        raise
    cache[path] = (time.time() + ttl, data)
    return data

def widgets():
    fx = fetch("/v1/fx/rates/USD?currencies=EUR", ttl=60)["rates"][0]
    gold = fetch("/v1/commodities/spot/XAU/USD", ttl=60)
    return [
        {"name": "USD/EUR", "value": fx["rate"], "at": fx["timestamp"], "stale": fx["stale"]},
        {"name": "Gold USD/oz", "value": gold["price"], "at": gold["timestamp"], "stale": gold["stale"]},
    ]

for row in widgets():
    print(row)
widgets()
print("requests sent for two renders:", requests_sent)
Output
{'name': 'USD/EUR', 'value': '0.89435', 'at': '2026-11-03T02:31:44.626287Z', 'stale': False}
{'name': 'Gold USD/oz', 'value': '3946.3299', 'at': '2026-11-03T02:31:00Z', 'stale': False}
requests sent for two renders: 2

The second render found both entries unexpired, so the two renders sent 2 requests in total. No 429 came back in these requests, so the fallback path is written but not exercised here.

Handle stale values and rate limits

SituationWhat the response saysWhat the widget does
Value is out of datestale is trueShow the value with a delayed label, or hide it
Market is closedmarket_session is closed, stale is falseShow the value and its time
Too many requests in a short window429 with rate_limit_exceeded and a Retry-After headerServe the cached copy, retry after the delay
Balance used up402 with insufficient_creditsServe the cached copy and alert the team
Server error500 or 503Serve the cached copy, retry with backoff

For a spot price or a rate, stale is true when the value is out of date while its market is open: its source marks it stale, or it is more than 10 minutes old, counting only open-market time. A closed market never makes a value stale, so a weekend gold quote with stale: false is correct. The errors reference lists the problem codes. For prices that must update on screen as they change, the stream guide covers a WebSocket for gold, other metals and FX pairs.

Can a widget call the API from the browser?

Not with your key in the page, because anyone could read it. Call the API from your server, cache the result there and let the page read your own endpoint. That also means the number of API requests depends on your cache lifetime, not on your traffic.

Should a stale price be shown?

That is a product decision. A widget on a trading screen should hide or grey a stale value. A widget on a marketing page can show it with the timestamp beside it. Either way, keep the flag in your cache entry so the page can decide at render time.

Published .

Next steps