Prooflytics + Apple Search Ads: Attribution from App Install to Closed B2B Deal
Apple Search Ads is an App Store advertising channel for acquiring users through search. For B2B SaaS with a mobile app, the flow looks like this: a user searches for “crm for sales” in the App Store, sees your ad, installs the app, and 2-3 weeks later becomes a paying customer. Without attribution, you only know your cost per install. With Prooflytics, you see the cost per closed B2B deal from each App Store campaign, broken down by keyword and adGroup.
B2B marketers tend to underestimate Apple Search Ads for a simple reason: standard analytics tools only show install rate and conversion to trial. The chain “keyword -> install -> trial -> qualified lead -> closed deal” typically breaks at the boundary between the mobile app and CRM. That gap is exactly what the Prooflytics + Apple Search Ads integration closes.
This article covers the AdServices attribution technology, how to pass the attributionToken to Prooflytics, and how to see the cost per closed deal from every App Store campaign.
Why Standard Tools Miss the Full Picture
Apple Search Ads provides built-in analytics: impressions, taps (clicks), installs, TTR, CR - at the campaign and keyword group level. But the data stops there - it never makes it into CRM.
After iOS 14.5 (ATT framework, 2021), tracking via IDFA requires explicit user consent. Most users decline. This means third-party MMPs (AppsFlyer, Adjust, Branch) operate with limited data.
Apple addressed this with its own solution: AdServices Attribution API - a privacy-preserving attribution mechanism that does not require IDFA. It works through a cryptographically signed attributionToken generated on the device.
AdServices attribution is Apple’s native attribution mechanism: the app requests a token on the device immediately after install, sends it to your server, and the server exchanges the token for campaign data via the Apple API. The token carries no personal user data - only campaign metadata.
Technical Architecture
iOS App
-> attributionToken() [AdServices framework]
-> POST /prooflytics/apple-attribution {token, user_id}
Prooflytics Backend
-> POST https://api-adservices.apple.com/api/v1/ {token}
<- {campaignId, adGroupId, keywordId, clickDate, countryOrRegion}
-> Save attribution record (user_id -> campaign mapping)
CRM (HubSpot / Kommo)
-> Lead/Deal created (email or user_id)
<- Prooflytics looks up attribution record by email
-> Write source: Apple Search Ads, campaignId, keyword
When deal is closed:
<- Prooflytics receives closed_won event
-> Calculate CAC: campaign spend / closed deals
-> Show in dashboard: keywords with lowest CAC
Implementation: iOS Side
In the iOS app (Swift), request the attribution token on first launch:
import AdServices
func fetchAndSendAttributionToken(userId: String) {
guard #available(iOS 14.3, *) else { return }
do {
// Get attribution token. TTL: 24 hours
let token = try AAAttribution.attributionToken()
// Send to Prooflytics immediately
sendToProoflytics(token: token, userId: userId)
} catch {
// Token unavailable if user did not come from an ad
// This is normal - simply no attribution data
print("No attribution token: \(error)")
}
}
func sendToProoflytics(token: String, userId: String) {
let url = URL(string: "https://api.prooflytics.io/v1/attribution/apple")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer YOUR_PROOFLYTICS_API_KEY", forHTTPHeaderField: "Authorization")
let body: [String: Any] = [
"token": token,
"user_id": userId,
"app_id": "your.bundle.identifier"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request).resume()
}
Call fetchAndSendAttributionToken on the first app launch after registration or sign-in - once userId is already known.
Implementation: Server Side (Prooflytics)
import requests, time
from flask import Flask, request, jsonify
app = Flask(__name__)
APPLE_ATTRIBUTION_API = "https://api-adservices.apple.com/api/v1/"
@app.route("/v1/attribution/apple", methods=["POST"])
def receive_apple_attribution():
data = request.json
token = data.get("token", "")
user_id = data.get("user_id", "")
app_id = data.get("app_id", "")
if not token or not user_id:
return jsonify({"error": "missing fields"}), 400
# Exchange token for campaign data with Apple
attribution = exchange_token(token)
if not attribution:
return jsonify({"status": "no_attribution"}), 200
# Save attribution record to DB
save_attribution(user_id, app_id, attribution)
return jsonify({"status": "ok"}), 200
def exchange_token(token: str) -> dict | None:
"""Exchange attribution token with Apple API. Token TTL: 24 hours."""
try:
r = requests.post(
APPLE_ATTRIBUTION_API,
json={"token": token},
headers={"Content-Type": "application/json"},
timeout=5
)
if r.status_code == 200:
data = r.json()
# attribution = None if user did not come from an ad
if data.get("attribution") is True:
return data
return None
except Exception:
return None
def save_attribution(user_id: str, app_id: str, data: dict):
"""
Example attribution payload (with ATT consent):
{
"attribution": true,
"orgId": 12345,
"campaignId": 678901,
"adGroupId": 234567,
"keywordId": 890123,
"adId": 456789,
"countryOrRegion": "US",
"conversionType": "download",
"clickDate": "2026-05-20T14:23:00Z"
}
"""
# Save to Prooflytics attribution store
# In a real implementation - INSERT into PostgreSQL table apple_attribution
record = {
"user_id": user_id,
"app_id": app_id,
"campaign_id": data.get("campaignId"),
"ad_group_id": data.get("adGroupId"),
"keyword_id": data.get("keywordId"),
"country": data.get("countryOrRegion"),
"click_date": data.get("clickDate"),
"conversion_type": data.get("conversionType"),
"org_id": data.get("orgId"),
"received_at": int(time.time()),
}
db.insert("apple_attribution", record)
Linking Attribution to a CRM Deal
When a lead from the mobile app is registered in CRM (HubSpot or Kommo), Prooflytics looks up the attribution record by user_id or email:
def enrich_lead_with_attribution(email: str, user_id: str) -> dict | None:
"""Find Apple Search Ads attribution for this user."""
record = db.query_one(
"SELECT * FROM apple_attribution WHERE user_id = %s ORDER BY received_at DESC LIMIT 1",
(user_id,)
)
if not record:
return None
# Retrieve campaign name and keyword from Apple Ads API
campaign_name = get_campaign_name(record["org_id"], record["campaign_id"])
keyword_text = get_keyword_text(record["org_id"], record["keyword_id"])
return {
"source": "Apple Search Ads",
"campaign_id": record["campaign_id"],
"campaign_name": campaign_name,
"keyword": keyword_text,
"click_date": record["click_date"],
"country": record["country"],
}
This data is written to CRM as UTM parameters or custom source properties - exactly the same way as for Google Ads (via gclid) or Meta (via fbclid).
What the Marketer Sees in Prooflytics
Once set up, the Prooflytics dashboard shows:
- CAC by campaign: “iPhone Users - US campaign: $847 per closed deal”
- CAC by keyword: “b2b crm app keyword: 3 deals this month, $612 CAC”
- Time to deal: average time from install to close (e.g., 34 days)
- Funnel by source: install -> trial -> qualified lead -> closed deal - per ad group
Instead of “we spent $5,000 on App Store and got 200 installs,” the marketer sees “of 200 installs, 12 converted to paid plans, average CAC $416, best keyword by CAC - ‘field service app’.”
Real-World Case
A B2B SaaS company in the field service space: iOS app, enterprise sales via an SDR team. Apple Search Ads was used as an acquisition channel, but effectiveness was only measured by installs and trials.
After integrating with Prooflytics:
- 4 months of data: $18,000 spent on Apple Search Ads, 840 installs
- Of 840 installs - 23 closed deals (2.7% install-to-deal conversion)
- Average CAC via App Store: $782 vs $1,240 via Google Ads
- Keyword “field service management app” - best CAC ($490), bids increased by 40%
- Keyword “work order app” - 0 closed deals over 4 months, paused
Who This Is For
B2B SaaS companies that:
- Have an iOS app as their primary or significant onboarding channel
- Spend $5,000+/month on Apple Search Ads
- Manage their B2B pipeline in HubSpot, Salesforce, or Kommo
- Want to see cost per deal, not just cost per install
This is especially relevant for mobile-first B2B: field service, project management, logistics, sales enablement.
A similar approach is described for other ad channels: Prooflytics + Google Ads, Prooflytics + Meta Ads. Apple Search Ads differs in its attribution mechanism (device-level token instead of UTM/pixel), but the logic of closing the loop on the deal is identical.
Frequently Asked Questions
Does attribution work without ATT consent?
Yes. AdServices Attribution API provides two levels of data: detailed (with ATT consent) and limited (without consent). In limited mode, campaignId and adGroupId are available, but not keywordId or countryOrRegion. For campaign-level attribution, this is sufficient. Keyword-level data requires ATT consent.
What is the TTL of the attributionToken?
24 hours from the time it is created. If the token is not exchanged within that window, it is invalidated and the data is lost. This is why it is important to call AAAttribution.attributionToken() and send the token to Prooflytics immediately on first launch - not defer it.
Should I use an MMP (AppsFlyer, Adjust) alongside Prooflytics?
Using both is possible and adds complementary coverage. MMPs handle Android attribution, cross-device tracking, and retargeting. Prooflytics uses AdServices for iOS attribution and focuses on closing the funnel at the CRM deal level - something MMPs do not do. For B2B SaaS with an iOS app, Prooflytics alone is sufficient without a separate MMP.
Does Apple Search Ads attribution support App Store subscriptions?
Yes. The conversionType field in the attribution payload can be download or redownload. For subscriptions via RevenueCat or StoreKit, attribution through AdServices works the same way: the token is generated at install, and the subscription event is linked to the attribution record via user_id.
Summary
Prooflytics + Apple Search Ads - full-cycle attribution for B2B mobile apps:
- iOS app:
AAAttribution.attributionToken()-> POST to Prooflytics - Backend: token exchange for campaign data via Apple Attribution API
- Attribution record linked to CRM lead / deal by
user_idor email - On deal close: CAC = campaign spend / closed deals from App Store
- Dashboard: best keywords by CAC, install -> closed deal funnel
If your B2B SaaS has an iOS app and you want to see real CAC from Apple Search Ads - contact the Prooflytics team to set up the integration.