# SDK options versus observed traffic (Superwall-iOS 4.15.3, purchases-ios 5.21.1)

Compiled 10 October 2026 from the SDK source at the pinned tags (`github.com/superwall/Superwall-iOS/tree/4.15.3`, `github.com/RevenueCat/purchases-ios/tree/5.21.1`; file paths and line numbers refer to those tags) and from the 10 published HAR files (`data/*.pub.har`). Observed values are given as counts over the files; "20/20" means all 20 cold starts with content. Vendor dashboard settings are not visible from the device and are outside this table.

Verdicts: **L-used** = limiting setting in effect; **L-unused** = limiting setting available in this SDK version, not used; **E-on** = expanding setting switched on by the app; **default** = default with no limiting alternative in this version; **n/a** = no effect on network data.

## 1. Superwall-iOS 4.15.3

The app sends every `SuperwallOptions` value in the `config_attributes` event (22 fields, identical in 20/20 starts), so the configuration is observed directly.

| Option / call (code) | Default | Effect on traffic | Observed | Verdict |
|---|---|---|---|---|
| `isExternalDataCollectionEnabled` (`Config/Options/SuperwallOptions.swift#L305`) | `true` | `false` only makes `PlacementsQueue` (`#L79-95`) drop `trigger_fire`, `user_attributes` and the app's own placements (`Nudge`, `Onboarding`). `device_attributes`, launch/session events, `enrich`, headers, `entitlements`, `redeem` are unaffected. Counted on the files: 5–10 % of events per file. A switch covering all events (`eventTrackingBehavior = .none`) exists only from 4.16.0 (released 30 June 2026); it does not cover the `enrich` request, which has no switch in any version up to 4.18.0 (checked 17 October 2026). | `true` 20/20 | L-unused (partial) |
| `paywalls.shouldPreload` (`PaywallOptions.swift#L103`) | `true` | `false` stops `preloadAllPaywalls()` after config fetch (`ConfigManager.swift#L559-564`): no `GET /paywall/<id>` per paywall, no webview load (connections to `user-content.superwalleditor.com`, Google Fonts, `js.stripe.com`), no `paywallPreload_*`, `paywallResponseLoad_*`, `paywallWebviewLoad_*`, `paywallProductsLoad_*`, `shimmerView_*` events: about 35 of the 41–73 events per start. Paywalls would load on presentation; on a device with an active entitlement the rules never allow presentation. | `true` 20/20; `paywallPreload_start` in every start with content | L-unused |
| `shouldBypassAppTransactionCheck` (`SuperwallOptions.swift#L326`) | `false` | `true` skips `AppTransaction.shared` (`ReceiptManager.swift#L86-123`): no `$appTransactionId` in `device_attributes` and no one-off `POST /redeem` registering it. Described in the SDK changelog and documentation as a testing option (the code comment does not say so). | `$appTransactionId` present in 28/37 `device_attributes` (absent in 9: 7 with 50 fields, 2 with 58 fields); key present in 5/5 `redeem` bodies | L-unused (testing option) |
| `storeKitVersion` (`SuperwallOptions.swift#L128`, `#L387-403`) | StoreKit 2 on iOS 15+ | With StoreKit 1 the `redeem` body carries no JWS receipts (`SK1ReceiptManager`, `transactionReceipts = []`); with StoreKit 2 the SDK collects a JWS per original transaction. Note: `Superwall.swift#L771-777` forces StoreKit 1 when configured from Objective-C with a purchase controller; the app's public Swift code sets it explicitly. | `STOREKIT1` 20/20; `receipts` empty in 5/5 `redeem` | L-used |
| `enableExperimentalDeviceVariables` (`#L323`) | `false` | `true` adds `latestSubscription*` fields to `device_attributes`/`enrich` (StoreKit 2 only). | `false` 20/20; no such keys | default (expansion not enabled) |
| `shouldObservePurchases` (`#L298`) | `false` | `true` reports StoreKit transactions made outside paywalls. | `false` 20/20 | default (expansion not enabled) |
| `localeIdentifier` (`#L315`) | `nil` | Overrides the locale value sent; does not remove any field. | `null` 20/20 | default |
| `localResources` (`#L46`) | empty | Serves paywall resources from the app bundle instead of the network, only for resources the paywall design marks as local. | `$localResourceIds` empty 37/37 | default (depends on paywall design) |
| `networkEnvironment`, `isGameControllerEnabled`, `testModeBehavior`, `maxConfigRetryCount`, `logging.level`, all remaining `PaywallOptions` (haptics, alerts, `automaticallyDismiss`, `overrideProductsByName`, `transactionBackgroundView`, restore texts) | defaults | UI or local behaviour only. | defaults 20/20 (`$logLevel` 20 = `.info`) | n/a |
| `configure(... purchaseController:)` (`Superwall.swift#L608-613`) | `nil` | A custom controller changes who completes purchases and where the subscription status comes from; it does not reduce events. | `$using_purchase_controller: true` 20/20 | n/a |
| `identify(userId:)` (`Identity/PublicIdentity.swift#L24-32`) | not called | Would send `identity_alias`, add `appUserId` to `user_attributes`, `X-App-User-ID`, and a `POST /redeem`. | no `identity_alias` in 10/10 files; `X-App-User-ID` empty in all requests; `$appUserId` empty 37/37 | expansion not used |
| `setUserAttributes(_:)` | no custom attributes | Each call sends `user_attributes` with the merged dictionary and adds it to `enrich`. | 45 `user_attributes` events, keys only `aliasId`, `applicationInstalledAt`, `seed` (SDK-added); same 3 keys in `enrich` | expansion not used |
| `setIntegrationAttributes` (`Superwall.swift#L902-937`) | not called | Only path on which the SDK reads the IDFA; sends `integration_attributes` and `redeem` metadata. | none in 10/10 files; `redeem` metadata empty 5/5; no `idfa` field | expansion not used |
| `delegate`, `setPlatformWrapper`, `setInterfaceStyle`, `reset()` | not set / not called | Local copies of events or additional `device_attributes`/`config_attributes`. | `$has_delegate: false` 20/20; no `reset` event | n/a |

