Apple Search Ads Server-Side vs Client-Side | OpsBlu Docs

Apple Search Ads Server-Side vs Client-Side

Comparison and implementation guidance for server-side and client-side Apple Search Ads attribution tracking.

Overview

Apple Search Ads attribution can be implemented using client-side (in-app) or server-side (backend API) approaches. Understanding the trade-offs is essential for accurate attribution, privacy compliance, and optimal performance.

Client-Side Attribution

How It Works

  • ADServices framework runs in the iOS app
  • Attribution token requested from device
  • Token validated with Apple's API from client
  • SKAdNetwork postbacks received from Apple servers
  • Attribution data stored locally in app

Implementation

import AdServices

// Client-side attribution token retrieval
func getClientSideAttribution() {
    guard let token = try? AAAttribution.attributionToken() else {
        print("No attribution token available")
        return
    }

    // Validate directly from client
    validateAttributionClientSide(token: token)
}

func validateAttributionClientSide(token: String) {
    let endpoint = "https://api-adservices.apple.com/api/v1/"
    var request = URLRequest(url: URL(string: endpoint)!)
    request.httpMethod = "POST"
    request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
    request.httpBody = token.data(using: .utf8)

    URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data else {
            print("Error: \(error?.localizedDescription ?? "Unknown")")
            return
        }

        if let attribution = try? JSONDecoder().decode(AttributionResponse.self, from: data) {
            // Store locally
            saveAttributionLocally(attribution)

            // Optionally send to your server
            sendAttributionToServer(attribution)
        }
    }.resume()
}

struct AttributionResponse: Codable {
    let attribution: Bool
    let orgId: Int?
    let campaignId: Int?
    let adGroupId: Int?
    let keywordId: Int?
}

Advantages

  • Simple implementation: Add framework and make API call
  • Real-time attribution: Immediate results on device
  • No server dependencies: Works without backend infrastructure
  • Lower latency: Direct communication with Apple's API
  • Offline capable: Can queue and retry locally
  • Easier debugging: Test directly on device

Limitations

  • Network dependency: Requires internet connection at app launch
  • Client-side storage: Attribution data only on device
  • Limited analytics: Harder to aggregate across users
  • Privacy constraints: Limited by iOS privacy frameworks
  • No historical analysis: Data lost if app deleted
  • Fraud vulnerability: Client-side validation easier to spoof

Server-Side Attribution

How It Works

  • App sends attribution token to your server
  • Server validates token with Apple's API
  • Attribution data stored in your database
  • Server-to-server communication for validation
  • Centralized attribution data management

Implementation

Client-Side: Send Token to Server

import AdServices

func getServerSideAttribution() {
    guard let token = try? AAAttribution.attributionToken() else {
        print("No attribution token available")
        return
    }

    // Send to your server for validation
    sendTokenToServer(token: token)
}

func sendTokenToServer(token: String) {
    let endpoint = "https://api.yourdomain.com/attribution/validate"
    var request = URLRequest(url: URL(string: endpoint)!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")

    let payload: [String: Any] = [
        "attribution_token": token,
        "device_id": getDeviceId(),
        "app_version": getAppVersion(),
        "os_version": UIDevice.current.systemVersion,
        "timestamp": Date().timeIntervalSince1970
    ]

    request.httpBody = try? JSONSerialization.data(withJSONObject: payload)

    URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data else { return }

        if let result = try? JSONDecoder().decode(ServerAttributionResult.self, from: data) {
            print("Server attribution: \(result)")
        }
    }.resume()
}

struct ServerAttributionResult: Codable {
    let success: Bool
    let attribution: AttributionResponse?
    let userId: String?
}

func getDeviceId() -> String {
    return UIDevice.current.identifierForVendor?.uuidString ?? ""
}

func getAppVersion() -> String {
    return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
}

Server-Side: Validate and Store (Python Example)

from flask import Flask, request, jsonify
import requests
import hashlib
import time
from datetime import datetime

app = Flask(__name__)

@app.route('/attribution/validate', methods=['POST'])
def validate_attribution():
    data = request.json
    token = data.get('attribution_token')
    device_id = data.get('device_id')

    if not token:
        return jsonify({'success': False, 'error': 'No token provided'}), 400

    # Validate with Apple's API
    attribution = validate_with_apple(token)

    if attribution:
        # Store in database
        user_id = store_attribution(
            device_id=device_id,
            attribution=attribution,
            app_version=data.get('app_version'),
            os_version=data.get('os_version')
        )

        return jsonify({
            'success': True,
            'attribution': attribution,
            'user_id': user_id
        })
    else:
        return jsonify({
            'success': False,
            'error': 'Attribution validation failed'
        }), 400

