ReflectDocs
Browse docsOverview
Compliance

Privacy & consent

GDPR, CCPA, ATT, advertising consent. What the SDK does, what you have to do, and the APIs that make it easy.

What Reflect collects, by default

  • Install UUID (random GUID, in PlayerPrefs); Android ID (SSAID)
  • Device taxonomy: model, manufacturer/brand, type (phone/tablet/tv), OS version + build, CPU, screen size/format/density, RAM, UI mode
  • App bundle id, version (+ version code), install source / store, first install + last update time, system-app flag
  • Locale, timezone; network type, carrier name + MCC/MNC
  • Engagement: session count & length, app foreground state; environment (production/sandbox)
  • Fraud signals: emulator, root/jailbreak, VPN, mock-location detection
  • Public IP + country (server-side, from CF headers)

What requires explicit consent

All advertising/device identifiers are gated by RequireAdvertisingConsent and scrubbed server-side if the user later denies consent.

  • GAID (Android advertising id) + its source/retry — gated by RequireAdvertisingConsent + the user’s system "Limit Ad Tracking" setting.
  • IDFA / IDFV (iOS) — gated by Apple’s ATT prompt; only after the user taps Allow.
  • Google App Set ID (Android 12+) — privacy-friendly per-developer id used for matching as GAID availability declines.
  • Amazon Fire ID (Fire OS devices only).
  • Push token (FCM/APNS) — set via SetPushToken for re-engagement; treated as a quasi-identifier and dropped on consent denial.
  • External device id — your own cross-system id set via SetExternalDeviceId; dropped on consent denial.
  • China only, OFF by default: IMEI/MEID and OAID — collected only when you explicitly enable CollectImei / CollectOaid for China distribution (require extra permissions / the China OAID SDK). Leave off unless you have a lawful basis.

Advertising consent (GDPR / EEA / UK)

If you’re distributing in regions requiring opt-in for cross-app tracking, set:

new ReflectConfig {
    RequireAdvertisingConsent = true,
    ...
}

The SDK will NOT collect GAID / IDFA until you call:

ReflectSDK.SetAdvertisingConsent(granted: true);
// or
ReflectSDK.SetAdvertisingConsent(granted: false);   // re-collects without ad ids

Wire this to your CMP (consent management platform) callback. Reflect doesn’t ship a CMP UI — every studio has different brand requirements and we don’t want to lock you into ours.

iOS App Tracking Transparency

Apple requires the ATT prompt before any cross-app tracking on iOS 14.5+. Reflect supports two modes:

// 1) Auto-prompt on first launch
new ReflectConfig {
    AutoRequestIosTracking = true,
}

// 2) Manual — call when you have a contextual moment in your UX
ReflectSDK.RequestIosTracking(status => {
    Debug.Log("ATT result: " + status);
    // status is one of: NotDetermined, Restricted, Denied, Authorized, Unavailable
});

The build post-processor injects NSUserTrackingUsageDescription into Info.plist automatically. Edit the wording in Editor/ReflectBuildPostProcessor.cs.

Android — AD_ID permission & Play Console Data Safety

The com.google.android.gms.permission.AD_ID permission (required on Android 13+ to read the GAID) is declared in the SDK’s Plugins/Android/AndroidManifest.xml and merges into your app automatically — you don’t add it. For COPPA / kids apps, remove that line and set RequireAdvertisingConsent = true.

You must still complete two Google Play Console declarations:

  • App content → Advertising ID: declare that the app uses an advertising ID (purpose: analytics / attribution). Declaring “no” while the AD_ID permission is present triggers a Play review warning.
  • Data safety: declare collection of Device or other IDs — GAID, Android ID, App Set ID, and (if enabled) Fire ID / IMEI / OAID, plus the push token; App activity (events, sessions); and Purchase history if you call TrackPurchase. Declare the collection purpose as analytics / attribution and whether data is shared with third parties (mirror your SetThirdPartySharing setting).

GDPR / CCPA right-to-be-forgotten

Reflect supports three operations per install_uuid: delete (irreversible wipe), opt-out (drop future events, keep history), and export (downloadable JSON snapshot). Tenant operators submit them from the admin console at Settings → Privacy, or programmatically via the worker:

# Tenant operator triggers from their backend on user request:
POST https://api.reflect.cloud/admin/gdpr/delete
Cookie: reflect_session=<operator session>
{ "install_uuid": "8c1f2a4d-..." }

