Swift Guide

Detect Website Tech Stacks in Swift with StackPeek API

Published March 29, 2026 · 12 min read

Swift is the language behind every iOS app, a growing force in server-side development with Vapor, and a first-class choice for command-line tools on macOS and Linux. If you are building a competitive intelligence app, a sales enrichment tool, or a security scanner in Swift, you need a reliable way to detect which technologies a website is running.

The StackPeek API gives you exactly that: send a URL, get back structured JSON listing every detected framework, CMS, analytics tool, CDN, and hosting provider. No WebKit rendering, no headless browser dependencies, no third-party SDKs to install. Just a single HTTP GET request using Foundation's URLSession.

This guide covers real, production-ready Swift code for every common pattern: basic URLSession calls, Codable struct definitions for type-safe parsing, modern async/await with Swift 5.5+, concurrent scanning with TaskGroup, a SwiftUI view for displaying results, and server-side Vapor middleware. Every example compiles and runs in Xcode or with swift run on the command line.

Codable Struct Definitions

Before writing any networking code, define Swift structs that conform to Codable so JSONDecoder can automatically map the API response into type-safe values. This is the foundation every other example in this guide builds on.

import Foundation

/// A single detected technology from the StackPeek API.
struct TechDetection: Codable, Identifiable {
    let name: String
    let category: String
    let confidence: Double
    let version: String?
    let website: String?

    var id: String { name }
}

/// The top-level response from the StackPeek detect endpoint.
struct DetectResponse: Codable {
    let url: String
    let technologies: [TechDetection]
    let scanTime: Int?
}

/// Wraps a detection result with domain context
/// for batch scanning operations.
struct ScanResult: Identifiable {
    let id = UUID()
    let domain: String
    let response: DetectResponse?
    let error: String?
}

Swift's Codable protocol eliminates all manual JSON parsing. Property names match the API's JSON keys exactly, so no CodingKeys enum is needed. Optional properties like version and website are declared with ? — if the key is absent from the response, the value is nil rather than causing a decoding error. The Identifiable conformance on TechDetection makes it ready for SwiftUI lists and ForEach loops.

Basic URLSession Call

Foundation's URLSession is the standard networking layer on every Apple platform. Here is the simplest possible call to the StackPeek API using a completion handler, compatible with Swift 5.0+ and every version of iOS and macOS:

import Foundation

let baseURL = "https://us-central1-todd-agent-prod.cloudfunctions.net"
    + "/stackpeekApi/api/v1/detect"

func detectTechStack(
    targetURL: String,
    completion: @escaping (Result<DetectResponse, Error>) -> Void
) {
    guard let encoded = targetURL.addingPercentEncoding(
        withAllowedCharacters: .urlQueryAllowed
    ) else {
        completion(.failure(URLError(.badURL)))
        return
    }

    guard let url = URL(string: "\(baseURL)?url=\(encoded)") else {
        completion(.failure(URLError(.badURL)))
        return
    }

    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }

        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            let code = (response as? HTTPURLResponse)?.statusCode ?? -1
            completion(.failure(URLError(.badServerResponse,
                userInfo: ["statusCode": code])))
            return
        }

        guard let data = data else {
            completion(.failure(URLError(.zeroByteResource)))
            return
        }

        do {
            let result = try JSONDecoder().decode(
                DetectResponse.self, from: data
            )
            completion(.success(result))
        } catch {
            completion(.failure(error))
        }
    }
    task.resume()
}

// Usage
detectTechStack(targetURL: "https://linear.app") { result in
    switch result {
    case .success(let response):
        print("Technologies detected on \(response.url):")
        for tech in response.technologies {
            let ver = tech.version.map { " v\($0)" } ?? ""
            print("  \(tech.name)\(ver) — \(tech.category) "
                + "(\(Int(tech.confidence))%)")
        }
    case .failure(let error):
        print("Error: \(error.localizedDescription)")
    }
}

Key details: addingPercentEncoding(withAllowedCharacters:) encodes the target URL so special characters do not break the query string. The completion handler uses Swift's Result type to provide either a decoded response or a typed error. JSONDecoder handles all the parsing — you never manually extract values from a dictionary. The task.resume() call is easy to forget and will silently fail if omitted.

Async/Await Version (Swift 5.5+)

Swift 5.5 introduced async/await, which eliminates callback pyramids and makes asynchronous code read like synchronous code. If you are targeting iOS 15+, macOS 12+, or building server-side Swift, this is the preferred approach:

import Foundation

