Skip to content

IPWhois/ipwhois-swift

Repository files navigation

ipwhois-swift

CI Swift Version Platforms License

Official, dependency-free Swift client for the ipwhois.io IP Geolocation API.

  • ✅ Single and bulk IP lookups (IPv4 and IPv6)
  • ✅ Works with both the Free and Paid plans
  • ✅ HTTPS by default
  • ✅ Localisation, field selection, threat detection, rate info
  • ✅ Never throws — all errors returned as Result.failure(IPWhoisError)
  • ✅ Async/await native, with a polyfill for iOS 13/14 and Linux
  • ✅ No external dependencies — pure Foundation
  • ✅ Swift 5.10+, iOS 13+, macOS 10.15+, tvOS 13+, watchOS 6+, Linux

Installation

Swift Package Manager

Add the dependency to your Package.swift:

.package(url: "https://github.com/IPWhois/ipwhois-swift.git", from: "1.2.0")

and the product to your target:

.target(name: "MyApp", dependencies: [
    .product(name: "IPWhois", package: "ipwhois-swift"),
]),

Or in Xcode: File → Add Package Dependencies…, paste the repository URL, and pick the IPWhois library.

Free vs Paid plan

The same IPWhois class is used for both plans. The only difference is whether you pass an API key:

  • Free plan — create the client without arguments. No API key, no signup required. Suitable for low-traffic and non-commercial use.
  • Paid plan — create the client with your API key from https://ipwhois.io. Higher limits, plus access to bulk lookups and threat-detection data.
let free = IPWhois()                       // Free plan — no API key
let paid = IPWhois(apiKey: "YOUR_API_KEY") // Paid plan — with API key

Everything else (lookup(), options, error handling) is identical.

Quick start — Free plan (no API key)

import IPWhois

let ipwhois = IPWhois() // no API key

switch await ipwhois.lookup("8.8.8.8") {
case .success(let info):
    print("\(info.country ?? "?") \(info.flag?.emoji ?? "")")
    // → United States 🇺🇸
    print("\(info.city ?? "?"), \(info.region ?? "?")")
    // → Mountain View, California
case .failure(let error):
    print("Lookup failed: \(error.message)")
}

Quick start — Paid plan (with API key)

Get an API key at https://ipwhois.io and pass it to the constructor:

import IPWhois

let ipwhois = IPWhois(apiKey: "YOUR_API_KEY") // with API key

if case .success(let info) = await ipwhois.lookup("8.8.8.8") {
    print("\(info.country ?? "?") \(info.flag?.emoji ?? "")")
    print("\(info.city ?? "?"), \(info.region ?? "?")")
}

ℹ️ Call lookup() with no arguments (or pass nil) to look up your own public IP — works on both plans.

Lookup options

Every option below can be passed per call, or set once on the client as a default.

Option Type Plans needed Description
lang String Free + Paid One of: en, ru, de, es, pt-BR, fr, zh-CN, ja
fields [String] Free + Paid Restrict the response to specific fields (e.g. ["country", "city"])
rate Bool Basic and above Include the rate block (limit, remaining)
security Bool Business and above Include the security block (proxy/vpn/tor/hosting)

Setting defaults once

Every option can be passed two ways: per call (as the options argument to lookup(_:options:) / bulkLookup(_:options:)) or once as a default on the client. Per-call options always override the defaults, so it's safe to set sensible defaults and only override what differs for a specific call.

Defaults are set with fluent setters — setLanguage(), setFields(), setSecurity(), setRate(), setTimeout(), setConnectTimeout(), setUserAgent() — and can be chained:

// Free plan
let ipwhois = IPWhois()
    .setLanguage("en")
    .setFields(["success", "country", "city", "flag.emoji"])
    .setTimeout(8)
// Paid plan
let ipwhois = IPWhois(apiKey: "YOUR_API_KEY")
    .setLanguage("en")
    .setFields(["success", "country", "city", "flag.emoji"])
    .setTimeout(8)

Either client behaves the same way at call time — per-call options always win over the defaults:

await ipwhois.lookup("8.8.8.8")                                   // uses lang=en, the field whitelist, and timeout=8
await ipwhois.lookup("1.1.1.1", options: .init(lang: "de"))       // overrides lang for this single call only

⚠️ When you restrict fields with setFields() (or the per-call fields option), the API only returns the fields you ask for. Include "success" in the list if you also want the success flag echoed back — otherwise it will be missing on responses.

ℹ️ setSecurity(true) requires Business+ and setRate(true) requires Basic+. See the table above for what's available where.

HTTPS Encryption

