Skip to content
C# / .NET guide

Exchange rate API in C# and .NET

A typed .NET client for live and historical exchange rates: HttpClient with System.Text.Json records, registration through IHttpClientFactory, IMemoryCache with a stale fallback, and decimal conversion.

Last updated: · fxapi team

This guide shows how to call an exchange rate API in C# from any .NET 8+ application. You’ll build a typed client on HttpClient and System.Text.Json records, register it with IHttpClientFactory, cache rates with IMemoryCache, convert amounts with decimal, and handle 401, 403, 422 and 429 responses explicitly.

Setup

Get a key from the free sign-up and store it with user secrets during development:

dotnet user-secrets set "FxApi:Key" "your-api-key"

In production, set the environment variable FxApi__Key. All requests go to https://api.fxapi.com/v1/ with the key in the apikey header.

Response records with System.Text.Json

The latest, historical and convert endpoints share one response shape:

using System.Text.Json.Serialization;

public sealed record Rate(string Code, decimal Value);

public sealed record RatesMeta(
    [property: JsonPropertyName("last_updated_at")] DateTimeOffset LastUpdatedAt);

public sealed record RatesResponse(RatesMeta Meta, Dictionary<string, Rate> Data);

ReadFromJsonAsync uses web defaults (case-insensitive property names), and Value is read directly as decimal – no double round trip.

An exchange rate API client in C# with HttpClient

using System.Globalization;
using System.Net;
using System.Net.Http.Json;

public sealed class FxApiException(HttpStatusCode status, string message) : Exception(message)
{
    public HttpStatusCode Status { get; } = status;
}

public sealed class FxApiClient(HttpClient http)
{
    public Task<RatesResponse> LatestAsync(string baseCurrency = "USD", string? currencies = null,
        CancellationToken ct = default) =>
        GetAsync($"latest?base_currency={baseCurrency}{Filter(currencies)}", ct);

    public Task<RatesResponse> HistoricalAsync(DateOnly date, string baseCurrency = "USD",
        string? currencies = null, CancellationToken ct = default) =>
        GetAsync($"historical?date={date:yyyy-MM-dd}&base_currency={baseCurrency}{Filter(currencies)}", ct);

    // Requires the Basic plan or higher (403 otherwise)
    public Task<RatesResponse> ConvertAsync(decimal value, string baseCurrency, string currencies,
        CancellationToken ct = default) =>
        GetAsync($"convert?value={value.ToString(CultureInfo.InvariantCulture)}" +
                 $"&base_currency={baseCurrency}&currencies={currencies}", ct);

    private static string Filter(string? currencies) =>
        currencies is null ? "" : $"&currencies={Uri.EscapeDataString(currencies)}";

    private async Task<RatesResponse> GetAsync(string pathAndQuery, CancellationToken ct)
    {
        using var res = await http.GetAsync(pathAndQuery, ct);
        if (res.IsSuccessStatusCode)
            return (await res.Content.ReadFromJsonAsync<RatesResponse>(ct))!;

        var body = await res.Content.ReadAsStringAsync(ct);
        var quotaLeft = res.Headers.TryGetValues("X-RateLimit-Remaining-Quota-Month", out var v)
            ? v.FirstOrDefault() : null;
        throw res.StatusCode switch
        {
            HttpStatusCode.Unauthorized => new FxApiException(res.StatusCode, "Invalid API key"),
            HttpStatusCode.Forbidden => new FxApiException(res.StatusCode, "Endpoint or option not in your plan"),
            HttpStatusCode.UnprocessableEntity => new FxApiException(res.StatusCode, $"Validation error: {body}"),
            HttpStatusCode.TooManyRequests => new FxApiException(res.StatusCode,
                $"Rate limit reached, monthly quota left: {quotaLeft}"),
            _ => new FxApiException(res.StatusCode, $"Unexpected status {(int)res.StatusCode}")
        };
    }
}

The 422 body contains an errors object keyed by parameter name, which is included in the exception message. Validation and server errors don’t count against your quota; 429 means the monthly quota is used up or, on the Free plan, more than 10 requests were made in a minute.

Register with IHttpClientFactory

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<FxApiClient>(client =>
{
    client.BaseAddress = new Uri("https://api.fxapi.com/v1/");
    client.DefaultRequestHeaders.Add("apikey", builder.Configuration["FxApi:Key"]);
    client.Timeout = TimeSpan.FromSeconds(10);
});
builder.Services.AddScoped<ExchangeRateService>();

var app = builder.Build();

app.MapGet("/convert", async (decimal amount, string from, string to, ExchangeRateService fx, CancellationToken ct) =>
    Results.Ok(new { amount, from, to, result = await fx.ConvertAsync(amount, from, to, ct: ct) }));

app.Run();

Typed clients are transient, so inject them into scoped or transient services – not singletons – to let the factory rotate handlers.

Latest and historical exchange rates in C#

With fxApi being an injected FxApiClient:

