Get Started
API Client & Security
Every network call in ShipThatApp — chat, image generation, vision, the Dex scanner — goes through one place: ApiClient. It exists so two things are always true: your OpenAI key never ships inside the app, and only your app can use your backend. If you build a new networked feature, route it through here and you inherit both guarantees for free.
The secure-proxy model
The app never talks to OpenAI directly. Instead it calls your backend proxy, which holds the OpenAI key server-side and forwards requests. The key lives where users can't extract it, and you can add rate limits, logging, and abuse protection in one place.
ShipThatApp ──HMAC-signed request──▶ Your backend proxy ──▶ OpenAI
(no API key on device) (holds the key)
Why this matters
The most common way indie AI apps leak money is shipping the OpenAI key in the binary, where anyone can pull it out and run up your bill. ShipThatApp never embeds it — the key stays on your server, and the app authenticates with a short-lived token instead.
One client, typed responses
ApiClient.shared is the single entry point. Its generic sendRequest decodes straight into your model and returns a Result, so call sites stay tiny:
let result = await ApiClient.shared.sendRequest(
endpoint: Endpoints.vision,
body: try JSONEncoder().encode(requestModel),
responseModel: VisionResponse.self
)
switch result {
case .success(let response): return response
case .failure(let error): throw error
}
Errors arrive as a typed RequestError (invalidURL, noResponse, decode, unAuthorized(code:), serverError(code:), unknown), and decoding failures are logged with the exact missing key or type mismatch to make debugging painless.
HMAC request signing
Before any request leaves the device, ApiClient signs it. The signature covers the HTTP method, path, a timestamp, and a SHA-256 hash of the body — so a captured request can't be replayed or tampered with:
let timestamp = String(Int(Date().timeIntervalSince1970))
let bodyHash = CryptoUtils.shared.sha256Hex(body ?? Data())
let canonical = "\(endpoint.method.rawValue)\n\(signedPath)\n\(timestamp)\n\(bodyHash)"
let signature = CryptoUtils.shared.createHmac(key: signingKey, phrase: canonical)
request.setValue(signature, forHTTPHeaderField: "x-signature")
request.setValue(timestamp, forHTTPHeaderField: "x-timestamp")
The backend recomputes the identical canonical string and verifies the signature before doing any work. Because the timestamp and body hash are part of what's signed, replay and tampering both fail.
Per-session tokens, not embedded secrets
Feature endpoints don't sign with an app-embedded secret. The app first calls /auth_token (bootstrapped with Config.Api.authKey), receives a short-lived session token, and stores it in the Keychain. Every subsequent call signs with that token:
var signingKey: String? {
switch self {
case .auth:
return Config.Api.authKey // bootstrap only
case .chatgpt, .chatgptSimple, .dalle, .vision /* ... */:
return KeychainSwift().get(Config.Keychain.tokenKey) // per-session token
}
}
If a signed call returns 401, ApiClient waits briefly and retries once, giving the session a chance to refresh before the call ultimately fails.
Where the secrets live
Config.Api.authKey (and your Supabase/RevenueCat/TelemetryDeck keys) are read at runtime from a gitignored Config.xcconfig — never hardcoded, never committed. API_AUTH_KEY on the device must match the backend's AUTH_SECRET_KEY. See Configurations.
Adding your own endpoint
Adding a networked feature is a two-step change, and signing/keys come along automatically:
- Add a
caseto theEndpointsenum with itspathandmethod, and include it in thesigningKeyswitch (use the per-session Keychain token for anything that hits your proxy). - Call
ApiClient.shared.sendRequest(endpoint:body:responseModel:)with aCodablerequest and your expected response type.
That's it — the new call is HMAC-signed, token-authenticated, retried on 401, and decoded into your model, exactly like the built-in AI features.
Where it's used
Every AI feature in the kit runs on this client: AI Chat (cloud path), AI Image Generation, AI Vision, and the cloud pass of the Dex Scanner. On-device chat via Apple Foundation Models skips the network entirely — see AI Chat.