LearnTutorial

Get exchange rates in Swift with URLSession

Build a concurrency-safe Swift client with URLSession, Codable, URLComponents, and an explicit freshness model for exchange-rate responses.

8 Sep 2026 · 6 min read

Use URLSession for the network call and Codable for the response contract. The keyless request is safe for evaluation; keep a real key on your server and never embed it in an iOS, macOS, or watchOS client bundle.

  • Use URLComponents to build the endpoint and URLSession for async networking.
  • Decode snake_case fields with JSONDecoder.keyDecodingStrategy.
  • Keep authentication on a trusted server; the mobile app can call your server instead.
  • Check HTTP status before decoding and preserve source and update timestamps.

Define the response model

The model keeps the rate map and the fields that explain the observation. data_updated_at stays a provider timestamp string so fractional-second and whole-second ISO-8601 responses are both preserved, while currency values remain Decimal for later amount calculations.

swift · ExchangeRateClient.swift
import Foundationfinal class NoRedirects: NSObject, URLSessionTaskDelegate {    func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {        completionHandler(nil)    }}struct Latest: Decodable {    let base: String    let source: String    let marketSession: String    let dataUpdatedAt: String    let rates: [String: Decimal]}struct ExchangeRateClient {    let session: URLSession    let apiKey: String?    func latest(base: String) async throws -> Latest {        let normalizedBase = base.uppercased()        guard normalizedBase.count == 3, normalizedBase.allSatisfy({ $0.isASCII && $0.isLetter }) else {            throw URLError(.badURL)        }        guard var components = URLComponents(string: "https://api.exchangerate.dev/v1/latest/\(normalizedBase)") else {            throw URLError(.badURL)        }        components.queryItems = [URLQueryItem(name: "symbols", value: "EUR,GBP,JPY")]        guard let url = components.url else { throw URLError(.badURL) }        var request = URLRequest(url: url)        request.timeoutInterval = 10        if let apiKey { request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") }        let (data, response) = try await session.data(for: request)        guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {            throw URLError(.badServerResponse)        }        let decoder = JSONDecoder()        decoder.keyDecodingStrategy = .convertFromSnakeCase        return try decoder.decode(Latest.self, from: data)    }}

Keep the key on a server

For a mobile or desktop app, call your own backend and let that backend add Authorization: Bearer .... A key placed in an app bundle can be extracted. For a server-side Swift process, read the key from ProcessInfo.processInfo.environment and pass it into the client.

swift · server setup
let key = ProcessInfo.processInfo.environment["EXCHANGERATE_API_KEY"]let session = URLSession(configuration: .ephemeral, delegate: NoRedirects(), delegateQueue: nil)let client = ExchangeRateClient(    session: session,    apiKey: key)let latest = try await client.latest(base: "USD")print(latest.rates["EUR"] as Any, latest.source, latest.dataUpdatedAt)

Handle status and freshness

Treat non-2xx responses as errors before decoding. Fix a 401 by checking authentication, and handle 429 with bounded backoff and less polling. Keep source, market_session, and data_updated_at in the model so the UI can explain whether a value is intraday or a daily reference.

Indicative data needs context

A successful HTTP response does not make a rate executable. Show the observation timestamp and source context, and keep settlement or regulated pricing on the provider responsible for that transaction.

  • Configure URLSession timeouts for the app’s network policy.
  • Cache successful observations with their timestamp.
  • Do not put API keys in Info.plist, source code, or app logs.
  • Use Decimal and an explicit rounding rule for displayed amounts.

Questions

Can I put the API key in an iOS app?

No. A key in a client bundle can be extracted. Put it on your server and let the app call a controlled backend.

Does the example work without a key?

Yes. Leave apiKey nil for evaluation, subject to anonymous access limits.

Why use Decimal instead of Double?

Decimal avoids binary floating-point artifacts when an application multiplies a rate by a money amount and rounds the result.

What does market_session tell me?

It describes the market context of the observation. Keep it beside the rate and timestamp instead of assuming every successful response is intraday.

Keep reading

Start with a free API key

5,000 requests a month on the free plan. No credit card.

Get a free key