Prerequisites
- Swift 5.9 or later (Xcode 15+, or the Swift toolchain on Linux)
- A Swift package or app target to add the dependency to
- An ip-api.io API key — get one free
Add the package
Add the official ipapi-swift package via Swift Package Manager — in Xcode
(File → Add Packages) or in your Package.swift. It has zero dependencies.
// Package.swift
dependencies: [
.package(url: "https://github.com/ip-api-io/ipapi-swift.git", from: "1.0.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [.product(name: "IpApiIo", package: "ipapi-swift")]
)
] Construct the client
Import the module and build one IpApiClient with your API key. Read the key
from the environment — never hardcode it. The API rejects keyless requests with
401.
import IpApiIo
let apiKey = ProcessInfo.processInfo.environment["IP_API_IO_KEY"]!
let client = IpApiClient(apiKey: apiKey)
// ... use the client ...
IpApiClient is backed by a shared URLSession — construct one
and reuse it across your app instead of creating a new client per request.
Look up an IP address
Call lookup(ip:) for any IPv4 or IPv6 address, or lookup() to
resolve the caller's own IP. Responses are Codable structs; nullable fields
are optionals — unwrap before use.
let info = try await client.lookup(ip: "8.8.8.8")
print(info.ip) // "8.8.8.8"
print(info.isp ?? "") // "Google LLC"
print(info.location?.country ?? "") // "United States"
print(info.suspiciousFactors.isDatacenter) // true
// Resolve the caller's own IP:
let me = try await client.lookup()
print(me.ip) country and city are optionals because they're absent for
private ranges or unrecognized addresses — use if let or ??.
Boolean flags such as info.suspiciousFactors.isVpn are plain Bool.
Detect VPN, proxy, Tor & batch lookups
The suspiciousFactors struct carries seven boolean fields on every
lookup — this is what sets ip-api.io apart from plain geolocation APIs. You get
security intelligence in the same call, at no extra cost.
let info = try await client.lookup(ip: "185.220.101.45") // example Tor exit node
let f = info.suspiciousFactors
if f.isVpn || f.isProxy || f.isTorNode {
print("Anonymized traffic — consider a CAPTCHA or block")
}
if f.isDatacenter {
print("Cloud/datacenter IP — likely automated")
}
if f.isThreat {
print("Active threat signal — block this request")
}
let risky = f.isVpn || f.isProxy || f.isTorNode || f.isThreat
Need to check many IPs at once? lookupBatch(ips:) takes up to 100 addresses
in a single request:
let batch = try await client.lookupBatch(ips: ["8.8.8.8", "1.1.1.1", "9.9.9.9"])
print(batch.totalProcessed, batch.successfulLookups)
for (ip, info) in batch.results {
print(ip, info.suspiciousFactors.isVpn)
} try await client.riskScore(ip: ip) for a combined
fraud risk score from
0 to 100.
Production-ready error handling
Methods throw IpApiError. switch over the cases — the client
never retries on its own, so on a rate limit the .rateLimit
case's reset tells you exactly when to try again.
do {
let info = try await client.lookup(ip: "8.8.8.8")
print(info.ip)
} catch let error as IpApiError {
switch error {
case .rateLimit(let limit, let remaining, let reset):
print("Rate limited. \(remaining)/\(limit). Resets at \(reset)")
case .authentication:
print("Invalid or missing API key — get one free at https://ip-api.io")
case .invalidRequest(let message):
print("Bad request: \(message)")
case .server(let status):
print("ip-api.io is having issues (status \(status)) — try again later")
case .transport(let underlying):
print("transport / decode error: \(underlying)")
}
} .rateLimit case's reset (a Unix timestamp) to schedule
the next attempt. Transport failures (DNS, connect, timeout, decode) surface as
.transport rather than an API status error.
In a Vapor route, run the lookup server-side and pass the real client IP from the
X-Forwarded-For header:
app.get("check") { req async -> [String: String?] in
let clientIP = req.headers.first(name: "X-Forwarded-For")?
.split(separator: ",").first
.map { $0.trimmingCharacters(in: .whitespaces) }
?? req.remoteAddress?.ipAddress
?? "0.0.0.0"
do {
let info = try await client.lookup(ip: clientIP)
return [
"ip": info.ip,
"country": info.location?.country,
"isVpn": String(info.suspiciousFactors.isVpn)
]
} catch {
// Treat ip-api.io as a non-critical dependency
return ["ip": clientIP, "country": nil]
}
}