### Behaviour with no switch in 4.15.3

| Behaviour | Code | Switchable by an SDK option? | Observed |
|---|---|---|---|
| `POST /api/v1/enrich` with the full device template; response with `ipCountry`, `ipRegion`, `ipCity`, `ipContinent`, `ipV4`, `ipV4ObservedAt`, `demandScore`, `demandTier` | `ConfigManager.swift#L174-178`, `#L281-322` (unconditional in `fetchConfiguration` and `refreshConfiguration`); request body `DeviceHelper.swift#L506-520` | No | 25 requests, 25 responses with 8 fields |
| Merging the `enrich` response into `device_attributes`, `enrichment_complete`, `paywall_open` | `DeviceHelper.swift#L602-607`; `PaywallViewController.swift#L355-356` | No | 8 `enrich` fields in 58–61-field `device_attributes` |
| `device_attributes` event on every start | `ConfigManager.swift#L190-194` | No (`PlacementsQueue` does not filter it) | 37 events |
| `X-Vendor-ID`, `X-App-Install-Date`, `X-Device-*` and other `X-*` headers on every request | `DependencyContainer.swift#L406-445` | No | 27 header names, as in code |
| Lifecycle events (`app_install`, `first_seen`, `app_launch`, `app_open`/`app_close`, `session_start`, `config_refresh`, `enrichment_*`, `customerInfo_didChange`, `subscriptionStatus_didChange`) | `Storage.swift#L191-210`, `Superwall.swift#L444`, `ConfigManager.swift#L325-342` | No. "Verbose" paywall-load events can be disabled only by the server flag `disable_verbose_events` (`TrackingLogic.swift#L92-140`). | `toggles` in `static_config` contain only `enable_text_interaction`, so verbose events are sent |
| `GET subscriptions-api …/users/$SuperwallDevice:<IDFV>/entitlements` on every start | `ConfigManager.swift#L429-433`, `WebEntitlementRedeemer`: no condition on the first `processConfig` | No (test mode only) | 23 requests; `web2app_config` null 6/6 |
| One `POST …/redeem` per installation registering `appTransactionId` | `ReceiptManager.swift#L105-123` | Indirectly, via `shouldBypassAppTransactionCheck` | 5 observations |
| Apple Search Ads attribution in Superwall | `AttributionPoster.swift#L222` reads `config.attribution.appleSearchAds.enabled` (dashboard setting; the former `collectAdServicesAttribution` option was removed in 3.12.0) | Not by an SDK option; dashboard | `apple_search_ads.enabled: false` 6/6 (limiting dashboard setting in effect) |
| IDFA | read only on the `setIntegrationAttributes` path | not read by default | absent |

