If you are building a payments backend, a SaaS billing engine, or an enterprise pricing service on the Microsoft stack, integrating a currency exchange rate API in C# / .NET is one of the highest-leverage tasks you can ship this week. Modern .NET gives you everything you need out of the box — HttpClient, IHttpClientFactory, System.Text.Json, IMemoryCache, and first-class dependency injection — and yet most tutorials still show a single WebClient.DownloadString snippet that would fail any production code review. This guide takes you all the way from your first request to a typed, cached, resilient client that you can drop into ASP.NET Core, a Blazor app, a WPF desktop tool, or an Azure Function.
By the end of this tutorial you will have a reusable Finexly client for the Finexly API documentation, a small console converter, and an ASP.NET Core minimal API that serves real-time exchange rates to the rest of your stack — all written in idiomatic .NET 8 / .NET 9 with the patterns Microsoft itself recommends.
Why C# / .NET Is a Strong Choice for Currency Integrations
Currency conversion in production is mostly an I/O problem: fetch rates, cache them, multiply some decimals, return JSON. The .NET runtime has been optimized for exactly that workload over the last decade:
IHttpClientFactorysolves socket exhaustion. It poolsHttpMessageHandlerinstances so you can injectHttpClienteverywhere without leaking sockets — the single biggest source of intermittent currency-API failures we see in legacy .NET code.System.Text.Jsonis fast and allocation-light. It deserializes a typical 170-currency response in well under a millisecond, with no third-party dependency.decimalis built into the language. You never need a third-partyBigDecimalto handle money correctly — and that matters when you are summing thousands of converted invoice lines.- Polly is the de facto resilience library. Retries, circuit breakers, and timeouts are a single
AddPolicyHandlercall away. - Minimal APIs and AOT. Spinning up a tiny rates microservice in 30 lines is now realistic, and it cold-starts in milliseconds.
If you are evaluating other stacks, we have parallel guides for Node.js, Python, Go, and PHP. This article is specifically for C# and assumes you are comfortable with async/await, generic hosting, and dependency injection.
Prerequisites and Project Setup
You need .NET 8 or .NET 9 installed. Confirm your SDK:
dotnet --versionCreate a new solution with a class library for the client and a console app to exercise it:
mkdir Finexly.Sample && cd Finexly.Sample
dotnet new sln -n Finexly.Sample
dotnet new classlib -n Finexly.Client
dotnet new console -n Finexly.ConsoleApp
dotnet sln add Finexly.Client/Finexly.Client.csproj
dotnet sln add Finexly.ConsoleApp/Finexly.ConsoleApp.csproj
dotnet add Finexly.ConsoleApp/Finexly.ConsoleApp.csproj reference Finexly.Client/Finexly.Client.csprojYou also need a Finexly API key. Sign up for free — no credit card required, and the free tier ships with 1,000 requests per month, which is plenty for development and CI.
Never hard-code the key. Set it as an environment variable:
# macOS / Linux
export FINEXLY_API_KEY="your_api_key_here"# Windows PowerShell
$env:FINEXLY_API_KEY = "your_api_key_here"In production, store it in Azure Key Vault, AWS Secrets Manager, or dotnet user-secrets for local development.
Your First Request with HttpClient
Let us start with the simplest working example. Open Finexly.ConsoleApp/Program.cs and write:
using System.Net.Http;
using System.Net.Http.Json;
var apiKey = Environment.GetEnvironmentVariable("FINEXLY_API_KEY")
?? throw new InvalidOperationException("FINEXLY_API_KEY not set");
using var http = new HttpClient
{
BaseAddress = new Uri("https://api.finexly.com/v1/")
};
var url = $"latest?base=USD&apikey={apiKey}";
var payload = await http.GetFromJsonAsync<Dictionary<string, object>>(url);
Console.WriteLine(payload?["rates"]);Run it:
dotnet run --project Finexly.ConsoleAppYou will see a dictionary of currency codes and their exchange rates against USD. This works, but it has every smell you would expect from a 10-line example: untyped object values, no caching, no retries, a manually instantiated HttpClient, and the API key in the URL. Let us fix all of that.
Defining Strongly Typed Response Models
System.Text.Json deserializes cleanly into C# record types. In the Finexly.Client project, create Models/LatestRatesResponse.cs:
using System.Text.Json.Serialization;
namespace Finexly.Client.Models;
public sealed record LatestRatesResponse(
[property: JsonPropertyName("base")] string Base,
[property: JsonPropertyName("date")] DateOnly Date,
[property: JsonPropertyName("rates")] IReadOnlyDictionary<string, decimal> Rates
);A few details worth noting:
decimalinstead ofdouble. Floating-point error is unacceptable for money.decimalgives you 28–29 significant digits and exact base-10 arithmetic.IReadOnlyDictionary. The response is read-only — express that in the type.DateOnly. Modern .NET ships a proper date type. Use it instead ofDateTimewhen there is no time component.
Now we can deserialize without any dynamic or Dictionary<string, object> gymnastics.
Building a Typed HttpClient with IHttpClientFactory
The recommended .NET pattern is a typed client registered through IHttpClientFactory. It gives you connection pooling, configurable handlers, and easy DI. Create FinexlyClient.cs in the Finexly.Client project:
using System.Net.Http.Json;
using Finexly.Client.Models;
namespace Finexly.Client;
public sealed class FinexlyClient
{
private readonly HttpClient _http;
private readonly string _apiKey;
public FinexlyClient(HttpClient http, FinexlyOptions options)
{
_http = http;
_apiKey = options.ApiKey;
_http.BaseAddress = new Uri("https://api.finexly.com/v1/");
_http.DefaultRequestHeaders.Add("X-Api-Key", _apiKey);
_http.Timeout = TimeSpan.FromSeconds(5);
}
public async Task<LatestRatesResponse> GetLatestRatesAsync(
string baseCurrency = "USD",
IEnumerable<string>? symbols = null,
CancellationToken cancellationToken = default)
{
var query = $"latest?base={baseCurrency}";
if (symbols is not null)
{
query += $"&symbols={string.Join(",", symbols)}";
}
var response = await _http.GetFromJsonAsync<LatestRatesResponse>(
query, cancellationToken);
return response
?? throw new InvalidOperationException("Empty response from Finexly");
}
public async Task<decimal> ConvertAsync(
string from, string to, decimal amount,
CancellationToken cancellationToken = default)
{
var rates = await GetLatestRatesAsync(from, new[] { to }, cancellationToken);
if (!rates.Rates.TryGetValue(to, out var rate))
{
throw new ArgumentException($"Unknown currency: {to}");
}
return amount * rate;
}
}
public sealed class FinexlyOptions
{
public required string ApiKey { get; init; }
}A few things to call out:
- The API key goes in a header, not the URL — safer for logs, proxies, and traces.
- A 5-second
Timeoutprevents a stalled currency API from cascading into a slow checkout. - The client takes a
CancellationTokenon every method — this is non-negotiable in modern ASP.NET. - It uses
GetFromJsonAsync<T>, which combines the GET, status check, and deserialization in one allocation-friendly call.
Registering the Client in Dependency Injection
In your Program.cs (Console, ASP.NET Core, Worker — same API everywhere), wire it up:
using Finexly.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton(new FinexlyOptions
{
ApiKey = Environment.GetEnvironmentVariable("FINEXLY_API_KEY")!
});
builder.Services.AddHttpClient<FinexlyClient>();
using var host = builder.Build();
var finexly = host.Services.GetRequiredService<FinexlyClient>();
var eur = await finexly.ConvertAsync("USD", "EUR", 100m);
Console.WriteLine($"100 USD = {eur:0.00} EUR");AddHttpClient<FinexlyClient>() registers FinexlyClient as a transient service and gives it a pooled HttpClient from the factory. You can now inject FinexlyClient into any controller, minimal API endpoint, or background service.
Adding Retries and a Circuit Breaker with Polly
External APIs occasionally drop a request, throttle, or hiccup at the network layer. Polly is the standard .NET resilience library and integrates with IHttpClientFactory in two lines.
Install the package:
dotnet add Finexly.ConsoleApp package Microsoft.Extensions.Http.PollyWire up a retry-with-backoff and a circuit breaker:
using Polly;
using Polly.Extensions.Http;
builder.Services.AddHttpClient<FinexlyClient>()
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(r => (int)r.StatusCode == 429)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt =>
TimeSpan.FromMilliseconds(200 * Math.Pow(2, attempt))))
.AddPolicyHandler(HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
handledEventsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30)));This single registration retries on 5xx, 408, and 429 with exponential backoff (200ms, 400ms, 800ms), and trips a circuit breaker if the API is fully down — so your checkout page degrades gracefully instead of holding open connections for minutes.
Caching Responses with IMemoryCache
Live exchange rates rarely change more than once per minute, so a small in-process cache typically reduces upstream calls by 95% or more. .NET ships IMemoryCache in the box.
dotnet add Finexly.Client package Microsoft.Extensions.Caching.MemoryCreate CachedFinexlyClient.cs:
using Finexly.Client.Models;
using Microsoft.Extensions.Caching.Memory;
namespace Finexly.Client;
public sealed class CachedFinexlyClient
{
private readonly FinexlyClient _inner;
private readonly IMemoryCache _cache;
private static readonly TimeSpan DefaultTtl = TimeSpan.FromSeconds(60);
public CachedFinexlyClient(FinexlyClient inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public Task<LatestRatesResponse> GetLatestRatesAsync(
string baseCurrency,
CancellationToken cancellationToken = default)
{
var key = $"finexly:latest:{baseCurrency}";
return _cache.GetOrCreateAsync(key, entry =>
{
entry.AbsoluteExpirationRelativeToNow = DefaultTtl;
return _inner.GetLatestRatesAsync(baseCurrency,
cancellationToken: cancellationToken);
})!;
}
}Register it alongside IMemoryCache:
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<CachedFinexlyClient>();A 60-second TTL is a great default for checkout pricing, billing previews, and admin dashboards. For historical data, bump the TTL to hours or days — historical rates do not change.
A Working Console Currency Converter
Putting it all together, here is a small CLI that mirrors currencyconverter --amount 100 --from USD --to JPY:
using Finexly.Client;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddSingleton(new FinexlyOptions
{
ApiKey = Environment.GetEnvironmentVariable("FINEXLY_API_KEY")!
});
builder.Services.AddHttpClient<FinexlyClient>();
builder.Services.AddSingleton<CachedFinexlyClient>();
using var host = builder.Build();
decimal amount = 1m;
string from = "USD", to = "EUR";
for (int i = 0; i < args.Length - 1; i++)
{
switch (args[i])
{
case "--amount": amount = decimal.Parse(args[i + 1]); break;
case "--from": from = args[i + 1].ToUpperInvariant(); break;
case "--to": to = args[i + 1].ToUpperInvariant(); break;
}
}
var finexly = host.Services.GetRequiredService<CachedFinexlyClient>();
var rates = await finexly.GetLatestRatesAsync(from);
if (!rates.Rates.TryGetValue(to, out var rate))
{
Console.Error.WriteLine($"Unknown currency: {to}");
return 1;
}
Console.WriteLine($"{amount:0.00} {from} = {amount * rate:0.00} {to}");
return 0;Build a single-file binary you can drop on any server:
dotnet publish Finexly.ConsoleApp -c Release -r linux-x64 \
--self-contained -p:PublishSingleFile=trueExposing Rates with an ASP.NET Core Minimal API
For most teams, the right architecture is an internal rates microservice that wraps the upstream API, adds caching, and gives every other service a fast, free, in-VPC endpoint. ASP.NET Core minimal APIs make this a 30-line file:
using Finexly.Client;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddSingleton(new FinexlyOptions
{
ApiKey = builder.Configuration["Finexly:ApiKey"]!
});
builder.Services.AddHttpClient<FinexlyClient>();
builder.Services.AddSingleton<CachedFinexlyClient>();
var app = builder.Build();
app.MapGet("/rates", async (
string? @base,
CachedFinexlyClient finexly,
CancellationToken ct) =>
{
var rates = await finexly.GetLatestRatesAsync(@base ?? "USD", ct);
return Results.Ok(rates);
});
app.MapGet("/convert", async (
string from, string to, decimal amount,
CachedFinexlyClient finexly,
CancellationToken ct) =>
{
var rates = await finexly.GetLatestRatesAsync(from, ct);
return rates.Rates.TryGetValue(to, out var rate)
? Results.Ok(new { from, to, amount, converted = amount * rate })
: Results.NotFound(new { error = $"Unknown currency {to}" });
});
app.Run();Deploy this behind your normal API gateway and any service in your fleet can call GET /convert?from=USD&to=EUR&amount=99.95 with sub-10-millisecond latency on a cache hit. For a browser-side experience, the same data is available in our hosted currency converter.
Handling Money Correctly in C#
A few defensive patterns that pay for themselves the first time you audit a payments ledger:
- Always use
decimal, neverdoubleorfloat.0.1 + 0.2is famously not0.3in floating-point — and that single rounding error can cost you a chargeback dispute. - Round at presentation, not at calculation. Keep full precision through every multiplication and only
Math.Round(value, 2, MidpointRounding.ToEven)when you display or persist. - Pin the rounding mode. Banker's rounding (
ToEven) is the financial-industry default in IEEE 754 and ISO 80000-1. - Treat currencies as opaque codes. Use ISO 4217 strings — never assume a currency has two decimal places. JPY has zero, BHD has three. Our ISO 4217 guide covers the edge cases.
Production Best Practices
A few extra patterns separate a hobby integration from a system you can run for years:
- Set tight HTTP timeouts. A default 100-second
HttpClient.Timeoutwill block your thread pool under load. Five seconds is plenty for a currency API. - Wrap every external call in a
CancellationToken. ASP.NET Core passes one in for free; honor it so dropped requests do not keep talking to the upstream. - Log status codes, not bodies. Currency-API responses are too large to log usefully and may contain rate data you do not want in Splunk.
- Monitor your quota. Compare pricing plans and set CloudWatch / Application Insights alerts at 80% of your monthly cap.
- Pin the currencies you actually use. Sending
symbols=EUR,GBP,JPYshrinks the response from ~10 KB to under 200 bytes. - Cache aggressively. Latency-sensitive checkouts can run on a 30-second cache; admin dashboards can run on 5 minutes.
If you are evaluating providers, our free vs paid currency API comparison walks through accuracy, uptime, and pricing across the major options.
Frequently Asked Questions
Which currency API works best with C# and .NET?
Any REST API with clean JSON and stable uptime will integrate well via IHttpClientFactory. Finexly is specifically designed to be language-agnostic — the same endpoints are consumed by Node, Python, Go, Java, and .NET clients with no SDK lock-in. See our free currency API overview for context.
Should I use Newtonsoft.Json or System.Text.Json?
For new code on .NET 6+, use System.Text.Json. It is 2–3× faster, allocates less, and is part of the base class library, so there is no extra package to keep up to date. Newtonsoft.Json is still excellent and worth keeping in legacy code, but greenfield C# currency integrations should default to System.Text.Json.
Do I need a NuGet SDK or is HttpClient enough? A hand-written typed client (about 50 lines of C#) is almost always the right answer. It gives you full control over caching, retries, telemetry, and serialization, and avoids the version-skew risk that comes with any third-party SDK. The example in this tutorial is production-ready.
How often should I refresh exchange rates in a .NET application? For most use cases — e-commerce checkouts, SaaS billing, invoicing — a 30- to 60-second cache is safe. Trading systems obviously need real-time data, but for everything else, minute-level freshness is invisible to users and dramatically reduces your API spend.
Can I run this in Azure Functions or AWS Lambda?
Yes. IHttpClientFactory works the same way in isolated-process Azure Functions and Microsoft.Extensions.Hosting Lambda handlers. For cold-start-sensitive workloads, publish with AOT (PublishAot=true) and your function will start in under 100 ms.
Is decimal slow compared to double?
decimal is about 10–20× slower than double per arithmetic operation, but a currency conversion does at most a handful of multiplications. You will never measure the difference in a real workload — and the correctness gain is non-negotiable for money.
Wrapping Up
You now have a complete, production-grade blueprint for integrating a currency exchange rate API in C# / .NET: strongly typed models, an IHttpClientFactory-backed client, Polly-based retries and a circuit breaker, in-process caching, a console converter, and an ASP.NET Core minimal API. Every pattern here is straight out of the official Microsoft guidance and battle-tested in real fintech production code.
Ready to ship real-time rates into your .NET application? Get your free Finexly API key — no credit card required. Start with 1,000 free requests per month and upgrade as your product grows. If you want to see how Finexly stacks up against other providers first, read our comparison of currency APIs.
Explore More
Vlado Grigirov
Senior Currency Markets Analyst & Financial Strategist
Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...
View full profile →