def validate_with_apple(token):
    """Validate attribution token with Apple's API"""
    endpoint = 'https://api-adservices.apple.com/api/v1/'

    try:
        response = requests.post(
            endpoint,
            data=token,
            headers={'Content-Type': 'text/plain'},
            timeout=10
        )

        if response.status_code == 200:
            return response.json()
        else:
            print(f"Apple API error: {response.status_code}")
            return None

    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return None

def store_attribution(device_id, attribution, app_version, os_version):
    """Store attribution in database"""
    # Example using SQLAlchemy or your DB
    user_id = hashlib.sha256(device_id.encode()).hexdigest()[:16]

    attribution_record = {
        'user_id': user_id,
        'device_id': device_id,
        'attribution_source': 'apple_search_ads' if attribution.get('attribution') else 'organic',
        'campaign_id': attribution.get('campaignId'),
        'ad_group_id': attribution.get('adGroupId'),
        'keyword_id': attribution.get('keywordId'),
        'country': attribution.get('countryOrRegion'),
        'app_version': app_version,
        'os_version': os_version,
        'attributed_at': datetime.utcnow(),
        'created_at': datetime.utcnow()
    }

    # Insert into database
    # db.attribution.insert_one(attribution_record)

    return user_id

Server-Side: Node.js Example

const express = require('express');
const axios = require('axios');
const crypto = require('crypto');

const app = express();
app.use(express.json());

app.post('/attribution/validate', async (req, res) => {
    const { attribution_token, device_id, app_version, os_version } = req.body;

    if (!attribution_token) {
        return res.status(400).json({
            success: false,
            error: 'No token provided'
        });
    }

    try {
        // Validate with Apple's API
        const attribution = await validateWithApple(attribution_token);

        if (attribution) {
            // Store in database
            const userId = await storeAttribution({
                deviceId: device_id,
                attribution: attribution,
                appVersion: app_version,
                osVersion: os_version
            });

            res.json({
                success: true,
                attribution: attribution,
                userId: userId
            });
        } else {
            res.status(400).json({
                success: false,
                error: 'Attribution validation failed'
            });
        }
    } catch (error) {
        console.error('Attribution error:', error);
        res.status(500).json({
            success: false,
            error: 'Internal server error'
        });
    }
});

async function validateWithApple(token) {
    const endpoint = 'https://api-adservices.apple.com/api/v1/';

    try {
        const response = await axios.post(endpoint, token, {
            headers: {
                'Content-Type': 'text/plain'
            },
            timeout: 10000
        });

        return response.data;
    } catch (error) {
        console.error('Apple API error:', error);
        return null;
    }
}

async function storeAttribution(data) {
    const userId = crypto.createHash('sha256')
        .update(data.deviceId)
        .digest('hex')
        .substring(0, 16);

    const attributionRecord = {
        userId: userId,
        deviceId: data.deviceId,
        attributionSource: data.attribution.attribution ? 'apple_search_ads' : 'organic',
        campaignId: data.attribution.campaignId,
        adGroupId: data.attribution.adGroupId,
        keywordId: data.attribution.keywordId,
        country: data.attribution.countryOrRegion,
        appVersion: data.appVersion,
        osVersion: data.osVersion,
        attributedAt: new Date(),
        createdAt: new Date()
    };

    // Insert into database
    // await db.collection('attributions').insertOne(attributionRecord);

    return userId;
}

Advantages

  • Centralized data: All attribution in one database
  • Advanced analytics: Easy to aggregate and analyze
  • Data persistence: Attribution data survives app deletion
  • Fraud prevention: Server-side validation harder to spoof
  • Cross-platform: Link mobile attribution with web analytics
  • Historical analysis: Query attribution trends over time
  • Data enrichment: Combine with other data sources
  • Privacy control: Manage PII server-side

Limitations

  • Implementation complexity: Requires backend development
  • Infrastructure costs: Server hosting and maintenance
  • Latency: Additional network hop to your server
  • Server dependency: Requires backend availability
  • More moving parts: Client + server + database
  • Debugging complexity: Harder to troubleshoot

Combine both methods for maximum reliability and functionality:

Implementation

