Tutorial/Swift quickstart

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.

ERexchangerate.dev·Sep 8, 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.

Key points
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.swiftcopy
import Foundation

final 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 setupcopy
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.
ER
exchangerate.dev
Integration guides for developers building with FX data.

Keep reading

ReferenceExchange rate API quickstartRead TutorialExchange rates in JavaScript and Node.jsRead GuideIndicative vs executable FX ratesRead
More ComparisonsFixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.devCurrencylayer vs ExchangeRate-API
LearnReading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB reference rates, explained
Live RatesEUR/USDGBP/USDUSD/JPY