/// A lightweight client for the StackPeek tech detection API.
actor StackPeekClient {
    private let baseURL = "https://us-central1-todd-agent-prod.cloudfunctions.net"
        + "/stackpeekApi/api/v1/detect"
    private let session: URLSession
    private let decoder = JSONDecoder()

    init(timeoutInterval: TimeInterval = 30) {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = timeoutInterval
        config.timeoutIntervalForResource = 60
        config.waitsForConnectivity = true
        self.session = URLSession(configuration: config)
    }

    /// Detect the tech stack of a given URL.
    func detect(url targetURL: String) async throws -> DetectResponse {
        guard let encoded = targetURL.addingPercentEncoding(
            withAllowedCharacters: .urlQueryAllowed
        ),
        let url = URL(string: "\(baseURL)?url=\(encoded)") else {
            throw URLError(.badURL)
        }

        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Accept")
        request.setValue("stackpeek-swift/1.0", forHTTPHeaderField: "User-Agent")

        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw URLError(.badServerResponse)
        }

        guard httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse,
                userInfo: ["statusCode": httpResponse.statusCode])
        }

        return try decoder.decode(DetectResponse.self, from: data)
    }
}

// Usage
let client = StackPeekClient()

do {
    let result = try await client.detect(url: "https://stripe.com")
    print("Detected \(result.technologies.count) technologies on \(result.url)")
    for tech in result.technologies {
        let ver = tech.version ?? "-"
        print("  \(tech.name.padding(toLength: 20, withPad: " ", startingAt: 0))"
            + "\(tech.category.padding(toLength: 12, withPad: " ", startingAt: 0))"
            + "\(ver.padding(toLength: 10, withPad: " ", startingAt: 0))"
            + "\(Int(tech.confidence))%")
    }
} catch {
    print("Detection failed: \(error)")
}

The actor keyword makes StackPeekClient thread-safe by default — Swift's concurrency runtime ensures that only one task accesses the actor's mutable state at a time. The async throws function signature means callers use try await, and errors propagate naturally through Swift's standard error handling. No callbacks, no DispatchQueue hopping, no retain cycles to worry about.

The waitsForConnectivity configuration tells URLSession to wait for a network connection rather than failing immediately when the device is offline — useful for mobile apps where connectivity is intermittent.

Concurrent Scanning with TaskGroup

Swift's structured concurrency makes concurrent scanning clean and safe. TaskGroup manages child tasks automatically — if one fails or the parent task is cancelled, all child tasks are cancelled too. No manual thread management required.

import Foundation

/// Scan multiple domains concurrently using TaskGroup.
/// Returns results in the order they complete (not input order).
func scanAll(
    domains: [String],
    maxConcurrency: Int = 5
) async -> [ScanResult] {
    let client = StackPeekClient()

    return await withTaskGroup(of: ScanResult.self,
                               returning: [ScanResult].self) { group in
        var results: [ScanResult] = []
        var index = 0

        // Seed the group with initial tasks up to maxConcurrency
        for _ in 0..<min(maxConcurrency, domains.count) {
            let domain = domains[index]
            group.addTask {
                do {
                    let response = try await client.detect(
                        url: "https://\(domain)"
                    )
                    return ScanResult(domain: domain,
                                     response: response, error: nil)
                } catch {
                    return ScanResult(domain: domain,
                                     response: nil,
                                     error: error.localizedDescription)
                }
            }
            index += 1
        }

        // As each task completes, add the next domain
        for await result in group {
            results.append(result)
            if index < domains.count {
                let domain = domains[index]
                group.addTask {
                    do {
                        let response = try await client.detect(
                            url: "https://\(domain)"
                        )
                        return ScanResult(domain: domain,
                                         response: response, error: nil)
                    } catch {
                        return ScanResult(domain: domain,
                                         response: nil,
                                         error: error.localizedDescription)
                    }
                }
                index += 1
            }
        }

        return results
    }
}

// Usage
let domains = [
    "stripe.com", "linear.app", "vercel.com", "notion.so",
    "figma.com", "github.com", "shopify.com", "slack.com",
    "gitlab.com", "netlify.com"
]

let start = Date()
let results = await scanAll(domains: domains, maxConcurrency: 5)
let elapsed = Date().timeIntervalSince(start)

print("Scanned \(results.count) domains in \(String(format: "%.1f", elapsed))s\n")

for r in results {
    if let error = r.error {
        print("  \(r.domain.padding(toLength: 18, withPad: " ", startingAt: 0))"
            + "ERROR: \(error)")
    } else if let resp = r.response {
        let techNames = resp.technologies.prefix(5).map(\.name)
        let extra = resp.technologies.count > 5
            ? " (+\(resp.technologies.count - 5) more)" : ""
        print("  \(r.domain.padding(toLength: 18, withPad: " ", startingAt: 0))"
            + "\(techNames.joined(separator: ", "))\(extra)")
    }
}