## 2. purchases-ios 5.21.1

RevenueCat sends no snapshot of its configuration; settings are inferred from endpoints, headers and receipt bodies.

| Option / call (code) | Default | Effect on traffic | Observed | Verdict |
|---|---|---|---|---|
| `Configuration.Builder.with(appUserID:)` (`Purchasing/Configuration.swift#L97`, `#L159`) | `nil` → `$RCAnonymousID:<random>` | A custom ID would tie RevenueCat records to the app's user and call `POST /v1/subscribers/identify`. The anonymous ID is persisted and restored after reinstallation. | all subscriber paths and receipt bodies use `$RCAnonymousID:`; 0 × `/identify` | L-used (default) |
| `with(purchasesAreCompletedBy:)` (`#L106`, `#L182-189`) | `.revenueCat` | `.myApp` only changes who finishes transactions; same endpoints and headers (plus no `product_entitlement_mapping`). | `X-Observer-Mode-Enabled: false` in all requests; `observer_mode: false` 22/22 | n/a |
| `with(storeKitVersion:)` (`#L115`, `Misc/StoreKitVersion.swift#L36`) | **`.storeKit2`** | StoreKit 1 sends the full App Store receipt file (all transactions of the app); StoreKit 2 sends per-transaction JWS. Format, not field list. | `X-StoreKit-Version: 1` in all requests, so the app set StoreKit 1 explicitly | n/a (format); explicit app setting |
| `with(diagnosticsEnabled:)` (`#L114`, `#L279-282`) | `false` | `true` sends diagnostics to `api-diagnostics.revenuecat.com`. | host absent from HAR, SNI and DNS | L-used (default) |
| `with(entitlementVerificationMode:)` (`#L112`) | `.informational` | Adds `X-Nonce` and hash headers; no identifying data. | present as in default mode | n/a |
| `with(platformInfo:)`, `with(showStoreMessagesAutomatically:)`, timeouts, `userDefaults`, `proxyURL`, `logLevel` | defaults | No effect on scope. | defaults | n/a |
| `DangerousSettings.autoSyncPurchases` (`Misc/DangerousSettings.swift#L59`) | `true` | `false` stops automatic receipt posting for StoreKit queue transactions; marked "dangerous" by RevenueCat. | 6 receipts with `initiation_source: queue` | L-unused (not a recommended setting) |
| `DangerousSettings.customEntitlementComputation` (`#L79`) | `false` | `true` removes the automatic `GET /subscribers/<id>` on start, `$attConsentStatus` and the AdServices token, but requires a custom app user ID. | header absent; automatic GET on every start | L-unused (requires identification, so not a pure limitation) |
| `Attribution.enableAdServicesAttributionTokenCollection()` (`Attribution.swift#L29`, `#L65-69`; `AttributionPoster.swift#L109-136`) | **off** | Once per installation: `POST /v1/subscribers/<id>/adservices_attribution` with `{aad_attribution_token}`, an Apple AdServices token the server can exchange for Apple Search Ads campaign data. | 6 × POST, one per first launch (S01, S17, A5 ×2, A8, C8), 0.2–2 s before the first receipt | **E-on** (the only option here the app had to enable explicitly) |
| `Attribution.collectDeviceIdentifiers()` (`#L95-97`) | not called | Would set `$idfa` (with ATT consent), `$idfv`, `$ip` as subscriber attributes. | no `POST …/attributes`; receipt `attributes` contain only `$attConsentStatus` | L-used (default) |
| `Attribution.setAttributes` / `setEmail` / `setPhoneNumber` / `setDisplayName` / `setPushToken` / integration IDs (Adjust, AppsFlyer, Facebook, mParticle, OneSignal, Airship, CleverTap, Kochava, Mixpanel, Firebase, Tenjin, PostHog) / campaign fields (`Attribution.swift#L109-413`) | none | Each attribute is posted to `/subscribers/<id>/attributes` and added to receipts. | none | L-used (default) |
| `Purchases.logIn` / `logOut` (`Purchases.swift#L844-907`) | not called | `logIn` posts `/subscribers/identify` linking the anonymous and the new ID. | 0 × `/identify` | L-used (default) |
| `Purchases.restorePurchases()` / `syncPurchases()` (`#L1045-1053`, `#L1100-1107`) | not called by the SDK | Each call posts `/v1/receipts` with the full App Store receipt, product data, `attributes` and `is_restore`; also when the user never bought anything. | 16 receipts with `initiation_source: restore`: three within about 2 s, with the same receipt, on every first launch (S01, S17, A5 ×2, A8) and one on C8 without product data | **E-on** (explicit app calls; the app's public code calls both) |
| `allowSharingAppStoreAccount` (deprecated) | `nil` → equals "user is anonymous" (true) | Only sets the `is_restore` flag of queue receipts. | `is_restore: false` in 5/6 queue receipts, so the app sets it to `false` | n/a |
| Automatic refresh on `configure()` and foreground (`Purchases.swift#L1952-2033`) | always | `GET /v1/subscribers/<id>`, `GET …/offerings`, `GET /v1/product_entitlement_mapping` on every start, with all device headers. | 37 / 22 / 6 requests | default |
| `X-Apple-Device-Identifier` (IDFV) header (`Networking/HTTPClient/HTTPClient.swift#L148-150`; `Misc/SystemInfo.swift#L129-144`) | always on iOS | No option in 5.21.1. | all requests | **default (no switch)** |
| Other constant headers (`X-Platform-Device`, `X-Platform-Version`, `X-Client-*`, `X-Preferred-Locales`, `X-Storefront`, `X-Is-Sandbox`, `X-Is-Debug-Build`, `X-Is-Backgrounded`, `X-StoreKit*`, `X-Observer-Mode-Enabled`, ETag) (`HTTPClient.swift#L120-142`) | always | No option. | all requests | default |
| `$attConsentStatus` in every receipt (`CustomerAPI.swift#L97-105`; `AttributionFetcher.swift#L188-207`) | always | Value is the ATT status, or `denied` when the AppTrackingTransparency framework is not linked and the system is 14.5 or later. | `denied` 22/22; the app's public code contains no ATT framework | default; `denied` here means "app does not ask" |
| `app_transaction` (JWS `AppTransaction`) in receipts (`TransactionPoster.swift#L110`; `PostReceiptDataOperation.swift#L275`, `#L293`) | always for queue transactions on iOS 16+ | Contains `appTransactionId`, the same identifier Superwall sends as `$appTransactionId`. | 6/22 receipts (all queue receipts) | default |
| Product data in receipts (`Purchasing/ProductRequestData.swift#L71-113`) | always when the product is known | `product_id`, price, currency, store country, payment mode, durations, subscription group. | 21/22 (none on C8) | default |
| RevenueCat paywall/Customer Center events, `health`, legacy attribution endpoint | not used | Would add `api-paywalls.revenuecat.com` events, `/attribution` with `rc_idfa`/`rc_idfv`. | hosts and paths absent | L-used (default) |
| SDK privacy manifest `Sources/PrivacyInfo.xcprivacy` | declares `PurchaseHistory`, not linked, no tracking | Vendor's own manifest, not an option. | – | informational |

## 3. Summary

- Superwall: 3 limiting options available and unused (`isExternalDataCollectionEnabled` with partial effect, `paywalls.shouldPreload`, `shouldBypassAppTransactionCheck`); 1 limiting setting in effect (StoreKit 1); no expanding option enabled; 9 behaviours with no switch in 4.15.3, including `enrich`, `device_attributes`, the IDFV and the lifecycle events.
- RevenueCat: the limiting settings in effect are all defaults; 2 expanding behaviours switched on by the app (AdServices token, triple restore on first launch); the IDFV header and the automatic start requests have no switch.
- In the app's public code (`github.com/strongbox-password-safe/Strongbox`, state 1.65.0) the only option set in either SDK is the StoreKit version.
