Skip to content
InternetData

Swift

The official Swift client library for the InternetData database API.

See on GitHub

Getting Started

Add the package to your Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/internetdata/sdk-swift.git", from: "2.9.1"),
]

and the library to your target. The repository ends in sdk-swift, which is the package name SwiftPM derives, but the library it exposes is InternetData:

swift
.target(
    name: "YourTarget",
    dependencies: [.product(name: "InternetData", package: "sdk-swift")],
)

Requires Swift 6.1 or newer, and macOS 13, iOS 16, tvOS 16, watchOS 9 or visionOS 1 on Apple platforms. Linux is supported on any distribution the Swift toolchain runs on.

Usage

Every database published today needs an API key carrying the db.download scope; create one in the console. The Open databases are licensed to every organization from the start and are free under CC BY-SA 4.0, and list() marks them open. The rest are licensed by contract, one family at a time: if your organization doesn't have a license yet, request an evaluation. apiKey is nevertheless optional - a client built without one sends no Authorization header at all, ready for a database served without a license.

swift
import InternetData

let client = InternetDataClient(apiKey: ProcessInfo.processInfo.environment["INTERNETDATA_API_KEY"]!)

for database in try await client.database.list() {
    print(database.base, database.standing, database.versions.map(\.id))
    // vpn_ip  licensed  ["vpn_ip_v1"]
}

The database calls live under client.database, where the sibling VPNDetection client spells the same seven the same way, so a codebase holding both does not have to remember which one is flat.

A license is held against a FAMILY (vpn_ip), while a download names a VERSION (vpn_ip_v1), so the IDs everything below takes come from versions. Old versions are frozen rather than migrated, so both stay downloadable.

Every setting has a default, and InternetDataClient.Options is where you change one:

swift
let client = InternetDataClient(options: .init(apiKey: key, retries: 4))

Each attempt is abandoned after 30 seconds by default (timeout on the options), which fails as a retryable network error. A download is timed only until object storage starts answering, so a large file is never cut off.

From 2.2.0, list, metadata, checksums, downloads and downloadURL each take a timeout of their own, bounding each attempt at that one call in place of the client's:

swift
let databases = try await client.database.list(timeout: .seconds(5))

The transfers deliberately take none, so there is nothing to pass and a call that tries does not compile rather than accepting the option and quietly doing nothing with it: a database runs to gigabytes and minutes, so any bound that suits a JSON call would abandon a healthy download. downloadURL does take one, because minting the link is an ordinary API request - it bounds that request, not whatever you do with the link afterwards.

What is inside a database

metadata carries the build date, the row count, the columns and the size of every format, without downloading anything. Poll it to decide whether today's build is worth fetching, and read size to budget the transfer:

swift
let metadata = try await client.database.metadata(id: "vpn_ip_v1")

print(metadata.updated)              // "2026-09-04"
print(metadata.entries)              // 3_214_887
print(metadata.size["mmdb"])         // Optional(48_213_504)
print(metadata.schema["csvgz"]?.map(\.name) ?? [])
print(metadata.sample["csvgz"]?.first ?? [:])

Not every database is built in every format: the _provider catalogs are keyed by provider ID rather than by IP range, so asking them for an MMDB is a badRequest rather than a gap. DatabaseVersion.formats says which exist.

Downloading

download streams straight to disk, so nothing bigger than a single chunk is ever held in memory whatever the file weighs. It writes a neighbouring .part file and renames it on success, so a transfer that dies half way leaves nothing that reads as a whole database:

swift
let written = try await client.database.download(
    "vpn_ip_v1", format: .mmdb,
    to: URL(fileURLWithPath: "vpn_ip_v1.mmdb"),
)
print("\(written) bytes")

Hand it a closure instead when the bytes are going somewhere other than a file: a parser, an archive, another socket. The closure is awaited, so a slow sink slows the transfer rather than queueing behind it:

swift
try await client.database.download("vpn_ip_v1", format: .csvgz) { chunk in
    try await gunzip.write(chunk)
}

downloadBytes hands the whole file back at once. It holds all of it in memory, and the catalog spans seven orders of magnitude, so reach for it at the small end and use download for anything you have not measured:

swift
let bytes = try await client.database.downloadBytes("bogon_asn_v1", format: .csvgz)

The API answers a download with a 302 to a time-limited link that carries its own signature, so downloadURL gives you something you can pass to a job runner, a CDN or a shell script that holds no API key at all:

swift
let url = try await client.database.downloadURL(id: "vpn_ip_v1", format: .mmdb)

The link authorizes the START of a transfer, so one already running is not interrupted when it lapses.

Downloading a named build