This implementation uses a sliding window pattern: it seeds the TaskGroup with up to maxConcurrency tasks, then adds a new task each time one completes. This keeps exactly maxConcurrency tasks running at all times without exceeding the limit. The pattern avoids overwhelming the API while maximizing throughput — scanning 10 domains with a concurrency of 5 takes roughly the time of 2 sequential scans.

Because TaskGroup is structured, cancellation propagates automatically. If you wrap this in a SwiftUI .task modifier and the user navigates away, every in-flight scan is cancelled cleanly.

SwiftUI View for Detected Technologies

If you are building an iOS or macOS app, here is a complete SwiftUI view that lets users enter a URL, scan it, and see detected technologies in a polished list:

import SwiftUI

struct TechStackView: View {
    @State private var targetURL = "https://stripe.com"
    @State private var technologies: [TechDetection] = []
    @State private var isLoading = false
    @State private var errorMessage: String?

    private let client = StackPeekClient()

    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // URL input bar
                HStack {
                    TextField("Enter website URL", text: $targetURL)
                        .textFieldStyle(.roundedBorder)
                        .autocorrectionDisabled()
                        .textInputAutocapitalization(.never)

                    Button {
                        Task { await scan() }
                    } label: {
                        if isLoading {
                            ProgressView()
                                .controlSize(.small)
                        } else {
                            Image(systemName: "magnifyingglass")
                        }
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(isLoading || targetURL.isEmpty)
                }
                .padding()

                if let error = errorMessage {
                    Text(error)
                        .foregroundStyle(.red)
                        .font(.caption)
                        .padding(.horizontal)
                }

                // Results list
                List(technologies) { tech in
                    HStack {
                        VStack(alignment: .leading, spacing: 4) {
                            HStack {
                                Text(tech.name)
                                    .font(.headline)
                                if let version = tech.version {
                                    Text("v\(version)")
                                        .font(.caption)
                                        .foregroundStyle(.secondary)
                                }
                            }
                            Text(tech.category)
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                        Spacer()
                        Text("\(Int(tech.confidence))%")
                            .font(.system(.body, design: .monospaced))
                            .foregroundStyle(
                                tech.confidence >= 90 ? .green :
                                tech.confidence >= 70 ? .orange : .red
                            )
                    }
                }
                .listStyle(.insetGrouped)
            }
            .navigationTitle("StackPeek")
        }
    }

    private func scan() async {
        isLoading = true
        errorMessage = nil
        technologies = []

        do {
            let result = try await client.detect(url: targetURL)
            technologies = result.technologies.sorted {
                $0.confidence > $1.confidence
            }
        } catch {
            errorMessage = error.localizedDescription
        }

        isLoading = false
    }
}

The view uses the StackPeekClient actor defined earlier, so all networking is thread-safe. The Task block inside the button action bridges from synchronous SwiftUI event handling into async code. Results are sorted by confidence score so the most certain detections appear first. The confidence percentage is color-coded: green for 90%+, orange for 70-89%, and red below 70%.

This view works on iOS, iPadOS, and macOS with no platform-specific code. Add it to your app by placing TechStackView() in your navigation hierarchy or using it as a sheet. The NavigationStack wrapper provides a title bar and supports push navigation if you want to add detail views for each technology.

Vapor Server-Side Middleware

Swift runs on the server too. If you are building a web application with Vapor, you can add middleware that automatically detects the tech stack of referring websites. Every time a visitor arrives from another site, you learn what that site is built with — useful for analytics, competitive intelligence, and lead qualification.

import Vapor
import Foundation

/// Vapor middleware that detects the tech stack of referring websites.
/// Results are cached in-memory with a configurable TTL.
final class TechStackMiddleware: AsyncMiddleware {
    private let apiBase = "https://us-central1-todd-agent-prod.cloudfunctions.net"
        + "/stackpeekApi/api/v1/detect"
    private let cache = TechStackCache()

    func respond(
        to request: Request,
        chainingTo next: AsyncResponder
    ) async throws -> Response {
        // Extract and validate the Referer header
        guard let referer = request.headers.first(name: .referer),
              let refererURL = URL(string: referer),
              let host = refererURL.host else {
            return try await next.respond(to: request)
        }

        let domain = host.hasPrefix("www.")
            ? String(host.dropFirst(4)) : host

        // Check cache first
        if let cached = await cache.get(domain) {
            request.storage.set(TechStackKey.self, to: cached)
            return try await next.respond(to: request)
        }

        // Fire async detection in background — do not block the request
        let app = request.application
        Task {
            do {
                guard let encoded = "https://\(domain)"
                    .addingPercentEncoding(
                        withAllowedCharacters: .urlQueryAllowed
                    ),
                    let url = URL(string: "\(apiBase)?url=\(encoded)") else {
                    return
                }

                let (data, _) = try await URLSession.shared.data(from: url)
                let result = try JSONDecoder().decode(
                    DetectResponse.self, from: data
                )
                await cache.set(domain, response: result)
                app.logger.info("Detected \(result.technologies.count) "
                    + "techs on referrer \(domain)")
            } catch {
                app.logger.error("StackPeek error for \(domain): \(error)")
            }
        }

        return try await next.respond(to: request)
    }
}

