- Updated: March 18, 2026
- 8 min read
Building a Native iOS Swift SDK for OpenClaw Rating API
Answer: Build a native iOS Swift SDK for the OpenClaw Rating API by setting up Xcode, adding a Swift Package, securely storing your API key in the Keychain, creating a reusable URLSession wrapper, defining typed endpoints for rating submission and retrieval, handling errors and rate limits, writing unit tests, and finally publishing the package to the Swift Package Registry.
Introduction
The OpenClaw Rating API lets developers collect, store, and query user‑generated ratings for any digital product. For iOS teams, a native Swift SDK removes boilerplate networking code, enforces best‑practice security, and accelerates time‑to‑market. This guide walks you through a complete, production‑ready SDK—from project scaffolding to publishing on the Swift Package Registry—so you can focus on building great user experiences instead of reinventing HTTP calls.
Whether you’re a solo developer or part of a larger mobile team, the steps below follow the MECE principle, ensuring each task is mutually exclusive and collectively exhaustive. By the end, you’ll have a clean, testable, and documented Swift package ready for integration into any iOS app.
Prerequisites
- Xcode version: 15.0 or later (Swift 5.9+ recommended).
- Swift version: 5.9 or newer to leverage modern concurrency features.
- OpenClaw account & API key: Sign up at the OpenClaw hosting portal and generate a secret key for authentication.
Project Setup
1. Create a New Xcode Project
Open Xcode → File → New → Project. Choose iOS App, name it OpenClawSDKDemo, and select Swift as the language. Ensure Use SwiftUI is unchecked if you prefer UIKit.
2. Add the SDK as a Swift Package
From the project navigator, select the project file, then go to Package Dependencies → Add Package Dependency. Use the following URL (replace with your repository URL after you publish):
https://github.com/your‑org/OpenClawSwiftSDK.gitSelect the Up to Next Major rule and finish the wizard. Xcode will fetch the package and make it available to your app target.
3. Optional: CocoaPods Alternative
If your team prefers CocoaPods, add this line to your Podfile:
pod 'OpenClawSwiftSDK', :git => 'https://github.com/your‑org/OpenClawSwiftSDK.git'Run pod install and open the generated workspace.
Authentication
OpenClaw uses a simple Bearer token scheme. Storing the token in plain text is a security risk, so we’ll use the iOS Keychain.
Keychain Helper
import Security
final class KeychainHelper {
static let shared = KeychainHelper()
private init() {}
func save(key: String, data: Data) -> Bool {
let query = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
] as [String : Any]
SecItemDelete(query as CFDictionary) // Remove existing item
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
func read(key: String) -> Data? {
let query = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
] as [String : Any]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
return status == errSecSuccess ? result as? Data : nil
}
}
Storing the API Key
Call this once—ideally after the user logs in or when you first receive the key:
let apiKey = "YOUR_OPENCLAW_API_KEY"
if let data = apiKey.data(using: .utf8) {
_ = KeychainHelper.shared.save(key: "OpenClawAPIKey", data: data)
}
Generating Authorization Header
func authHeader() -> [String: String]? {
guard let data = KeychainHelper.shared.read(key: "OpenClawAPIKey"),
let token = String(data: data, encoding: .utf8) else { return nil }
return ["Authorization": "Bearer \(token)"]
}
Request Handling
We’ll wrap URLSession in a generic NetworkClient that supports async/await, automatic JSON decoding, and built‑in retry logic.
NetworkClient.swift
import Foundation
enum NetworkError: Error {
case invalidURL
case noData
case decoding(Error)
case server(Int)
case unauthorized
case rateLimited
}
final class NetworkClient {
static let shared = NetworkClient()
private let session: URLSession
private let baseURL = URL(string: "https://api.openclaw.com/v1")!
private init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 60
self.session = URLSession(configuration: config)
}
func request(_ endpoint: Endpoint,
retries: Int = 2) async throws -> T {
guard var url = URL(string: endpoint.path, relativeTo: baseURL) else {
throw NetworkError.invalidURL
}
// Append query parameters for GET requests
if let query = endpoint.queryItems, endpoint.method == .get {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
components.queryItems = query
url = components.url!
}
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
request.allHTTPHeaderFields = endpoint.headers
// Add auth header if available
if let auth = authHeader() {
request.allHTTPHeaderFields?.merge(auth) { $1 }
}
// Encode body for POST/PUT
if let body = endpoint.body {
request.httpBody = try JSONEncoder().encode(body)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.noData
}
switch http.statusCode {
case 200...299:
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw NetworkError.decoding(error)
}
case 401:
throw NetworkError.unauthorized
case 429:
throw NetworkError.rateLimited
default:
throw NetworkError.server(http.statusCode)
}
} catch {
if retries > 0, (error as? NetworkError) == .rateLimited {
// Exponential back‑off
try await Task.sleep(nanoseconds: UInt64(1_000_000_000 * pow(2.0, Double(3 - retries))))
return try await request(endpoint, retries: retries - 1)
}
throw error
}
}
}
Endpoint Definition
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
struct Endpoint {
let path: String
let method: HTTPMethod
let queryItems: [URLQueryItem]?
let headers: [String: String]?
let body: Encodable?
}
Rating Submission Endpoint
struct RatingRequest: Encodable {
let productId: String
let userId: String
let score: Int // 1‑5
let comment: String?
}
struct RatingResponse: Decodable {
let ratingId: String
let createdAt: String
}
extension Endpoint {
static func submitRating(_ payload: RatingRequest) -> Endpoint {
return Endpoint(
path: "/ratings",
method: .post,
queryItems: nil,
headers: nil,
body: payload
)
}
}
Rating Retrieval Endpoint
struct RatingListResponse: Decodable {
let ratings: [RatingResponse]
let total: Int
}
extension Endpoint {
static func fetchRatings(productId: String, page: Int = 1) -> Endpoint {
let query = [
URLQueryItem(name: "productId", value: productId),
URLQueryItem(name: "page", value: "\(page)")
]
return Endpoint(
path: "/ratings",
method: .get,
queryItems: query,
headers: nil,
body: nil
)
}
}
Using the SDK in Your App
func submitExample() async {
let request = RatingRequest(productId: "com.myapp", userId: "user123", score: 5, comment: "Great!")
do {
let response: RatingResponse = try await NetworkClient.shared.request(.submitRating(request))
print("Rating saved with ID: \\(response.ratingId)")
} catch {
print("Failed to submit rating: \\(error)")
}
}
func fetchExample() async {
do {
let list: RatingListResponse = try await NetworkClient.shared.request(.fetchRatings(productId: "com.myapp"))
print("Fetched \\(list.total) ratings")
} catch {
print("Error fetching ratings: \\(error)")
}
}
Best Practices
Error Handling & User Feedback
- Map
NetworkErrorto user‑friendly messages (e.g., “You are offline”, “Too many requests – please wait”). - Log server‑side errors with a unique request ID for easier debugging.
Rate Limiting & Retry Logic
OpenClaw enforces a 100 requests per minute limit per API key. The NetworkClient already retries on 429 with exponential back‑off. For high‑traffic apps, consider a local token bucket algorithm to pre‑emptively throttle calls.
Unit Testing the SDK
Use XCTest with URLProtocol stubs to mock network responses.
class NetworkClientTests: XCTestCase {
override func setUp() {
super.setUp()
URLProtocol.registerClass(MockURLProtocol.self)
}
func testSubmitRatingSuccess() async throws {
let json = """
{"ratingId":"abc123","createdAt":"2024-03-18T12:00:00Z"}
"""
MockURLProtocol.requestHandler = { request in
XCTAssertEqual(request.httpMethod, "POST")
return (HTTPURLResponse(url: request.url!,
statusCode: 201,
httpVersion: nil,
headerFields: nil)!, json.data(using: .utf8)!)
}
let payload = RatingRequest(productId: "p1", userId: "u1", score: 4, comment: nil)
let result: RatingResponse = try await NetworkClient.shared.request(.submitRating(payload))
XCTAssertEqual(result.ratingId, "abc123")
}
}
Documentation & Code Comments
Generate a README.md with usage examples, and run swift doc to produce HTML docs. Include /// style comments for each public type so Xcode’s Quick Help displays them automatically.
Versioning Strategy
Follow Semantic Versioning (SemVer):
- MAJOR – breaking API changes.
- MINOR – new, backward‑compatible features.
- PATCH – bug fixes and documentation updates.
Publishing the SDK
Swift Package Registry
Apple’s Swift Package Registry lets you host private packages securely. Follow these steps:
- Create a Git repository (public or private) with a
Package.swiftmanifest. - Tag a release, e.g.,
git tag 1.0.0 && git push --tags. - Register the package at UBOS platform overview (our internal registry service).
- Provide the package URL to your team:
https://github.com/your‑org/OpenClawSwiftSDK.git.
Maintaining Compatibility
When you add new endpoints (e.g., bulk rating import), keep the old ones functional and mark them @available(*, deprecated). This prevents downstream apps from breaking unexpectedly.
Automated Release Pipeline
Integrate GitHub Actions to run tests, lint Swift code with swiftlint, and publish a new package version automatically when a release branch is merged.
Conclusion and Next Steps
You now have a fully functional, secure, and testable Swift SDK for the OpenClaw Rating API. By adhering to the best‑practice checklist—secure Keychain storage, reusable URLSession wrapper, robust error handling, and semantic versioning—you’ll deliver a reliable rating experience to your iOS users while keeping maintenance overhead low.
Next steps you might consider:
- Implement a UI component that visualizes star ratings using SwiftUI.
- Add offline caching with
CoreDataorRealmfor rating submissions when the network is unavailable. - Explore AI marketing agents to automatically prompt users for feedback after key in‑app events.
- Review the UBOS pricing plans if you need additional compute resources for large‑scale analytics.
For a deeper dive into building AI‑enhanced mobile experiences, check out our UBOS templates for quick start. They include pre‑built components for authentication, analytics, and even voice‑enabled assistants powered by ElevenLabs AI voice integration.
Happy coding, and may your ratings soar!
For background on OpenClaw’s recent feature rollout, see the original announcement here.