The official Swift client library for the InternetData database API.
It downloads the IP and ASN databases your organization is licensed for, verifies them against the published checksums, and tells you what is inside one before you fetch it.
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/internetdata/sdk-swift.git", from: "1.0.0"),
]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:
.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.
Every database published today needs an API key carrying the db.download scope. Access is granted by contract, one family at a time, so there is no self-serve signup: talk to us and we will issue one. apiKey is nevertheless optional - a client built without one sends no Authorization header at all, ready for a database served without a licence.
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"]
}Every call lives under client.database. The downloads are the whole of this API today, but the sibling VPNDetection client spells the same seven calls the same way, so a codebase holding both does not have to remember which one is flat.
A licence 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:
let client = InternetDataClient(options: .init(apiKey: key, retries: 4))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:
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.
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:
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:
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:
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:
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.
checksums returns the whole published digest set for one file rather than a single algorithm:
let checksums = try await client.database.checksums(id: "vpn_ip_v1", format: .mmdb)
print(checksums.sha256)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:
for attempt in try await client.database.downloads(limit: 20) {
print(attempt.created, attempt.datasetId, attempt.outcome, attempt.bytes ?? 0)
}Failures throw an InternetDataError carrying a kind, the API's own rc as its message, and an isRetryable flag:
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.
Note that rateLimited and quotaExceeded both arrive as HTTP 429 and are not the same thing. A rate limit is the API protecting itself against a burst, so retrying later works; a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, waiting as long as the API asked, but never retries a spent quota.
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.
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:
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.
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/internetdata for more.
IP, ASN and Domain data to reveal unique insights about the internet. APIs, Databases and Live Feeds available.
This project is licensed under the MIT License.