var latest = await fxApi.LatestAsync("EUR", "USD,GBP,CHF");
Console.WriteLine($"{latest.Meta.LastUpdatedAt:u}  EUR/USD {latest.Data["USD"].Value}");

var hist = await fxApi.HistoricalAsync(new DateOnly(2024, 12, 31), "USD", "EUR,JPY");
Console.WriteLine(hist.Data["JPY"].Value);

Historical values are end-of-day rates in UTC from 1999-01-01 up to yesterday – see the historical exchange rates API. Leave out currencies to get all 190+ currencies in one response.

Caching with IMemoryCache and decimal conversion

Rates only change as often as your plan updates them – daily on Free, hourly on Basic, every 60 seconds on Professional and Enterprise (pricing) – so cache one all-currency response per base:

using Microsoft.Extensions.Caching.Memory;

public sealed class ExchangeRateService(FxApiClient fx, IMemoryCache cache, ILogger<ExchangeRateService> log)
{
    private static readonly TimeSpan Ttl = TimeSpan.FromHours(1); // Basic plan: hourly updates

    public async Task<RatesResponse> GetLatestAsync(string baseCurrency = "USD", CancellationToken ct = default)
    {
        var key = $"fx:latest:{baseCurrency}";
        if (cache.TryGetValue(key, out RatesResponse? cached))
            return cached!;

        try
        {
            var fresh = await fx.LatestAsync(baseCurrency, ct: ct);
            cache.Set(key, fresh, Ttl);
            cache.Set($"{key}:last-good", fresh); // no expiry: fallback copy
            return fresh;
        }
        catch (Exception ex) when (ex is FxApiException or HttpRequestException or TaskCanceledException)
        {
            log.LogWarning(ex, "fxapi request failed; serving last known rates");
            if (cache.TryGetValue($"{key}:last-good", out RatesResponse? stale))
                return stale!;
            throw;
        }
    }

    public async Task<decimal> ConvertAsync(decimal amount, string from, string to, int digits = 2,
        CancellationToken ct = default)
    {
        var rates = await GetLatestAsync(from, ct);
        return Math.Round(amount * rates.Data[to].Value, digits, MidpointRounding.ToEven);
    }
}

MidpointRounding.ToEven is banker’s rounding; use AwayFromZero if your invoicing rules require half-up. Read the correct number of decimals per currency (JPY 0, KWD 3) from /v1/currencies – details in currency rounding and precision.

IMemoryCache is per process. With several instances, use IDistributedCache backed by Redis so all nodes share one cached response, and store LastUpdatedAt with every transaction for auditability. More patterns: caching exchange rates.

Testing without spending quota

Because FxApiClient takes an HttpClient, unit tests can inject a fake handler and never touch the network:

sealed class StubHandler(string json) : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct) =>
        Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
        });
}

var json = """{"meta":{"last_updated_at":"2026-10-01T23:59:59Z"},"data":{"EUR":{"code":"EUR","value":0.9}}}""";
var client = new FxApiClient(new HttpClient(new StubHandler(json)) { BaseAddress = new Uri("https://api.fxapi.com/v1/") });
var rates = await client.LatestAsync();   // rates.Data["EUR"].Value == 0.9m

Return a 429 or 422 from the stub to test your fallback paths. For integration tests against the real API, the Professional and Enterprise plans include sandbox keys: they return random dummy values and report X-Cost: 0, so CI runs don’t consume your quota.

Official .NET package

An fxapi package is available on NuGet:

dotnet add package fxapi

We show plain HttpClient code here because it gives you async calls with cancellation, typed decimal values and full control over caching and error handling. The package’s NuGet page lists its methods.

Next steps

The examples in this guide use fxapi, a foreign exchange rate API with a free plan of 300 requests a month – no credit card required.

Frequently asked questions

Should I deserialize exchange rates as double or decimal in C#?
Use decimal. System.Text.Json reads JSON numbers straight into decimal, which avoids binary floating point errors when you multiply amounts by rates.
Why use IHttpClientFactory instead of new HttpClient()?
Creating and disposing HttpClient per request can exhaust sockets, and a single static instance can miss DNS changes. IHttpClientFactory manages handler lifetimes for you and keeps base address, headers and timeout in one place.
Where do I store the API key in ASP.NET Core?
In configuration: user secrets for development, environment variables or a secret store such as Azure Key Vault in production. Never put it in client-side Blazor WebAssembly code.
Can I convert currencies without the convert endpoint?
Yes. Fetch /v1/latest with your source currency as base_currency and multiply by the target rate. /v1/convert is available from the Basic plan.
Is there an official .NET package?
There is a fxapi package on NuGet. This guide uses HttpClient directly so you control async calls, cancellation, typing and caching.
Free plan · no credit card

Get your free exchange rate API key

300 requests a month, latest and historical rates, fluctuation and averages – free forever. Upgrade when you need faster updates or more requests.