What "delete" actually does:

  1. Cascade DELETE across every install-keyed D1 table: events, attributions, attribution_credits, clicks (joined via attribution), postbacks_log, webhook_deliveries (joined via event_id), ad_revenue_events, fraud_anomaly_scores, reengagement_attributions, touch_log, consent_records.
  2. Sweep all R2 audit blobs tagged with this install_uuid (via the per-install index written at ingest time).
  3. Insert the install into data_opt_outs so future events drop at the queue boundary — no D1 write, no attribution, no postback. KV-cached, sub-ms hot-path check.
  4. Log a row in gdpr_requests with rows_affected + the operator email + timestamps. Idempotent: re-submitting a completed delete is a no-op.

SDK-side wipe

The SDK exposes ReflectSDK.DeleteUserData(callback) which:

  1. Locally: wipes PlayerPrefs / AsyncStorage / shared_preferences (install_uuid + first-launch flag), drops the offline event queue, clears global properties + user_id.
  2. Server-side: POSTs /admin/gdpr/delete with the install_uuid. Returns synchronously with the deletion summary.

The local wipe completes synchronously — even if the server call fails, the user's device is clean. The server-side cascade typically finishes in <500ms because the install is already indexed.

What's NOT deleted by the cascade (legitimate-interest retention):

  • audit_log entries about the deletion itself (kept for legal record of the operation).
  • Daily DB backup blobs in R2 (whole-DB disaster-recovery snapshots — separate retention policy).
  • Aggregated analytics (cohort retention rollups, hourly counters) — derived from deleted source rows but not directly identifying after aggregation.

Privacy Manifest (iOS) — required by App Store

Apple requires every SDK on the App Store to ship a PrivacyInfo.xcprivacy declaring the data it collects and the required-reason-codes for any restricted API it uses. All three Reflect SDKs (Unity, React Native, Flutter) ship one; you don't need to author it manually.

Where it lives in each SDK

SDKManifest pathPackaging
UnityPlugins/iOS/PrivacyInfo.xcprivacyBuild post-processor copies it into the Xcode project automatically
React Nativeios/PrivacyInfo.xcprivacyresource_bundles entry in reflect-react-native.podspec — picked up by pod install
Flutterios/Classes/PrivacyInfo.xcprivacyresource_bundles entry in reflect_sdk.podspec — picked up by Flutter's iOS build

What it declares

The manifest tells Apple that the SDK collects four kinds of data, two of which are linked to user identity:

Data typeLinkedTrackingPurposes
DeviceID (IDFA / IDFV / install_uuid)yesyesAnalytics, Third-Party Advertising
ProductInteraction (events)yesnoAnalytics
PurchaseHistory (revenue, subscriptions)yesnoAnalytics
CoarseLocation (server-inferred from IP)yesnoAnalytics

Plus two required-reason-code declarations for restricted Apple APIs the SDK uses internally:

API categoryReasonWhy we use it
UserDefaultsCA92.1Persist install_uuid + event-queue cursor
FileTimestampC617.1Read / write timestamps on the on-disk event buffer

What YOU still need to do

  • Add an app-level PrivacyInfo.xcprivacy in your iOS project root declaring categories you collect outside of Reflect (email, name, photos, etc.). Apple validates the app manifest in addition to every SDK's manifest.
  • Add Reflect's attribution endpoint hostname to NSPrivacyTrackingDomains in the SDK's manifest. The shipped manifest has an empty array because the hostname is tenant-configurable:
    <key>NSPrivacyTrackingDomains</key>
    <array>
        <string>api.reflect.cloud</string>      <!-- or your custom domain -->
    </array>
    The build post-processor in Unity does this automatically when you configure BaseUrl; for React Native and Flutter you edit the manifest file directly after pod install the first time, or via a post-install hook.
  • If your app does NOT do server-side IP geo-lookup, you can remove the CoarseLocation block from the SDK manifest before shipping.
App Store enforcement is live since 1 May 2024
Submissions without a complete privacy manifest are rejected. The SDK ships one; failing to declare your app-level manifest is the most common rejection cause.

Anonymized mode

Not implemented as a single config flag (it’d be a leaky abstraction). Compose:

  • RequireAdvertisingConsent = true + don’t call SetAdvertisingConsent(true) — drops GAID/IDFA.
  • Don’t call SetUserId — keeps user anonymous.
  • Don’t pass PII as event properties (email, name) — server-side validation truncates strings to 1024 chars but doesn’t scrub semantic PII.
Tracking domains
Reflect doesn’t track users across apps. The SDK only sends events from the app it’s embedded in to the server you configured (BaseUrl). No fingerprinting beyond what’s needed for install attribution.