Each download call fetches the build that is live unless you name one. A database.updated webhook names the build it announces and carries its sha256, so a receiver that downloads after the next publish asks for that build to get the file the hash is of:

swift
try await client.database.download("vpn_ip_v1", format: .mmdb, build: build, to: destination)

downloadURL and downloadBytes take build: the same way. A build is kept 7 days. Past that the call throws badRequest with the message BUILD_GONE, and downloading without a build fetches the live one.

Verifying a download

checksums returns the whole published digest set for one file rather than a single algorithm:

swift
let checksums = try await client.database.checksums(id: "vpn_ip_v1", format: .mmdb)
print(checksums.sha256)

Recent download attempts

downloads lists what your organization has fetched, newest first. Refusals are listed too, because a denial is what answers "it stopped working" and its absence answers nothing:

swift
for attempt in try await client.database.downloads(limit: 20) {
    print(attempt.created, attempt.datasetId, attempt.outcome, attempt.bytes ?? 0)
}

Errors

Failures throw an InternetDataError carrying a kind, the API's own rc as its message, and an isRetryable flag:

swift
do {
    _ = try await client.database.metadata(id: "vpn_ip_v1")
} catch let error as InternetDataError {
    print(error.kind, error.message, error.isRetryable)
}

kind is one of badRequest, unauthorized, forbidden, rateLimited, quotaExceeded, serverError or network.

rateLimited is a 429 that carries Retry-After, and the library retries it for you, waiting as long as it says. quotaExceeded is a 429 without one, which it never retries. InternetData's database API has no quota, so you won't see quotaExceeded from it.

What list shows you

Nothing here is cached, deliberately, and you should not cache it either: a listing belongs to the key that fetched it, and reusing one across keys will show an organization a catalog that is not its own.

Supplying your own transport

By default the library talks to the API over AsyncHTTPClient, configured to refuse redirects. Anything conforming to ClientTransport can take its place. To use URLSession on an Apple platform, add swift-openapi-urlsession to your own package and hand its transport in:

swift
import OpenAPIURLSession

let client = InternetDataClient(options: .init(apiKey: key, transport: URLSessionTransport()))

One thing to know if you do: the download endpoint answers 302, and the library follows that redirect itself as a second request rather than letting the transport do it. That is what keeps your API key off object storage, and it is what stops a whole database being read into memory before the library ever sees the link. Configure yours not to follow redirects. The library refuses such a response rather than reading it, but the transfer has already started by then.

Sign in with OAuth (device flow)

A program running on the person's own machine can let them sign in with a browser and pick one of their API keys, instead of asking them to paste it:

swift
let client = InternetDataClient()

let device = try await client.oauth.deviceAuthorization(
    clientID: "your-client-id", scope: "account.read apikeys.read apikeys.reveal",
)
print("Open \(device.verificationURI) and enter \(device.userCode)")

let token = try await client.oauth.pollDeviceToken(device, clientID: "your-client-id")
guard let apikey = token.apikey else {
    fatalError("no API key came back: none was picked, or it can't be shown again")
}
let keyed = InternetDataClient(apiKey: apikey)

A denied sign-in throws OauthError.accessDenied and a code that ran out OauthError.expiredToken. Client IDs are issued on request from support@internetdata.io, and client.oauth.revoke(refreshToken, clientID: "your-client-id") signs the machine out again.

Sign in with OAuth (authorization code)

An app that can take a browser redirect signs the person in there instead, with a PKCE pair made for that one sign-in:

swift
let client = InternetDataClient()
let redirectURI = "http://127.0.0.1:8765/callback"
let pkce = client.oauth.createPkce()

let url = try client.oauth.authorizationURL(
    clientID: "your-client-id", redirectURI: redirectURI, codeChallenge: pkce.challenge,
    scope: "apikeys.use", state: "your-state",
)
// Open url in the browser. Its redirect to redirectURI carries code and state.
let token = try await client.oauth.exchangeAuthorizationCode(
    code, clientID: "your-client-id", codeVerifier: pkce.verifier, redirectURI: redirectURI,
)

Check that state came back as you sent it before you exchange code, which works once. The client ID can also be the https URL of a client metadata document your app serves, and such an app is never handed a key, so token.apikey stays nil.

Register a client

An app with no client ID, and no client metadata document to use as one, registers itself, then signs in with the clientID it gets back as the client ID for authorizationURL and exchangeAuthorizationCode:

swift
let client = InternetDataClient()
let registration = try await client.oauth.register(
    redirectURIs: ["http://127.0.0.1:8765/callback"], clientName: "Your App")

Each redirect URI must be https on a public host name, or http on a loopback address, which then matches any port. Nothing is kept until a person approves the client, so an app with no client ID saved can simply register again.