# Django 4.2 with CurrencyCore

> A cached Django view that calls CurrencyCore with requests, propagates status, and keeps the key in server settings.

Canonical: https://currency-core.com/docs/guides/django/
Updated: 2026-11-11
Published: 2026-11-11

---
Django already has the two pieces this integration needs: a settings module for secrets and a cache framework for dated snapshots. The view below reads the key from the environment at startup, calls `/v1/rates/{base}` with `requests`, forwards the upstream status, and caches successful bodies by base and date.

The example targets Django 4.2 or later with `requests` installed. Free includes 1,000 requests per month at 60 per minute. Test keys use the same reference data and quota and are capped at 60 per minute. Keep the key in the environment, not in `settings.py` itself.

```python
# settings.py

CURRENCYCORE_API_KEY = os.environ.get("CURRENCYCORE_API_KEY", "")
CURRENCYCORE_BASE_URL = "https://api.currency-core.com/v1"
CURRENCYCORE_TIMEOUT = 10
```

```python
# views.py
from datetime import datetime, timezone

from django.conf import settings
from django.core.cache import cache
from django.http import JsonResponse
from django.views.decorators.http import require_GET

def error(code, message, status):
    return JsonResponse({"error": {"code": code, "message": message}}, status=status)

@require_GET
def rates(request, base="USD"):
    if not settings.CURRENCYCORE_API_KEY:
        return error("internal_error", "CURRENCYCORE_API_KEY is not set", 500)

    base = base.upper()
    day = request.GET.get("date", "")
    if len(base) != 3 or not base.isalpha():
        return error("invalid_input", "base must be a three-letter code", 400)
    if day and not (len(day) == 10 and day[4] == "-" and day[7] == "-"):
        return error("invalid_input", "date must be YYYY-MM-DD", 400)

    cache_key = f"cc:rates:{base}:{day or 'latest'}"
    cached = cache.get(cache_key)
    if cached is not None:
        return JsonResponse(cached)

    try:
        upstream = requests.get(
            f"{settings.CURRENCYCORE_BASE_URL}/rates/{base}",
            params={"date": day} if day else None,
            headers={"Authorization": f"Bearer {settings.CURRENCYCORE_API_KEY}"},
            timeout=settings.CURRENCYCORE_TIMEOUT,
        )
    except requests.RequestException:
        return error("internal_error", "CurrencyCore request failed", 502)

    try:
        payload = upstream.json()
    except ValueError:
        return error(
            "internal_error",
            f"Invalid JSON from upstream (HTTP {upstream.status_code})",
            502,
        )

    if upstream.status_code >= 400:
        return JsonResponse(payload, status=upstream.status_code)

    is_today = payload.get("date") == datetime.now(timezone.utc).date().isoformat()
    cache.set(cache_key, payload, 300 if is_today else 86_400)
    return JsonResponse(payload)
```

The payload is returned unchanged, so the base stays at `1` and the date is the rate date, not necessarily the one requested:

```json
{
  "base": "EUR",
  "date": "2026-11-10",
  "rates": { "EUR": 1, "USD": 1.0861, "INR": 91.62 }
}
```

Wire both the default and the rebased path:

```python
# urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("api/rates/", views.rates, name="rates"),
    path("api/rates/<str:base>/", views.rates, name="rates-base"),
]
```

A `403` for a historical date outside the Free 30-day window reaches the client with status `403` and the original `error.code`. Do not convert every failure into `200`.

## Production notes

- Never pass the key into a template context, a `data-` attribute, or client JavaScript. Django templates render HTML, and anything rendered is public.
- `LocMemCache` is per process. Use Redis or Memcached when several workers must share entries.
- Cache by base and requested date, as above. A response for a past date is stable for normal caching; today's entry should expire quickly because the feed polls hourly and the underlying fixings are daily.
- Set `CURRENCYCORE_TIMEOUT` low enough to protect a web worker, and translate `requests.Timeout` into `502`, not `500`.
- If your SPA runs on another origin, allow only that origin in CORS. The key still stays on the Django server.

## Next steps

- [API reference](/docs/api/) and [authentication](/docs/authentication/)
- [Error codes](/docs/errors/) and [rate limits](/docs/rate-limits/)
- [Observability guide](/docs/guides/observability/) for logging and timing
- [All developer guides](/docs/guides/)