By default, all requests are sent over HTTPS. If you need to disable it (for example, in environments without an up-to-date CA bundle), pass ssl: false in the client options:

// Free plan
let ipwhois = IPWhois(options: .init(ssl: false))
// Paid plan
let ipwhois = IPWhois(apiKey: "YOUR_API_KEY", options: .init(ssl: false))

ℹ️ HTTPS is strongly recommended for production traffic — your API key is sent in the query string and would otherwise travel in clear text.

Bulk lookup (Paid plan only)

The bulk endpoint sends up to 100 IPs in a single GET request. Each address counts as one credit. Available on the Business and Unlimited plans.

let ipwhois = IPWhois(apiKey: "YOUR_API_KEY")

let results = await ipwhois.bulkLookup([
    "8.8.8.8",
    "1.1.1.1",
    "208.67.222.222",
    "2c0f:fb50:4003::",  // IPv6 is fine — mix freely
])

guard case .success(let rows) = results else {
    if case .failure(let error) = results {
        print("Bulk request failed: \(error.message)")
    }
    return
}

for row in rows {
    switch row {
    case .failure(let error):
        // Per-IP errors (e.g. "Invalid IP address") are returned inline,
        // they do NOT throw — the rest of the batch is still usable.
        print("skip \(error.ip ?? "?"): \(error.message)")
    case .success(let info):
        print("\(info.ip ?? "?")\(info.country ?? "?")")
    }
}

ℹ️ Bulk requires an API key. Calling bulkLookup() without one returns .failure(IPWhoisError) with category == .invalidArgument — no request is sent.

Error handling

The library never throws. Every failure — invalid IP, bad API key, rate limit, network outage, bad options — comes back as the .failure case of a Result. Just switch on the result of every call:

switch await ipwhois.lookup("8.8.8.8") {
case .failure(let error):
    NSLog("Lookup failed: %@", error.message)
    return
case .success(let info):
    print(info.country ?? "?")
}

This means an outage of the ipwhois.io API (or of your device's DNS, connection, etc.) will never surface as a fatal error in your application — you decide how to react.

Error response fields

Every IPWhoisError carries a category so you can branch on the kind of failure, plus optional metadata where the API provides it:

Field When it's present
category Always — one of .api, .network, .environment, or .invalidArgument
message Always — human-readable description of what went wrong
httpStatus On HTTP 4xx / 5xx responses
retryAfter On HTTP 429 — free plan only (the paid endpoint does not send a Retry-After header)
ip For per-IP errors inside a bulk response
switch await ipwhois.lookup("8.8.8.8") {
case .failure(let error):
    if error.httpStatus == 429 {
        try? await Task.sleep(nanoseconds: UInt64(error.retryAfter ?? 60) * 1_000_000_000)
        // …retry
    }
    if error.category == .network {
        // DNS failure, connection refused, timeout, …
    }
    NSLog("Error: %@", error.message)
    return
case .success(let info):
    // …
    break
}

Response shape

A successful response is decoded into IPWhoisResponse. All fields are optional because the API allows you to restrict the response to specific fields with setFields() / options.fields.

public struct IPWhoisResponse: Decodable, Equatable, Sendable {
    public let ip: String?
    public let type: String?
    public let continent: String?
    public let continentCode: String?
    public let country: String?
    public let countryCode: String?
    public let region: String?
    public let regionCode: String?
    public let city: String?
    public let latitude: Double?
    public let longitude: Double?
    public let isEU: Bool?
    public let postal: String?
    public let callingCode: String?
    public let capital: String?
    public let borders: String?
    public let flag: Flag?
    public let connection: Connection?
    public let timezone: Timezone?
    public let currency: Currency?
    public let security: Security?
    public let rate: Rate?
}

For the full field reference, see the official documentation.

Concurrency notes

  • All network methods (lookup, bulkLookup) are async.
  • IPWhois is @unchecked Sendable — internal mutable state (the defaults set via the fluent setters) is protected by an NSLock, so calling setters and lookups from different tasks is safe.
  • On iOS 15 / macOS 12 (and newer) the library uses the native URLSession.data(for:) async API. On older platforms and on swift-corelibs-foundation (Linux), it falls back to a continuation-based bridge over the callback-based dataTask(with:completionHandler:).

Requirements

  • Swift 5.10 or newer
  • iOS 13+ / macOS 10.15+ / tvOS 13+ / watchOS 6+ / Linux

Contributing

Issues and pull requests are welcome on GitHub.

License

MIT © ipwhois.io

Packages

 
 
 

Contributors

Languages