// Client-side: Hybrid attribution
func getHybridAttribution() {
    guard let token = try? AAAttribution.attributionToken() else {
        print("No attribution token available")
        return
    }

    // 1. Validate client-side for immediate use
    validateAttributionClientSide(token: token)

    // 2. Send to server for long-term storage
    sendTokenToServer(token: token)
}

func validateAttributionClientSide(token: String) {
    // Quick client-side validation
    let endpoint = "https://api-adservices.apple.com/api/v1/"
    var request = URLRequest(url: URL(string: endpoint)!)
    request.httpMethod = "POST"
    request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
    request.httpBody = token.data(using: .utf8)

    URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data,
              let attribution = try? JSONDecoder().decode(AttributionResponse.self, from: data) else {
            return
        }

        // Store locally for offline access
        saveAttributionLocally(attribution)

        // Update UI immediately
        DispatchQueue.main.async {
            self.updateUIWithAttribution(attribution)
        }
    }.resume()
}

SKAdNetwork: Server-Side Postback Handling

Configure Postback URL

In App Store Connect, configure your postback URL to receive SKAdNetwork conversions:

https://api.yourdomain.com/skadnetwork/postback

Server-Side Postback Handler (Python)

from flask import Flask, request
import json
import hmac
import hashlib

@app.route('/skadnetwork/postback', methods=['POST'])
def handle_skadnetwork_postback():
    """Handle SKAdNetwork postback from Apple"""
    data = request.json

    # Validate postback signature
    if not validate_skadnetwork_signature(data):
        return jsonify({'error': 'Invalid signature'}), 403

    # Extract conversion data
    conversion_data = {
        'campaign_id': data.get('campaign-id'),
        'conversion_value': data.get('conversion-value'),
        'source_app_id': data.get('source-app-id'),
        'transaction_id': data.get('transaction-id'),
        'redownload': data.get('redownload', False),
        'received_at': datetime.utcnow()
    }

    # Store in database
    store_skadnetwork_conversion(conversion_data)

    return jsonify({'status': 'success'}), 200

def validate_skadnetwork_signature(data):
    """Validate SKAdNetwork postback signature"""
    # Implement signature validation
    # https://developer.apple.com/documentation/storekit/skadnetwork
    return True  # Simplified for example

def store_skadnetwork_conversion(data):
    """Store SKAdNetwork conversion"""
    # db.skadnetwork_conversions.insert_one(data)
    pass

Use Case Recommendations

E-commerce Apps

Recommended: Hybrid approach

  • Client-side: Immediate attribution for personalization
  • Server-side: Long-term LTV tracking and analytics

Gaming Apps

Recommended: Client-side + SKAdNetwork

  • Client-side: Quick attribution for gameplay customization
  • SKAdNetwork: Privacy-preserving conversion tracking
  • Optional server-side for cohort analysis

Subscription Apps

Recommended: Server-side

  • Centralized subscription tracking
  • Cross-platform subscription management
  • Advanced revenue analytics

Enterprise Apps

Recommended: Server-side

  • Compliance and audit requirements
  • Integration with enterprise systems
  • Advanced security controls

Decision Matrix

Scenario Client-Side Server-Side Hybrid
Simple attribution
Advanced analytics
Offline capability
Fraud prevention
Cross-platform tracking
Data persistence
Low latency
Privacy compliance

Performance Comparison

Metric Client-Side Server-Side Hybrid
Implementation Time 1-2 days 3-5 days 4-7 days
Latency 100-300ms 200-500ms 100-500ms
Reliability Medium High High
Data Quality Medium High High
Maintenance Low Medium Medium
Cost Low Medium Medium

Best Practices

  1. Start with client-side, migrate to hybrid as needs grow
  2. Always validate server-side for high-value conversions
  3. Implement retry logic for failed server communications
  4. Cache attribution locally for offline access
  5. Use server-side for fraud detection and validation
  6. Monitor attribution latency and success rates
  7. Implement fallback mechanisms for server failures
  8. Respect user privacy regardless of implementation
  9. Test both approaches before production deployment
  10. Document your architecture for team reference

Troubleshooting

Client-Side Issues

  • No attribution token: Check ADServices framework integration
  • API timeout: Implement retry with exponential backoff
  • Invalid response: Verify network connectivity and Apple API status

Server-Side Issues

  • Token validation fails: Check server can reach Apple's API
  • Database errors: Implement proper error handling and logging
  • High latency: Optimize database queries and use caching

Hybrid Issues

  • Inconsistent data: Implement deduplication logic
  • Race conditions: Use proper synchronization mechanisms
  • Increased complexity: Maintain clear documentation