iOS + Apollo

How to use the DataDome SDK with the Apollo Client for GraphQL

📘

Upgrading from 3.8.x or earlier?

This guide covers a fresh v4 integration. For a step-by-step
upgrade from a DataDomeSDK-based version, see MIGRATION.md.

📘

Still integrating an older version of this SDK?

The v3.x documentation has been moved to this page.

Requirements

iOS15.0+ (raised from iOS 12 in earlier versions)
Xcode26.4+ — the first version that supports Swift Package Manager package traits, which DataDomeApollo uses to select the Apollo major
Integration methodSwift Package Manager only — CocoaPods support was removed in 4.0.0
Apollo iOSv1 (1.x) or v2 (2.x) — pick one and enable the matching trait
DataDome client-side keyFind it on your DataDome Dashboard
https://app.datadome.co/dashboard/management/integrations?created=2

CocoaPods (pod "DataDomeApollo") was the legacy installation path. It is no longer supported;
integrate via Swift Package Manager.

Choosing your Apollo iOS major and adding the package

DataDomeApollo ships support for both Apollo majors in a single package and selects between them at
build time using two traits: ApolloV1 (enabled by default) and ApolloV2. SwiftPM resolves a
single Apollo version per build, so enable the trait that matches the Apollo major your app
depends on
. A mismatch is a compile error, not a silent failure.

Apollo v1 (default — no extra configuration)

Add the package to your Package.swift dependencies:

.package(url: "https://github.com/DataDome/datadome-apollo-package", from: "4.0.0")

The ApolloV1 trait is enabled by default, so nothing else is required. Existing Apollo v1
integrators get the v1 API as before.

Apollo v2 (opt in to the ApolloV2 trait)

Enable the ApolloV2 trait, and make sure your app pins apollo-ios to a 2.x version:

.package(url: "https://github.com/DataDome/datadome-apollo-package", from: "4.0.0", traits: ["ApolloV2"])

In Xcode, enable the ApolloV2 trait from the package dependency's trait settings (Project →
Package Dependencies → DataDomeApollo → Traits) rather than editing a manifest.

Whichever major you use, add the DataDomeApollo library to your app target.

Configure your DataDome client-side key

DataDomeApollo validates responses through a DataDome instance that you create and
inject. Configure it programmatically or from your Info.plist.

Option A — Programmatic

import CoreDataDome

let dataDomeConfiguration = DataDomeConfiguration(clientKey: "YOUR_CLIENT_SIDE_KEY")
let dataDome = DataDome(configuration: dataDomeConfiguration)

Create the DataDome instance once and reuse it for the lifetime of your Apollo client.

Option B — Info.plist

Add a DataDome dictionary with your ClientSideKey (and, optionally, a Domain):

<key>DataDome</key>
<dict>
    <key>ClientSideKey</key>
    <string>YOUR_CLIENT_SIDE_KEY</string>
    <!-- Optional -->
    <key>Domain</key>
    <string>https://your-domain.com</string>
</dict>

Then build the instance from the bundle:

import CoreDataDome

let dataDomeConfiguration = try DataDomeConfiguration.configurationFromBundle()
let dataDome = DataDome(configuration: dataDomeConfiguration)

DataDomeConfiguration.configurationFromBundle() throws if the DataDome dictionary or its
ClientSideKey entry is missing — handle the error, or use the programmatic initializer.

Integrate with Apollo v1

On Apollo v1, DataDome plugs in via an InterceptorProvider. Use
DataDomeInterceptorProvider, which subclasses Apollo's DefaultInterceptorProvider and inserts the DataDome interceptor immediately after NetworkFetchInterceptor (and before ResponseCodeInterceptor) so it can inspect the raw HTTP response.

import Apollo
import DataDomeApollo
import CoreDataDome

