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 -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/fx/rates/USD?currencies=EUR"{
"base": "USD",
"market_session": "open",
"rates": [
{"currency": "EUR", "rate": "0.89435", "timestamp": "2026-11-03T02:31:44.626287Z", "stale": false}
]
}curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/commodities/spot/XAU/USD"{
"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.
| Widget | Time to live | Reason |
|---|---|---|
| Exchange rate | 60 seconds | The rate timestamp includes fractions of a second, and a minute of delay is shown to the user |
| Gold price | 60 seconds | The 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.
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){'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: 2The 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
| Situation | What the response says | What the widget does |
|---|---|---|
| Value is out of date | stale is true | Show the value with a delayed label, or hide it |
| Market is closed | market_session is closed, stale is false | Show the value and its time |
| Too many requests in a short window | 429 with rate_limit_exceeded and a Retry-After header | Serve the cached copy, retry after the delay |
| Balance used up | 402 with insufficient_credits | Serve the cached copy and alert the team |
| Server error | 500 or 503 | Serve 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 .