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.
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).
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)
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
}
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()
}
}
}
// 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
| Approach | Use when | Streaming | Dependencies |
|---|---|---|---|
| URLSession async/await | Simple requests, iOS 15+ | Via delegate | None (stdlib) |
| URLSessionDataDelegate | SwiftUI streaming UI | Native | None (stdlib) |
| Backend proxy | Production iOS app | Depends on backend | Your server |
| Alamofire | Complex networking, retries | Via streaming | Alamofire 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.