Build privacy-compliant checkouts with visitor consent
In many jurisdictions, regulations require that you obtain explicit consent from visitors before collecting certain types of data or performing specific tracking activities. Checkout Kit enables you to pass visitor consent information from your mobile app to the Shopify checkout, ensuring privacy compliance throughout the purchase flow.
This guide shows you how to collect visitor consent in your mobile app, pass it through the Storefront API using the @inContext
directive, and present a privacy-compliant checkout experience. This is essential for compliance with privacy regulations like GDPR, CCPA, and other data protection laws.
Anchor to How it worksHow it works
Checkout Kit supports visitor consent collection through the Storefront API's @inContext
directive. When you create a cart, you can include a visitorConsent
parameter that specifies what types of data collection and processing the visitor has consented to. This consent information is automatically encoded into the checkout URL and applied throughout the checkout process to ensure compliance.
Anchor to RequirementsRequirements
- You have an app using Checkout Kit
- Your app has Storefront API access
- You're using Storefront API version 2025-10 or later
Anchor to Step 1: Understand consent typesStep 1: Understand consent types
Checkout Kit supports four types of visitor consent that align with common privacy regulations:
- Analytics: Consent for collecting data to understand customer behavior and improve services
- Preferences: Consent for storing visitor preferences to enhance their shopping experience
- Marketing: Consent for marketing communications and promotional activities
- Sale of data: Consent for selling personal data to third parties
Each consent type is optional and can be granted or denied independently, giving visitors granular control over their privacy preferences. The encoded consent information will automatically take into account which consent types are provided and their values.
Anchor to Step 2: Collect visitor consentStep 2: Collect visitor consent
Mobile platforms provide built-in consent mechanisms that users recognize and trust. Map the platform responses to Shopify's consent categories based on your specific data practices.
Consent mapping is your responsibility: Platform consent frameworks have specific scopes that may not directly correspond to Shopify's four consent categories. You must determine the appropriate mapping based on your app's data collection practices, applicable regulations, and legal counsel guidance.
Anchor to Use platform-native consent collectionUse platform-native consent collection
Platform-native consent collection
import AppTrackingTransparency
import AdSupport
class ConsentManager {
func requestConsent() async -> VisitorConsent {
// Request iOS App Tracking Transparency permission
let status = await ATTrackingManager.requestTrackingAuthorization()
return mapATTToVisitorConsent(status: status)
}
private func mapATTToVisitorConsent(status: ATTrackingManager.AuthorizationStatus) -> VisitorConsent {
// Map ATT response to your specific consent requirements
// ATT covers cross-app tracking - you decide how this maps to your data practices
switch status {
case .authorized:
// User allowed cross-app tracking
return VisitorConsent(
analytics: true, // Consider: Does this cover your analytics use?
preferences: true, // Consider: Are preferences affected by ATT?
marketing: true, // Consider: Does this cover your marketing practices?
saleOfData: true // Consider: Does this cover your data sharing?
)
case .denied, .restricted:
// User denied cross-app tracking
return VisitorConsent(
analytics: true, // Consider: Can you still do first-party analytics?
preferences: true, // Consider: Are preferences still allowed?
marketing: false, // Consider: What marketing is still allowed?
saleOfData: false // Consider: What data sharing is affected?
)
case .notDetermined:
// Handle undetermined state based on your requirements
return getDefaultConsent()
@unknown default:
return getDefaultConsent()
}
}
private func getDefaultConsent() -> VisitorConsent {
// Define your default consent state
return VisitorConsent(
analytics: false,
preferences: true, // Usually considered essential
marketing: false,
saleOfData: false
)
}
}
struct VisitorConsent {
let analytics: Bool
let preferences: Bool
let marketing: Bool
let saleOfData: Bool
static let defaultConsent = VisitorConsent(
analytics: false,
preferences: true,
marketing: false,
saleOfData: false
)
}
// Usage example
let consentManager = ConsentManager()
func collectConsentAndCreateCart() async {
let consent = await consentManager.requestConsent()
createCartWithConsent(consent: consent)
}
import com.google.android.ump.*
class ConsentManager(private val context: Context) {
fun requestConsent(completion: (VisitorConsent) -> Unit) {
val consentInformation = UserMessagingPlatform.getConsentInformation(context)
consentInformation.requestConsentInfoUpdate(
ConsentRequestParameters.Builder().build(),
{
// Load consent form if needed
UserMessagingPlatform.loadConsentForm(context) { form, error ->
if (form != null) {
form.show(context as Activity) {
val consent = mapUMPToVisitorConsent(consentInformation)
completion(consent)
}
} else {
// Handle form load error
completion(getDefaultConsent())
}
}
},
{ error ->
// Handle consent update error
completion(getDefaultConsent())
}
)
}
private fun mapUMPToVisitorConsent(consentInfo: ConsentInformation): VisitorConsent {
// Map UMP response to your specific consent requirements
// UMP scope depends on what YOU configured in your AdMob setup
return when (consentInfo.consentStatus) {
ConsentInformation.ConsentStatus.OBTAINED -> {
// User provided consent based on YOUR UMP configuration
VisitorConsent(
analytics = true, // Consider: What did you ask consent for?
preferences = true, // Consider: Did UMP cover preferences?
marketing = true, // Consider: What marketing was included?
saleOfData = true // Consider: Did you ask about data sales?
)
}
ConsentInformation.ConsentStatus.NOT_REQUIRED -> {
// Consent not required for user's location
VisitorConsent(
analytics = true,
preferences = true,
marketing = true,
saleOfData = true
)
}
else -> {
// Required but not obtained, or unknown status
getDefaultConsent()
}
}
}
private fun getDefaultConsent(): VisitorConsent {
return VisitorConsent(
analytics = false,
preferences = true, // Usually considered essential
marketing = false,
saleOfData = false
)
}
}
data class VisitorConsent(
val analytics: Boolean,
val preferences: Boolean,
val marketing: Boolean,
val saleOfData: Boolean
)
import { ATTrackingManager } from 'react-native-tracking-transparency';
class ConsentManager {
async requestConsent() {
try {
// Request iOS ATT permission
const trackingStatus = await ATTrackingManager.requestTrackingAuthorization();
return this.mapPlatformToVisitorConsent(trackingStatus);
} catch (error) {
console.error('Consent request failed:', error);
return this.getDefaultConsent();
}
}
mapPlatformToVisitorConsent(platformStatus) {
// Map platform response to your specific consent requirements
// Consider what each platform permission actually covers for YOUR app
switch (platformStatus) {
case 'authorized':
// User allowed tracking
return {
analytics: true, // Consider: Your analytics practices
preferences: true, // Consider: Preference handling
marketing: true, // Consider: Your marketing scope
saleOfData: true // Consider: Your data sharing
};
case 'denied':
case 'restricted':
// User denied or restricted tracking
return {
analytics: true, // Consider: First-party analytics
preferences: true, // Consider: Essential preferences
marketing: false, // Consider: Limited marketing
saleOfData: false // Consider: No data sharing
};
case 'notDetermined':
default:
return this.getDefaultConsent();
}
}
getDefaultConsent() {
// Define your app's default consent state
return {
analytics: false,
preferences: true, // Usually essential for app function
marketing: false,
saleOfData: false
};
}
}
// Usage example
const consentManager = new ConsentManager();
const collectConsentAndCreateCart = async () => {
const consent = await consentManager.requestConsent();
createCartWithConsent(consent);
};
Anchor to Understanding platform consent scopeUnderstanding platform consent scope
Platform | Covers | Additional notes | Documentation |
---|---|---|---|
iOS App Tracking Transparency (ATT) | Cross-app tracking for advertising, data broker sharing, third-party advertising networks | Does not cover first-party analytics within your app, essential app preferences, non-tracking marketing | Apple's App Tracking Transparency guide |
Android User Messaging Platform (UMP) | Whatever you configured in your Google AdMob Privacy and messaging setup | Scope varies based on your message configuration, selected partners, and consent purposes | Google's UMP documentation |
When you map platform specific consent scope to Shopify's Visitor consent types, consider the following:
- What specific data practices each platform permission covers
- Your app's actual data collection and usage
- Applicable privacy regulations in your markets
- Your privacy policy commitments
- Legal counsel guidance for your specific situation
Anchor to Advanced: Custom consent collectionAdvanced: Custom consent collection
Only implement custom consent UIs if platform-native solutions don't meet your specific requirements, for example when:
- Your app needs more granular consent options than platform frameworks provide.
- You're targeting enterprise/B2B scenarios with specific compliance requirements.
- Platform-native solutions don't cover your specific use case or jurisdiction.
- You need consent collection that happens outside of platform advertising frameworks.
Custom consent collection (fallback approach)
struct CustomConsentView: View {
@State private var analyticsConsent = false
@State private var preferencesConsent = true // Often essential
@State private var marketingConsent = false
@State private var saleOfDataConsent = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Privacy Preferences")
.font(.headline)
Toggle("Analytics - Help us improve our services", isOn: $analyticsConsent)
Toggle("Preferences - Remember your shopping preferences", isOn: $preferencesConsent)
Toggle("Marketing - Receive promotional offers", isOn: $marketingConsent)
Toggle("Sale of data - Allow sharing data with partners", isOn: $saleOfDataConsent)
Button("Continue to Checkout") {
let consent = VisitorConsent(
analytics: analyticsConsent,
preferences: preferencesConsent,
marketing: marketingConsent,
saleOfData: saleOfDataConsent
)
createCartWithConsent(consent: consent)
}
}
.padding()
}
}
class CustomConsentFragment : Fragment() {
private var analyticsConsent = false
private var preferencesConsent = true // Often essential
private var marketingConsent = false
private var saleOfDataConsent = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = inflater.inflate(R.layout.fragment_custom_consent, container, false)
view.findViewById<CheckBox>(R.id.analytics_checkbox).apply {
setOnCheckedChangeListener { _, isChecked -> analyticsConsent = isChecked }
}
view.findViewById<CheckBox>(R.id.preferences_checkbox).apply {
isChecked = true // Often essential
setOnCheckedChangeListener { _, isChecked -> preferencesConsent = isChecked }
}
view.findViewById<CheckBox>(R.id.marketing_checkbox).apply {
setOnCheckedChangeListener { _, isChecked -> marketingConsent = isChecked }
}
view.findViewById<CheckBox>(R.id.sale_of_data_checkbox).apply {
setOnCheckedChangeListener { _, isChecked -> saleOfDataConsent = isChecked }
}
view.findViewById<Button>(R.id.continue_button).setOnClickListener {
val consent = VisitorConsent(analyticsConsent, preferencesConsent, marketingConsent, saleOfDataConsent)
createCartWithConsent(consent)
}
return view
}
}
import React, { useState } from 'react';
import { View, Text, Switch, Button } from 'react-native';
function CustomConsentCollection({ onConsentCollected }) {
const [analyticsConsent, setAnalyticsConsent] = useState(false);
const [preferencesConsent, setPreferencesConsent] = useState(true); // Often essential
const [marketingConsent, setMarketingConsent] = useState(false);
const [saleOfDataConsent, setSaleOfDataConsent] = useState(false);
const handleContinue = () => {
onConsentCollected({
analytics: analyticsConsent,
preferences: preferencesConsent,
marketing: marketingConsent,
saleOfData: saleOfDataConsent
});
};
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Privacy Preferences</Text>
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 16 }}>
<Switch value={analyticsConsent} onValueChange={setAnalyticsConsent} />
<Text>Analytics - Help us improve our services</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Switch value={preferencesConsent} onValueChange={setPreferencesConsent} />
<Text>Preferences - Remember your shopping preferences</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Switch value={marketingConsent} onValueChange={setMarketingConsent} />
<Text>Marketing - Receive promotional offers</Text>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', marginTop: 12 }}>
<Switch value={saleOfDataConsent} onValueChange={setSaleOfDataConsent} />
<Text>Sale of data - Allow sharing data with partners</Text>
</View>
<Button title="Continue to Checkout" onPress={handleContinue} />
</View>
);
}
Anchor to Step 3: Pass consent when interacting with the cartStep 3: Pass consent when interacting with the cart
Use the @inContext
directive with the visitorConsent
parameter to pass the collected consent information when querying the cart's checkoutUrl
. The consent information is automatically encoded and included in the checkout URL.
All fields are optional. The checkoutUrl
field automatically includes the _cs
parameter containing the encoded consent information.
Anchor to Example: Create cart with consent using GraphQLExample: Create cart with consent using Graph QL
GraphQL mutation with consent
Variables
Example Response
Anchor to Step 4: Present the checkoutStep 4: Present the checkout
Once you have a cart with the encoded consent information, present the checkout using Checkout Kit. The consent preferences are automatically applied throughout the checkout experience.
Always use the complete checkoutUrl
returned from the Storefront API, including the _cs
parameter. Do not modify or strip this parameter as it contains the encoded consent information required for compliance.
Present the checkout
import ShopifyCheckoutSheetKit
func presentCheckout(with checkoutUrl: URL) {
ShopifyCheckoutSheetKit.present(
checkout: checkoutUrl,
from: self,
delegate: self
)
}
import com.shopify.checkoutsheetkit.ShopifyCheckoutSheetKit
fun presentCheckout(checkoutUrl: String) {
ShopifyCheckoutSheetKit.present(checkoutUrl, this, checkoutEventProcessor)
}
import {useShopifyCheckoutSheet} from '@shopify/checkout-sheet-kit';
function CheckoutButton({ checkoutUrl }) {
const shopifyCheckout = useShopifyCheckoutSheet();
const handlePress = () => {
shopifyCheckout.present(checkoutUrl);
};
return (
<Button title="Complete Checkout" onPress={handlePress} />
);
}
If visitor consent changes: You must query the Storefront API again to get a fresh checkoutUrl
with updated consent encoding before presenting checkout. The encoding is ephemeral and doesn't persist beyond each GraphQL operation.
Anchor to Visitor consent with preloaded checkoutsVisitor consent with preloaded checkouts
When using Checkout Kit's preloading feature with visitor consent, be aware that preloaded checkouts reflect the consent state when preload()
was called. If visitor consent changes after preloading, the preloaded checkout will contain outdated consent encoding and may not be compliant.
To resolve this, choose one of these strategies based on your app's consent collection flow:
- Collect consent before preloading: Collect all consent preferences early in the user journey (e.g., app launch, cart page), then preload with complete consent information.
- Invalidate when consent changes: If consent changes after preloading, invalidate the preloaded cache using
ShopifyCheckoutSheetKit.invalidate()
.
Anchor to Best Practices to ensure full Privacy compliance.Best Practices to ensure full Privacy compliance.
Being privacy compliance is an important legal requirement for your app. To ensure you are building your app fully compliant, follow these best practices:
- Respect user privacy: Don't share consent data across different merchants or apps - each buyer's consent needs to be isolated.
- Provide consent management: For platform-native approaches, direct users to the appropriate settings (device Settings for ATT, re-trigger UMP flows). For custom consent, provide in-app controls.
- Cache consent mappings appropriately: Typically visitor consent doesn't change frequently. Most users will define their consent choices once, and leave it untouched afterwards, hence storing consent mapping decisions locally is important to avoid repeatedly prompting users.
- Monitor platform consent changes: Users might still decide to update their consent. Check for platform consent status changes (like iOS Settings updates or UMP status changes) and update your locally stored consent mappings accordingly.