/// Thread-safe in-memory cache using an actor.
actor TechStackCache {
    private var entries: [String: CacheEntry] = [:]
    private let ttl: TimeInterval = 86400 // 24 hours

    struct CacheEntry {
        let response: DetectResponse
        let expiresAt: Date
    }

    func get(_ domain: String) -> DetectResponse? {
        guard let entry = entries[domain],
              Date() < entry.expiresAt else {
            return nil
        }
        return entry.response
    }

    func set(_ domain: String, response: DetectResponse) {
        entries[domain] = CacheEntry(
            response: response,
            expiresAt: Date().addingTimeInterval(ttl)
        )
    }
}

/// Storage key for attaching tech stack data to Vapor requests.
struct TechStackKey: StorageKey {
    typealias Value = DetectResponse
}

// Register the middleware in configure.swift:
//
// app.middleware.use(TechStackMiddleware())
//
// Access in any route handler:
//
// func index(req: Request) async throws -> String {
//     if let stack = req.storage.get(TechStackKey.self) {
//         return "Referrer uses: \(stack.technologies.map(\.name))"
//     }
//     return "Welcome"
// }

The middleware fires the API call in a detached Task so it never adds latency to the incoming request. The actor-based cache is inherently thread-safe — no locks, no mutexes, no data races. Results are cached for 24 hours. On the first request from a new referrer, the cache miss means no tech stack data is available for that specific request, but every subsequent request from the same referring domain gets the cached result instantly.

Vapor's StorageKey pattern lets you attach the detection result directly to the request object, so any downstream route handler can access it with req.storage.get(TechStackKey.self) without coupling to the middleware implementation.

Pricing: StackPeek vs Wappalyzer

If you have looked at Wappalyzer's API pricing for a Swift project, here is how the two compare:

Feature StackPeek Wappalyzer
Free tier 100 scans/day 50 scans/month
Paid plan (entry) $9/month $250/month
Scans on entry plan 5,000/month 50,000/month
Pro plan $29/month (25,000 scans) $450/month (100,000 scans)
Cost per scan (entry) $0.0018 $0.005
API key required No (free tier) Yes
Technologies detected 120+ 1,400+
Response format JSON (Codable-ready) JSON

StackPeek is 28x cheaper than Wappalyzer at the entry tier. For most Swift projects — iOS apps with tech lookup features, server-side lead enrichment, macOS developer tools — you need to detect the major frameworks, CMS platforms, and analytics tools. StackPeek covers these at a fraction of the cost. If you need to identify niche JavaScript libraries or obscure WordPress plugins, Wappalyzer's broader fingerprint database may justify the premium. But for 90% of use cases, StackPeek at $9/month vs Wappalyzer at $250/month is the obvious choice.

Use Cases for Swift Developers

Here are the most common scenarios where Swift developers use tech stack detection:

Start detecting tech stacks from Swift today

100 free scans/day. No API key required. JSON response works natively with Codable.

Try StackPeek Free →

Getting Started in 60 Seconds

The fastest way to test StackPeek from Swift requires zero dependencies — just Foundation:

  1. Open Xcode and create a new Command Line Tool project (or Swift Playground)
  2. Copy the Codable struct definitions and the async/await client into your main.swift
  3. Add a @main entry point with a simple scan call
  4. Run it with Cmd+R and see detected technologies in the console

For a Swift Package Manager project, the same code works with swift run on macOS or Linux — no Xcode required. Create a Package.swift, drop the code into Sources/, and run from the terminal.

From there, add TaskGroup for concurrent scanning, wrap the client in a SwiftUI view for a visual interface, or integrate it as Vapor middleware for server-side detection. The API is the same regardless of how you call it — a single GET request with a URL query parameter that returns Codable-compatible JSON.

For production deployments, add caching with an actor-based in-memory store or UserDefaults for persistence across app launches. Add retry logic with exponential backoff for transient network failures. Use TaskGroup with a sliding window for concurrent scanning without overwhelming the API. Every pattern in this guide is copy-paste ready and compiles with Swift 5.5+.