USD to IRR API in Python and Django: Fetch, Cache and Display Live Rates
Use a USD to IRR API in Python: fetch the free-market rate with requests, add retries and caching, build a Django view and avoid Rial/Toman bugs. Code included.

- A small Python client with a session, timeout and retries is enough to read live rates from the EZRate API.
- In Django, wrap the call in the cache framework so every visitor is served from one cached response.
- Store raw integers, keep the unit next to the value and format only at display time.
This tutorial builds a production-ready integration of the USD to IRR API in Python: a reusable client, caching, a Django view and a template that shows buy/sell rates with their update time. Everything uses the Tehran free-market rate, not the official one.
requests, read the key from an environment variable, call GET https://api.ezrate.me/v1/rates?symbols=usd with an X-API-Key header and a timeout, and cache the JSON for meta.freshness_sec seconds with Django’s cache framework. Pass quote=IRR if your system works in Rial.Part of the Iranian Rial exchange rate API guide. If you have no key yet, follow the quickstart.
1. Set up
python3 -m venv .venv && source .venv/bin/activate
pip install requests django
export EZRATE_KEY="your-key-here" # never commit it2. A small, robust client
# ezrate.py
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
BASE = "https://api.ezrate.me/v1"
class EZRate:
def __init__(self, key: str | None = None):
self.s = requests.Session()
self.s.headers["X-API-Key"] = key or os.environ["EZRATE_KEY"]
retry = Retry(total=3, backoff_factor=0.5,
status_forcelist=[502, 503, 504],
respect_retry_after_header=True)
self.s.mount("https://", HTTPAdapter(max_retries=retry))
def get(self, path: str, **params) -> dict:
r = self.s.get(f"{BASE}{path}", params=params, timeout=10)
if r.status_code == 429:
raise RuntimeError(f"rate limited, retry after {r.headers.get('Retry-After')}s")
r.raise_for_status()
return r.json()
def rates(self, symbols: list[str], quote: str = "IRT") -> dict:
body = self.get("/rates", symbols=",".join(symbols), quote=quote, lang="en")
return {x["symbol"]: x for x in body["data"]}, body["meta"]We do not retry 429 automatically: hitting your quota usually means the caching is wrong, and blind retries make it worse. Error codes are listed in the JSON reference.
3. Try it
from ezrate import EZRate
rates, meta = EZRate().rates(["usd", "eur", "aed", "usdt"])
usd = rates["usd"]
print(f"USD buy {usd['buy']:,} / sell {usd['sell']:,} {usd['quote']}")
print("sources:", usd["sources"], "low:", usd["low_sources"], "as of", meta["as_of"])4. Cache it in Django
# rates/services.py
from django.core.cache import cache
from ezrate import EZRate
KEY = "ezrate:rates:v1"
def live_rates():
hit = cache.get(KEY)
if hit:
return hit
try:
rates, meta = EZRate().rates(["usd", "eur", "aed", "try", "g18", "emami"])
except Exception:
stale = cache.get(KEY + ":last")
if stale:
return stale | {"stale": True}
raise
data = {"rates": rates, "meta": meta, "stale": False}
ttl = max(30, int(meta.get("freshness_sec", 60)))
cache.set(KEY, data, ttl)
cache.set(KEY + ":last", data, 24 * 3600) # fallback copy
return dataTwo keys: a short-lived one for normal reads and a long-lived “last good” copy you can serve, marked as stale, if the API is unreachable. Use Redis or Memcached as the cache backend in production. The same pattern in PHP is in Laravel exchange rate API caching.
5. View and template
# rates/views.py
from django.shortcuts import render
from .services import live_rates
def board(request):
return render(request, "rates/board.html", live_rates())<!-- rates/templates/rates/board.html -->
<table>
<tr><th>Asset</th><th>Buy</th><th>Sell</th><th>Change</th></tr>
{% for sym, r in rates.items %}
<tr>
<td>{{ r.name }}</td>
<td>{{ r.buy|floatformat:0 }}</td>
<td>{{ r.sell|floatformat:0 }}</td>
<td>{{ r.change_pct }}%</td>
</tr>
{% endfor %}
</table>
<p>Prices in {{ meta.quote }} · updated {{ meta.as_of }}{% if stale %} · showing last known rates{% endif %}</p>
6. Historical candles with pandas
import pandas as pd
from ezrate import EZRate
body = EZRate().get("/history/usd", interval="1d", **{"from": "2026-01-01", "to": "2026-10-01"})
df = pd.DataFrame(body["data"])
df["t"] = pd.to_datetime(df["t"], utc=True) # ISO 8601, UTC
df = df.set_index("t")
print(df["c"].resample("W").last().pct_change().describe())Candles are based on the sell rate and use UTC. Research workflows are covered in historical USD/IRR data.
Rial, Toman and Decimal
- Prefer
quote=IRRover multiplying by 10 yourself. - Rates in Toman/Rial are integers; for crypto in USDT use
Decimal, not float, in money logic. - Store the unit in the same row as the amount.
More in IRR vs Toman for developers.
US Dollar271,860Toman+0.70%How the pieces fit together
The client, the cache and the view each have one job. The client knows how to talk to the API: base URL, key, timeout and which errors are worth retrying. The service layer knows how fresh data must be and what to do when the upstream is unavailable. The view only formats numbers that are already in the cache. Keeping these responsibilities separate makes the integration easy to test and easy to change — for example, swapping Django’s local-memory cache for Redis in production is a one-line settings change.
Running it in production
- Cache backend: use Redis (
django-redis) or Memcached so every worker shares one copy; the local-memory cache is per process. - Warm the cache: a Celery beat task or a cron-driven management command that calls
live_rates()every minute means no user request ever waits for the API. - Timeouts: keep the request timeout well below your web server’s timeout so a slow upstream cannot block workers.
- Observability: log the age of the cached
as_ofand alert when it exceeds a few minutes. - Secrets: read
EZRATE_KEYfrom the environment or a secrets manager; never put it insettings.pyunder version control.
Testing without the network
Mock the HTTP layer with responses or requests-mock, feed in a recorded JSON body and assert on what your view renders. Add one test where the API returns 503 and verify that the page still shows the last cached rates with the stale notice. These two tests catch most real-world regressions.
import responses
from rates.services import live_rates
@responses.activate
def test_live_rates_uses_cache_on_failure(settings):
responses.get("https://api.ezrate.me/v1/rates", status=503)
# prime the fallback copy first in your fixture, then:
data = live_rates()
assert data["stale"] is True
Related live rates: US dollar rate, 18K gold price, Emami coin price.

Comments (0)
Comments are shown after review. Sign-in is needed to send.