private(set) lazy var apollo: ApolloClient = {
    let store = ApolloStore(cache: InMemoryNormalizedCache())

    // Reads ClientSideKey from the Info.plist `DataDome` dict (or use the programmatic initializer).
    let dataDome = DataDome(configuration: dataDomeConfiguration)

    guard let url = URL(string: "https://your-graphql-endpoint") else {
        fatalError("Unable to create url")
    }

    let provider = DataDomeInterceptorProvider(store: store, dataDome: dataDome)

    let transport = RequestChainNetworkTransport(
        interceptorProvider: provider,
        endpointURL: url
    )

    return ApolloClient(networkTransport: transport, store: store)
}()

Run your operations exactly as you normally would.

apollo.fetch(query: MyQuery()) { result in
    // Handle result
}

Customizing the URLSession (v1)

The provider defaults its URLSessionClient. To supply your own, use the full initializer:

let provider = DataDomeInterceptorProvider(
    client: URLSessionClient(),
    store: store,
    dataDome: dataDome
)

Keep the URLSessionClient on its default configuration so the DataDome cookie stored in the shared HTTPCookieStorage is attached to retried requests

Integrate with Apollo v2

On Apollo v2, DataDome plugs in at the transport level instead of via an interceptor provider. Wrap your
session in DataDomeURLSession and pass it to Apollo v2's RequestChainNetworkTransport.

import Apollo
import DataDomeApollo
import CoreDataDome

private(set) lazy var apollo: ApolloClient = {
    let store = ApolloStore(cache: InMemoryNormalizedCache())

    let dataDome = DataDome(configuration: dataDomeConfiguration)

    guard let url = URL(string: "https://your-graphql-endpoint") else {
        fatalError("Unable to create url")
    }

    let transport = RequestChainNetworkTransport(
        urlSession: DataDomeURLSession(dataDome: dataDome),
        interceptorProvider: DefaultInterceptorProvider.shared,   // your own / Apollo's default
        store: store,
        endpointURL: url
    )

    return ApolloClient(networkTransport: transport, store: store)
}()

There is no DataDome interceptor to register, and the rest of your interceptor chain (including
your own interceptors) is untouched. Run your operations as usual:

let result = try await apollo.fetch(query: MyQuery())

Customizing the wrapped session (v2)

DataDomeURLSession wraps URLSession(configuration: .default) by default. To wrap a different
ApolloURLSession, pass it via wrapping::

let session = DataDomeURLSession(dataDome: dataDome, wrapping: myApolloURLSession)

Hard blocks (v2)

When DataDome serves a block page (the request cannot proceed), the transport throws
DataDomeError.blocked. Handle it where you execute operations:

do {
    let result = try await apollo.fetch(query: MyQuery())
} catch DataDomeError.blocked {
    // The request was blocked by DataDome.
}

Troubleshooting

  • Crash on launch / configurationFromBundle() throws — the DataDome dictionary or its
    ClientSideKey is missing from your Info.plist. Add it (see Configure your DataDome client-side key) or use the programmatic initializer.
  • No such module 'CoreDataDome' — add the CoreDataDome package product to your app target
    (it is normally resolved transitively through DataDomeApollo).
  • Cannot find type 'DataDomeURLSession' (or DataDomeError) — you are building with the
    ApolloV1 trait; enable the ApolloV2 trait (and pin apollo-ios to 2.x).
  • Cannot find type 'DataDomeInterceptorProvider' / DataDomeInterceptor — you are building with the ApolloV2 trait; these are the Apollo v1 types. Use DataDomeURLSession for v2.
  • Compile errors against the Apollo API — your enabled trait does not match the resolved
    apollo-ios major. Enable ApolloV1 for Apollo 1.x or ApolloV2 for Apollo 2.x.
  • Cannot find type 'CaptchaDelegate' / 'DataDomeURLSessionClient' / 'ApolloCompletion' — these were removed in 4.0.0. Challenge presentation is now automatic; see
    MIGRATION.md.


What’s Next

Complete your integration by following the Configuration section on the main page

Did this page help you?