Una petición a /v1/latest/USD devuelve JSON con el tipo, su origen y la hora de observación. Usa Decimal para no perder precisión al multiplicar importes.
- urllib funciona sin dependencias adicionales.
- requests simplifica autenticación, timeout y errores.
- forex-python puede ser un adaptador temporal durante una migración.
- Guarda source, effective_at y data_updated_at junto al valor.
- Para históricos, usa una fecha concreta o
/v1/rangecon paginación.
Tu primera llamada, sin dependencias
No necesitas nada fuera de la biblioteca estándar. urllib.request resuelve una llamada suelta:
Eso corre sin clave. Las llamadas anónimas tienen tope por dirección IP, así que para cualquier cosa más allá de una prueba rápida quieres una clave gratuita.
Con requests y una clave de API
La mayoría de proyectos ya usan requests. Pasa la clave como token bearer:
Una clave gratuita sube tu límite a 5,000 llamadas al mes a 10 peticiones por minuto. La cabecera X-API-Key también funciona, para plataformas que no pueden fijar Authorization.
Convierte un importe
Para convertir en vez de listar tipos, llama a /v1/convert. Devuelve el tipo y el importe convertido juntos:
Sabe cómo de fresco es un tipo
Para la moneda que muestras, consulta sources[currency] y effective_at[currency]: indican el tipo de fuente y la hora de observación. source resume los datos menos recientes de toda la respuesta. market_session describe la sesión de mercado; timestamp es la hora de generación de la respuesta, no la hora de observación.
| Valor del campo | Qué significa | Cuándo se mueve |
|---|---|---|
| source: live | Tipo spot indicativo | Intradía (~60 s), semana de negociación |
| source: ecb_daily | Fix de referencia oficial | Una vez por día hábil |
| source: market_daily | Cierre guardado sin BCE | Feed de mercado diario |
| market_session: weekend | Sábado o domingo (interbancario cerrado) | El fix de referencia arrastra el valor del viernes; el feed en vivo no se actualiza |
Los tipos se publican para referencia, análisis y visualización. No son una cotización en firme y no deben usarse para liquidar una operación. Cada respuesta lo lleva en su campo notice.
Lee un día pasado concreto
Desde 1999 con clave Plus/Pro; Free cubre 30 días. La respuesta añade is_forward_filled, que es true cuando ese día no se publicó tipo, como un fin de semana o un festivo:
Para una serie completa entre dos fechas en una sola llamada, usa /v1/range con start_date y end_date. Hay una guía aparte que lo cubre.
Migra desde forex-python
forex-python puede servir como una interfaz conocida, pero usa su propio proveedor y no se conecta automáticamente a exchangerate.dev. Al cambiar de proveedor, conserva el origen y la hora de observación de exchangerate.dev en tu modelo.
import jsonfrom decimal import Decimalimport urllib.requesturl = "https://api.exchangerate.dev/v1/latest/USD?symbols=EUR,GBP"with urllib.request.urlopen(url, timeout=10) as response: data = json.load(response, parse_float=Decimal)print(data["rates"]["EUR"])print(data["sources"]["EUR"], data["effective_at"]["EUR"])print(data["market_session"])# Install first: python -m pip install requestsimport osfrom decimal import Decimalimport requestsresponse = requests.get( "https://api.exchangerate.dev/v1/latest/USD", headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"}, timeout=10, allow_redirects=False,)if response.status_code == 429: raise RuntimeError(f"Rate limited; Retry-After: {response.headers.get('Retry-After')}")if 300 <= response.status_code < 400: raise RuntimeError("Unexpected redirect")response.raise_for_status()data = response.json(parse_float=Decimal)print(data["rates"]["GBP"], data["effective_at"]["GBP"])import osfrom decimal import Decimalimport requestsresponse = requests.get( "https://api.exchangerate.dev/v1/convert/USD/EUR/100", headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"}, timeout=10, allow_redirects=False,)if not 200 <= response.status_code < 300: raise RuntimeError(f"HTTP {response.status_code}; Retry-After: {response.headers.get('Retry-After')}")data = response.json(parse_float=Decimal)print(data["rate"], data["converted"])import osfrom decimal import Decimalimport requestsresponse = requests.get( "https://api.exchangerate.dev/v1/2026-03-14/USD?symbols=JPY", headers={"Authorization": f"Bearer {os.environ['EXCHANGERATE_API_KEY']}"}, timeout=10, allow_redirects=False,)if not 200 <= response.status_code < 300: raise RuntimeError(f"HTTP {response.status_code}; Retry-After: {response.headers.get('Retry-After')}")data = response.json(parse_float=Decimal)print(data["rates"]["JPY"], data["is_forward_filled"])