Claude API Swift Example

Call the Anthropic Claude API from Swift using URLSession. Complete working examples for iOS and macOS: minimal request, streaming, async/await, and SwiftUI chat view.

💥 50p impulse-buy: Power Prompts PDF (first 10 buyers) 30 battle-tested Claude Code prompts · 8-page PDF · paste into CLAUDE.md and never re-type a prompt again · 50p impulse-buy, no commitment

Swift has no official Anthropic SDK, so you call the Claude API directly via URLSession. This guide covers every pattern for iOS and macOS: minimal async/await request, SwiftUI streaming chat, and the backend-proxy pattern for production apps (never embed API keys in the app binary).

Minimal async/await request (iOS 15+ / macOS 12+)

import Foundation

func askClaude(prompt: String) async throws -> String {
    let url = URL(string: "https://api.anthropic.com/v1/messages")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue(ProcessInfo.processInfo.environment["ANTHROPIC_API_KEY"] ?? "", forHTTPHeaderField: "x-api-key")
    request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

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

    let (data, _) = try await URLSession.shared.data(for: request)
    let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
    let content = (json["content"] as! [[String: Any]]).first!
    return content["text"] as! String
}

// Usage (in an async context)
let reply = try await askClaude(prompt: "Explain Swift actors in one paragraph.")
print(reply)

System prompt + conversation history

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

func chat(messages: [Message], system: String = "") async throws -> String {
    let url = URL(string: "https://api.anthropic.com/v1/messages")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue(ProcessInfo.processInfo.environment["ANTHROPIC_API_KEY"] ?? "", forHTTPHeaderField: "x-api-key")
    request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    var body: [String: Any] = [
        "model": "claude-sonnet-4-6",
        "max_tokens": 1024,
        "messages": messages.map { ["role": $0.role, "content": $0.content] }
    ]
    if !system.isEmpty { body["system"] = system }
    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    let (data, _) = try await URLSession.shared.data(for: request)
    let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
    let content = (json["content"] as! [[String: Any]]).first!
    return content["text"] as! String
}

SwiftUI streaming chat with URLSessionDataDelegate

import SwiftUI

@MainActor
class ChatViewModel: NSObject, ObservableObject, URLSessionDataDelegate {
    @Published var messages: [Message] = []
    @Published var streamingText: String = ""
    @Published var isStreaming = false

    func send(_ userText: String) {
        messages.append(Message(role: "user", content: userText))
        streamingText = ""
        isStreaming = true

        let url = URL(string: "https://api.anthropic.com/v1/messages")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue(ProcessInfo.processInfo.environment["ANTHROPIC_API_KEY"] ?? "", forHTTPHeaderField: "x-api-key")
        request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let body: [String: Any] = [
            "model": "claude-sonnet-4-6",
            "max_tokens": 1024,
            "stream": true,
            "messages": messages.map { ["role": $0.role, "content": $0.content] }
        ]
        request.httpBody = try? JSONSerialization.data(withJSONObject: body)

        let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
        session.dataTask(with: request).resume()
    }

    // URLSessionDataDelegate — receives SSE bytes incrementally
    nonisolated func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        guard let text = String(data: data, encoding: .utf8) else { return }
        for line in text.components(separatedBy: "
") {
            guard line.hasPrefix("data: "),
                  let jsonData = line.dropFirst(6).data(using: .utf8),
                  let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
                  let type_ = json["type"] as? String, type_ == "content_block_delta",
                  let delta = json["delta"] as? [String: Any],
                  let textDelta = delta["text"] as? String else { continue }
            DispatchQueue.main.async { self.streamingText += textDelta }
        }
    }

    nonisolated func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        DispatchQueue.main.async {
            if !self.streamingText.isEmpty {
                self.messages.append(Message(role: "assistant", content: self.streamingText))
            }
            self.streamingText = ""
            self.isStreaming = false
        }
    }
}

struct ChatView: View {
    @StateObject var vm = ChatViewModel()
    @State var input = ""

    var body: some View {
        VStack {
            ScrollView {
                ForEach(vm.messages, id: .content) { msg in
                    Text("(msg.role): (msg.content)").padding(4)
                }
                if !vm.streamingText.isEmpty {
                    Text("assistant: (vm.streamingText)").padding(4)
                }
            }
            HStack {
                TextField("Message", text: $input)
                Button("Send") {
                    let t = input; input = ""
                    vm.send(t)
                }.disabled(vm.isStreaming || input.isEmpty)
            }.padding()
        }
    }
}

Production: backend-proxy pattern (keep API key server-side)

// WRONG — API key in iOS app can be extracted from IPA
let apiKey = "sk-ant-api03-..."  // ❌ never do this

// CORRECT — proxy through your backend
func askClaudeViaProxy(prompt: String, userJWT: String) async throws -> String {
    let url = URL(string: "https://api.yourapp.com/claude")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("Bearer (userJWT)", forHTTPHeaderField: "Authorization")
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let body = ["prompt": prompt]
    request.httpBody = try JSONSerialization.data(withJSONObject: body)

    let (data, _) = try await URLSession.shared.data(for: request)
    let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
    return json["reply"] as! String
}
// Your server validates the JWT and calls Anthropic with the server-side API key

Swift approach comparison

ApproachUse whenStreamingDependencies
URLSession async/awaitSimple requests, iOS 15+Via delegateNone (stdlib)
URLSessionDataDelegateSwiftUI streaming UINativeNone (stdlib)
Backend proxyProduction iOS appDepends on backendYour server
AlamofireComplex networking, retriesVia streamingAlamofire SPM

For cost estimation before integrating Claude into your iOS app, use the Claude API Cost Calculator. For the Python equivalent quickstart, see the Python quickstart guide.

Frequently asked questions

Is there an official Anthropic Swift SDK?
No. As of mid-2026, Anthropic does not publish an official Swift package. Use `URLSession` with async/await — it is idiomatic, requires no dependencies, and works on iOS 15+/macOS 12+. For streaming, use `URLSessionDataDelegate`.
How do I call Claude from an iOS app safely?
Never embed your API key in the app binary — it can be extracted from the IPA. Proxy all Claude requests through your backend (e.g., a Swift Vapor or Node.js server that holds the key). Pass a user JWT to your server, which validates it and calls the Anthropic API.
How do I handle streaming in Swift?
Implement `URLSessionDataDelegate` and parse `data:` lines from the SSE byte stream in `urlSession(_:dataTask:didReceive:)`. Each `data:` line is a JSON string with `type: content_block_delta` containing the text delta. Accumulate deltas and publish via `@Published` for SwiftUI.
What iOS/macOS version does URLSession async/await require?
The `async`/`await` form of `URLSession.data(for:)` requires iOS 15+ or macOS 12+. For earlier targets, use completion handlers with `URLSession.shared.dataTask(with:completionHandler:)`.
How do I add the required Anthropic headers in Swift?
Set three headers on your `URLRequest`: `x-api-key` (your API key), `anthropic-version` (e.g., `2023-06-01`), and `Content-Type: application/json`. The `anthropic-version` header is required — requests without it return HTTP 400.

Free tools

Cost Calculator → API Cookbook → Diff Summarizer → Skills Browser →

More examples

Claude API Python QuickstartClaude API Node.js / TypeScript QuickstartClaude API Streaming in PythonClaude API Streaming in Node.js / TypeScriptClaude API Tool Use in PythonClaude API Tool Use in Node.js / TypeScript