# FastAPI with CurrencyCore and httpx

> Add one async FastAPI route that calls CurrencyCore with httpx, validates the response with Pydantic, and applies a timeout.

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

---
Create one `httpx.AsyncClient` for the application lifespan and reuse it across requests. Opening a client per request throws away the connection pool and adds TLS setup to every call. The route below forwards the API error status and validates success with Pydantic.

The official Python SDK is synchronous. This async route uses `httpx` deliberately so it does not block the event loop; a synchronous worker would use `create_client` from `currencycore` instead.

The example needs Python 3.10 or later, `fastapi`, `httpx`, and `uvicorn`. Free includes 1,000 requests per month at 60 per minute. Live keys require a paid plan; test keys use the same reference data and quota, capped at 60 per minute.

```python

from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel, ConfigDict, Field

BASE = "https://api.currency-core.com/v1"

@asynccontextmanager
async def lifespan(app: FastAPI):
    async with httpx.AsyncClient(base_url=BASE, timeout=10.0) as client:
        app.state.client = client
        yield

app = FastAPI(lifespan=lifespan)

class ConversionResult(BaseModel):
    to: str
    result: float
    rate: float

class ConvertResponse(BaseModel):
    model_config = ConfigDict(populate_by_name=True)

    from_currency: str = Field(alias="from")
    amount: float
    date: str
    results: list[ConversionResult]

@app.get("/convert", response_model=ConvertResponse)
async def convert(
    request: Request,
    from_currency: str = "USD",
    to: str = "INR",
    amount: float = 100.0,
):
    key = os.environ.get("CURRENCYCORE_API_KEY")
    if not key:
        raise HTTPException(status_code=500, detail="Set CURRENCYCORE_API_KEY")

    client: httpx.AsyncClient = request.app.state.client
    res = await client.get(
        "/convert",
        params={"from": from_currency, "to": to, "amount": amount},
        headers={"Authorization": f"Bearer {key}"},
    )

    if not res.is_success:
        raise HTTPException(status_code=res.status_code, detail=res.json())

    return res.json()
```

`from` is aliased because it is a Python keyword. The API returns one entry per target, and the base at `1` for a snapshot:

```json
{
  "from": "USD",
  "amount": 100,
  "date": "2026-11-10",
  "results": [{ "to": "INR", "result": 8345.0, "rate": 83.45 }]
}
```

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

Return the upstream status for `400`, `401`, `403`, and `429`, and preserve `error.code` in the detail. Do not retry a `400` or a `403`; the request or plan will not change on a retry.

Cache the snapshot in your own store by `base` and `date`. The API has no `ETag` or `Last-Modified`, so conditional requests do not produce a `304`.

## Next steps

- [API reference](/docs/api/) and [OpenAPI specification](/docs/openapi/)
- [Python guide](/docs/guides/python/) for a script client
- [Testing guide](/docs/guides/testing/) for fixtures and contract tests
- [All developer guides](/docs/guides/)
