Connecting your iOS app to external services is rarely as simple as copy-pasting a URL into URLSession. Between OAuth flows, token refresh, retry logic, decoding edge cases, and writing tests that don’t hit the real network, third party API integration on iOS can quickly turn into a tangled mess if you don’t set things up properly from day one.
This guide is a practical walkthrough for Swift developers in 2026. No fluff, no marketing speak. Just the patterns we use at Iris when shipping iOS apps that talk to Stripe, Firebase, OpenAI, Google, weather services, CRMs, and dozens of other APIs.
What Counts as a Third Party API Integration on iOS?
A third party API is any service you don’t own that your app communicates with over HTTP (or sometimes WebSocket / gRPC). Common categories include:
- Authentication and identity: Sign in with Apple, Google, Auth0, Firebase Auth
- Payments: Stripe, Adyen, RevenueCat
- Analytics and crash reporting: Mixpanel, Amplitude, Sentry
- AI and ML: OpenAI, Anthropic, Google Gemini, Replicate
- Maps and location: Mapbox, Google Maps, HERE
- Communication: Twilio, SendGrid, Stream Chat
- Data services: weather, finance, sports, social feeds
Each one introduces the same underlying challenges: authenticating securely, modeling requests and responses, handling failures, and keeping the integration testable.

Step 1: Design a Clean Networking Layer
The biggest mistake we still see in iOS codebases is scattered URLSession.shared.dataTask calls inside view controllers or SwiftUI views. It works until the day you need to add a header globally, mock a response, or swap environments.
The four building blocks
- Endpoint: a value type describing path, method, headers, and body
- HTTPClient: a protocol that executes endpoints and returns
Data+ response - APIClient: decodes responses into Swift models using
Codable - Service: a feature-specific facade (UserService, PaymentService, etc.)
Here is a minimal version using modern Swift concurrency:
struct Endpoint<Response: Decodable> {
let path: String
let method: String
var query: [URLQueryItem] = []
var headers: [String: String] = [:]
var body: Data? = nil
}
protocol HTTPClient {
func send<R>(_ endpoint: Endpoint<R>) async throws -> R
}
final class URLSessionHTTPClient: HTTPClient {
private let session: URLSession
private let baseURL: URL
private let decoder: JSONDecoder
init(baseURL: URL, session: URLSession = .shared, decoder: JSONDecoder = .init()) {
self.baseURL = baseURL
self.session = session
self.decoder = decoder
}
func send<R>(_ endpoint: Endpoint<R>) async throws -> R {
var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path),
resolvingAgainstBaseURL: false)!
components.queryItems = endpoint.query.isEmpty ? nil : endpoint.query
var request = URLRequest(url: components.url!)
request.httpMethod = endpoint.method
request.httpBody = endpoint.body
endpoint.headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
let (data, response) = try await session.data(for: request)
try APIErrorMapper.validate(response: response, data: data)
return try decoder.decode(R.self, from: data)
}
}
The win here: every view model depends on the HTTPClient protocol, not on URLSession. Testing becomes trivial.
Step 2: Handle Authentication Properly
Authentication is where most third party API integrations break. There are three patterns you will encounter on iOS.
API key (server-to-server style)
Never ship a secret API key inside your binary. It can be extracted in minutes. If the third party only offers a secret key, proxy the calls through your own backend. Use the iOS app to talk to your backend, and your backend to talk to the third party.
OAuth 2.0 / OIDC
For services like Google, GitHub, Spotify, or Notion, use ASWebAuthenticationSession. It opens a secure system browser sheet, supports PKCE, and returns the authorization code via a custom URL scheme.
let session = ASWebAuthenticationSession(
url: authURL,
callbackURLScheme: "myapp"
) { callbackURL, error in
// Exchange code for token
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = true
session.start()
Bearer tokens with refresh
Most modern APIs use short-lived access tokens. Build a small TokenStore backed by the Keychain, and an AuthenticatedHTTPClient that decorates your base client to inject the token and refresh it on 401.
| Storage option | Use for | Avoid for |
|---|---|---|
| Keychain | Access tokens, refresh tokens, API secrets returned at runtime | Large blobs or cached data |
| UserDefaults | Feature flags, user preferences | Anything sensitive |
| Info.plist / xcconfig | Public client IDs, base URLs | Secret keys |
Step 3: Build a Real Error Handling Strategy
A typed error enum beats Error every single time. It forces you to think about what can actually go wrong.
enum APIError: Error {
case invalidResponse
case unauthorized
case forbidden
case notFound
case rateLimited(retryAfter: TimeInterval?)
case server(status: Int, message: String?)
case decoding(DecodingError)
case transport(URLError)
}
Map status codes once, in a single place:
- 401: trigger token refresh, retry once, then sign the user out
- 403: surface a clear permission message, do not retry
- 404: often a real product case (empty state), not a crash
- 429: respect
Retry-Afterheader with exponential backoff - 5xx: retry with jitter, max 2 or 3 attempts
For user-facing messages, translate APIError into a presentation type at the view model layer. Your UI should never see raw HTTP codes.

Step 4: Decoding That Survives Real-World APIs
Third party APIs change. They add fields, rename them, return null where you expected a string, or send dates in three different formats. A few habits that save pain:
- Make every non-essential field Optional
- Use
@propertyWrapperor custominit(from:)for tolerant decoding - Configure
JSONDecoderwith the correctkeyDecodingStrategyanddateDecodingStrategyonce - Keep API DTOs separate from your domain models, then map between them
Separating DTOs from domain models is the single best protection against breaking changes. The third party can rename a field, you update one mapper, and the rest of the app keeps working.
Step 5: Testing Without Hitting the Network
Tests that hit live APIs are flaky, slow, and expensive. Use one of these approaches:
Protocol-based mocking
Because your services depend on HTTPClient, you can inject a MockHTTPClient that returns canned responses. Perfect for unit tests on view models and services.
URLProtocol stubs
For end-to-end tests of the networking layer itself, register a custom URLProtocol subclass on a dedicated URLSessionConfiguration. You intercept real requests and return fixture data.
final class StubURLProtocol: URLProtocol {
static var responder: ((URLRequest) -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool { true }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { request }
override func startLoading() {
guard let (response, data) = Self.responder?(request) else { return }
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
}
override func stopLoading() {}
}
Contract testing
For critical integrations, keep a small suite of tests that do hit a sandbox environment, scheduled nightly. They catch breaking changes from the third party before your users do.
Step 6: Observability and Resilience in Production
Once your app is live, you need to know when an integration degrades. Bare minimum:
- Log every failed request with endpoint, status, and a correlation ID (never log tokens or PII)
- Forward errors to Sentry, Crashlytics, or Datadog with the third party name as a tag
- Add a circuit breaker: if an endpoint fails N times in a row, stop hammering it for a minute
- Cache successful responses where it makes sense, so a third party outage degrades gracefully instead of breaking the UI

Common Pitfalls We See in iOS Codebases
- Putting API keys in the Info.plist and assuming they are safe. They are not.
- Decoding responses on the main thread. Use a background
JSONDecodercall, especially for large payloads. - Ignoring App Transport Security. Stick to HTTPS, avoid ATS exceptions in production.
- Missing the App Store privacy disclosure. If you call a third party that processes user data, declare it in your privacy nutrition labels and your privacy manifest.
- One giant APIClient class. Split by feature so changes stay scoped.
A Reference Architecture, Summarized
| Layer | Responsibility | Depends on |
|---|---|---|
| View / View Model | Presentation, user actions | Service protocols |
| Service | Feature-level operations (login, fetchOrders) | APIClient |
| APIClient | Endpoints, decoding, DTO mapping | HTTPClient |
| HTTPClient | Transport, auth headers, retries | URLSession, TokenStore |
| TokenStore | Read / write tokens securely | Keychain |
Each layer is small, has one job, and is easy to mock. That is the whole game.
FAQ
Should I use a networking library like Alamofire or stick with URLSession?
In 2026, URLSession with async/await covers 95% of needs. Reach for Alamofire only if you need its specific features (multipart helpers, reachability, advanced retry policies) and you accept the dependency cost.
Where do I store an API key safely on iOS?
If it is a secret key, do not store it in the app at all. Proxy through your backend. If it is a public client ID (OAuth client ID, Firebase config), the Info.plist or an xcconfig is fine.
How do I handle a third party API that breaks in production?
Three layers of defense: tolerant decoding so minor changes do not crash, observability so you detect it fast, and a fallback path (cached data or graceful empty state) so users are not blocked.
Do I need to disclose third party APIs to Apple?
Yes. App Store Connect requires privacy nutrition labels, and since 2024 Apple requires a privacy manifest listing required reason APIs and tracking SDKs. Check the third party’s documentation for the manifest they publish.
Async/await or Combine for networking?
Async/await for one-shot requests, which is most third party API calls. Combine or AsyncStream for push-based streams (WebSockets, server-sent events).
How do I test code that uses ASWebAuthenticationSession?
Wrap it behind a protocol like AuthSessionStarting and inject it. Your unit tests use a fake that returns a canned callback URL. The real implementation is only exercised in manual or UI tests.
Clean third party API integration on iOS is not about clever code. It is about boring, predictable layers: a protocol-based HTTPClient, secure token storage, typed errors, tolerant decoding, and tests that do not touch the network. Get those right and adding the next integration becomes a small, scoped task instead of a rewrite.
If you want help architecting or auditing your iOS integrations, the team at Iris does this every day. Reach out through irisapp.cc.

