← Blog

Claude API Swift iOS Integration Example

2026-10-11 · 5 min read · SubToAPI Team

Integrating Claude into an iOS app means making HTTPS requests from Swift code, parsing JSON responses, and handling streaming output if you want a typing effect in your UI. There's no official Anthropic Swift SDK, so most developers use URLSession directly or a lightweight wrapper. This article walks through a working example: a URLSession-based client, request/response models with Codable, streaming via Server-Sent Events, and the one architectural decision that trips up most mobile teams — where your API key actually lives.

The short version: you build a ClaudeClient struct that wraps URLSession, encode your messages into a Codable request body, decode the JSON response, and if you want live token-by-token output, you parse the text/event-stream response line by line. The part people get wrong is calling the API directly from the app with a hardcoded key. The rest of this guide covers both the direct integration pattern and the proxy pattern that avoids shipping secrets in your binary.

Basic Setup

Add your API endpoint and key to a config struct. Never hardcode secrets in source committed to a public repo, and never ship a raw Anthropic or SubToAPI key inside an App Store binary — it can be extracted from the IPA. For prototyping on your own device during development, it's fine; for production, see the proxy section below.

struct ClaudeConfig {
    static let baseURL = URL(string: "https://api.subtoapi.app/v1/messages")!
    static let apiKey = "sub_live_xxxxxxxxxxxxxxxx" // dev only, see note below
}

Request and Response Models

Define Codable structs matching the Messages API shape:

struct ClaudeMessage: Codable {
    let role: String
    let content: String
}

struct ClaudeRequest: Codable {
    let model: String
    let max_tokens: Int
    let messages: [ClaudeMessage]
}

struct ClaudeResponseContent: Codable {
    let type: String
    let text: String?
}

struct ClaudeResponse: Codable {
    let id: String
    let content: [ClaudeResponseContent]
    let usage: Usage?
}

struct Usage: Codable {
    let input_tokens: Int
    let output_tokens: Int
}

A Minimal Swift Client

final class ClaudeClient {
    func send(prompt: String) async throws -> String {
        var request = URLRequest(url: ClaudeConfig.baseURL)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue("Bearer \(ClaudeConfig.apiKey)", forHTTPHeaderField: "Authorization")

        let body = ClaudeRequest(
            model: "claude-sonnet-4",
            max_tokens: 1024,
            messages: [ClaudeMessage(role: "user", content: prompt)]
        )
        request.httpBody = try JSONEncoder().encode(body)

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

        guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        let decoded = try JSONDecoder().decode(ClaudeResponse.self, from: data)
        return decoded.content.first(where: { $0.type == "text" })?.text ?? ""
    }
}

Call it from a view model with Swift's structured concurrency:

func askClaude() {
    Task {
        do {
            let answer = try await ClaudeClient().send(prompt: "Summarize this in one sentence.")
            await MainActor.run { self.responseText = answer }
        } catch {
            await MainActor.run { self.errorMessage = error.localizedDescription }
        }
    }
}

This pattern works identically whether you're calling Anthropic's API directly or an endpoint shaped like it, since SubToAPI's /v1/messages endpoint mirrors the same request and response format — full field reference is in the docs/messages page.

Streaming for a Typing Effect

For chat UIs, users expect text to appear incrementally rather than waiting for the full response. SSE streaming requires reading the response body as it arrives rather than waiting for data(for:) to complete. URLSession.bytes(for:) gives you an AsyncSequence of lines you can parse:

func streamResponse(prompt: String) async throws -> AsyncStream<String> {
    var request = URLRequest(url: ClaudeConfig.baseURL)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("Bearer \(ClaudeConfig.apiKey)", forHTTPHeaderField: "Authorization")

    let body = ["model": "claude-sonnet-4", "max_tokens": 1024, "stream": true,
                "messages": [["role": "user", "content": prompt]]] as [String: Any]
    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    let (bytes, _) = try await URLSession.shared.bytes(for: request)

    return AsyncStream { continuation in
        Task {
            for try await line in bytes.lines {
                guard line.hasPrefix("data: ") else { continue }
                let payload = line.dropFirst(6)
                if payload == "[DONE]" { continuation.finish(); break }
                if let data = payload.data(using: .utf8),
                   let event = try? JSONDecoder().decode(StreamEvent.self, from: data),
                   let text = event.delta?.text {
                    continuation.yield(text)
                }
            }
            continuation.finish()
        }
    }
}

Bind the stream to a @Published string in your view model and append each chunk as it arrives — SwiftUI's Text view will re-render automatically. The exact event shapes for content_block_delta and friends are documented at docs/streaming; stream: true is the only extra parameter needed on top of the base request.

Don't Ship Your API Key in the App

This is the part most iOS tutorials skip. If you embed a live API key in your app bundle, anyone with otool or a jailbroken device can extract it and run up your bill. The standard fix is a thin backend proxy: your iOS app calls your own server, your server holds the key and calls Claude (or a Claude-compatible endpoint), and the response streams back to the device. This also gives you a place to enforce per-user rate limits and log usage.

If you don't want to build and maintain that proxy yourself, SubToAPI gives you a hosted API key (sub_live_...) scoped to one app, with per-key usage metadata so you can see exactly what your iOS client is consuming without building your own logging layer. You generate the key once from the dashboard after signup, put it behind your own thin auth-checking endpoint (even a single serverless function), and your Swift client talks to that instead of holding the real key. Review the request/response contract in docs/quickstart before wiring it into your app.

Questions

Does Apple restrict calling third-party AI APIs from iOS apps? No, App Store Review Guidelines don't prohibit calling LLM APIs over HTTPS. Standard App Transport Security rules apply — use HTTPS, which both Anthropic's API and SubToAPI's endpoints require by default.

Can I use Combine instead of async/await for this? Yes. Wrap URLSession.dataTaskPublisher(for:) in a Publisher chain with .decode(type:decoder:); the request/response models above work unchanged, only the concurrency mechanism differs.

Is there an official Anthropic Swift package? Not as of this writing. Community wrappers exist, but a direct URLSession client like the one above is lightweight enough that most teams skip adding a dependency for it.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →