# Apple
Source: https://docs.appstack.tech/Integrations/apple-ads
With the Apple Ads integration, you can import all your ad campaigns and metrics via the Apple Ads API.
**To successfully connect Apple Ads, you must:**
1. Have an Apple Ads API account with one of these roles: API Account Manager, API Account Read Only, Limited Access API Read & Write, or API Read Only
2. Ensure your Apple Ads account has access to the app used within Appstack
## Connect to Apple Ads
In your Apple Ads account, follow these steps:
1. Go to **Account Settings** > **User Management**.
2. Click **Invite Users**.
3. Fill in account information.
4. Assign the **API Account Manager** role.
5. Send invite.
Using the user with API Account Manager access, copy the dedicated Appstack Public Key you will :
1. In Appstack, from the side menu, select **Integrations** > **Apple Ads** > **Setup guide.**
2. Look for the designated public key (in step 2):
```text theme={null}
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcopQgAEgVn6XPywgAgZ6veyGwJ7/Gkj6oRB
oVH/8tOTT6u6tmQ8ca+5gvFe1p+DuRxuf4uULTmchIDRyxGIDKnSlEpE5A==
-----END PUBLIC KEY-----
```
3. Go to **Account Settings** > **API.**
4. Paste the **Public Key** you copied.
5. Look for the **Client ID**, **Team ID**, and **Key ID**.
Paste all the requested credentials to start the connection process.
1. Paste the **Client ID**, **Team ID**, and **Key ID**.
2. To find the **Apple Ads Campaign Group ID,** click your app name at the top left; a drop-down menu will appear, showing the name and ID of your campaign group.
3. Copy and paste the Apple Ads Campaign Group ID.
4. Click on **Connect**.
## Troubleshooting
### **General tips**
1. Make sure you have the right access (API Account Manager). Having Admin access won't work
2. Ensure your Apple Ads account has access to the app used within Appstack
3. When saving API credentials in Apple Ads, you may need to use the Safari browser if you encounter any 'invalid key' errors
4. Contact support: If issues persist, reach out to our support team with specific error messages at [support@appstack.tech](mailto:support@appstack.tech).
### Error messages
| Error | How to fix |
| :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Authentication failed | 1. Verify your Client ID, Team ID, Key ID, and Account ID are correct.
2. Ensure your API key is active and not expired.
3. Check that your account has the required API role: \*\*API Account Manager.
4. \*\*Ensure your Apple Ads account has access to the app used within Appstack. |
| No campaign data found | 1. Verify the app ID is correct in Apple Ads.
2. Ensure the app has campaign data over the last 30 days.
3. Check that campaigns are active and running.
4. Verify your Apple Ads account has access to this app. |
# Google
Source: https://docs.appstack.tech/Integrations/google-ads
With the Google Ads integration, you can:
1. Run enhanced app campaigns.
2. Import all your ad campaigns (app and web).
3. Enable Appstack to audit Google's reporting capabilities.
4. Allow Appstack to send in-app events, postbacks (signals) back to Google.
**To successfully connect Google Ads, you must:**
1. Have 'Administrator' access over the right Google Ads account. 'Standard' access is not enough to make the integration work
2. Ensure that the app used within Appstack is connected to the correct ad account
| Metrics | Definition |
| :------------------------ | :------------------------------------------------------------------------ |
| Ad spend | Money paid to run ads |
| Installs | New or old users who installed the app |
| Cost per install (CPI) | Cost per install from your ad campaigns. Formula: Ad spend / Installs |
| Installs per mille (IPM) | Installs per 1,000 impressions. Formula: (Installs / Impressions) x 1,000 |
| Impressions | Number of times an ad was shown |
| Cost per mille (CPM) | Cost per 1,000 impressions. Formula: (Ad spend / Impressions) x 1,000 |
| Clicks | Number of times an ad was clicked |
| Cost per click (CPC) | Cost per click. Formula Ad spend / Clicks |
| Click-through rate (CTR) | How often an impression becomes a click. Formula: Clicks / Impressions |
| Click to install rate | Share of clicks that became installs. Formula: Installs / Clicks |
| Return on ad spend (ROAS) | Return on ad spend. Formula: Ads revenue / Ad spend |
| Revenue | Total money earned from attributed users. |
## Connect to Google Ads
The first step is to connect your Google Ads account with Appstack. Follow these steps:
1. In Appstack, from the side menu, select **Integrations** > **Google Ads.**
2. Click on **Connect to Google Ads.**
3. In Google's Ads integration flow, select the correct Google account that has access to the correct ad accounts/apps.
4. Click on **Allow.**
## Configure the Google Ads API
Configuring the Google Ads API with Appstack lets you run enhanced app campaigns using the assigned ads link.
Appstack sends only attributed events to Google Ads. If you haven't launched a campaign yet, it's expected to see no activity at first, events will start flowing in once you have a campaign running and generating attributed installs.
1. Sign in to your Google Ads account.
2. Click your profile picture in the top right corner.
3. Your customer ID will be listed under **Account Information.**
4. (Optional) Paste your Manager Customer ID (MCC ID) for extra verification.
**Information**
1. For additional information on how to get the customer ID, follow the official instructions: [Get customer ID](https://support.google.com/google-ads/answer/1704344?hl=en)
Offline conversions allow Google Ads to measure in-app actions triggered by ads. Appstack maps SDK events to offline conversions and sends them back to Google Ads, enabling campaigns to be measured and optimized correctly.
It's mandatory to create one conversion action per application.
Follow these instructions to create the offline conversion action ID for your 'Install' event:
Follow these instructions to create the offline conversion actions ID for all the in-app events like 'Purchase', 'Start trial', etc.:
Follow these instructions to get the conversion action ID to paste in Appstack next to the correct SDK event:
If you see a misconfigured warning on your conversion goals, this will disappear automatically once you start a new campaign using those conversions.
1. Copy the ad link from the Appstack's integration page.
2. Inside Google Ads, click on **+** to start the campaign creation flow.
**Information**
1. Always ensure that you are filtering by mobile devices and the correct operating system (iOS or Android)
2. The Appstack ad link only needs to be pasted at the campaign level inside the **Tracking template** box within the **Campaign URL options** section. Don't paste the ad link at the ad set or ad level.
3. The final URL must always be your app store page URL, not the Appstack ad link.
4. When running search ads, remember to add the install CTA
## Pre-Launch EAC Checklist
SDK installed and app update released (if connecting a Subscription Platform, the update must include the SDK + snippet)
Events confirmed as flowing through the Appstack SDK page
Code snippet pasted following the docs instructions
Credentials copied and pasted
Update rolled out and Appstack ID coverage has reached 50% of events received
Google Ads connected
Google Ads API configured
Ad account and MMC ID added
Offline conversions created and mapped to SDK events
New campaign created with 'Create a campaign without guidance' selected
Campaign type selected ('Search' recommended — any type except App)
Conversion goal added for campaign optimization (e.g. appstack\_ios\_install)
Google Ads Campaign setup guide video followed for the selected campaign type
Appstack link pasted ONLY into the 'Tracking template' at the campaign level
Final URL set to the app store page URL
## Troubleshooting
### **General tips**
1. Check your permissions: Ensure you have the required access to connect.
2. Verify connections: Ensure your customer ID is correct and add your MCC ID if required.
3. Update tokens: Generate fresh API keys and access tokens by reconnecting.
4. Contact support: If issues persist, reach out to our support team with specific error messages at [support@appstack.tech](mailto:support@appstack.tech).
### Error messages
| Error | How to fix |
| :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Authentication** failed | 1. Go to your Google Ads Manager
2. Check that your user account has the correct permissions
3. Try to connect again in Appstack |
| Application not found | 1. Verify your customer ID is correct in
2. Ensure your user account has access to the correct ad account
3. Check that the app is connected to the right ad account |
# Meta
Source: https://docs.appstack.tech/Integrations/meta-ads
With the Meta Ads integration, you can:
1. Run enhanced app campaigns.
2. Import all your ad campaigns (app and web).
3. Enable Appstack to audit Meta's reporting capabilities.
4. Allow Appstack to send in-app events, postbacks (signals) back to Meta.
**To successfully connect Meta Ads, you must:**
1. Have Full/Admin control access to the right ad business portfolio (including Business Manager and Meta for Developers). Partial access is not enough to make the integration work.
2. Ensure the app used in Appstack (it must be created in Meta for Developers) is connected to the correct ad account.
| Metrics | Definition |
| :------------------------ | :------------------------------------------------------------------------ |
| Ad spend | Money paid to run ads |
| Installs | New or old users who installed the app |
| Cost per install (CPI) | Cost per install from your ad campaigns. Formula: Ad spend / Installs |
| Installs per mille (IPM) | Installs per 1,000 impressions. Formula: (Installs / Impressions) x 1,000 |
| Impressions | Number of times an ad was shown |
| Cost per mille (CPM) | Cost per 1,000 impressions. Formula: (Ad spend / Impressions) x 1,000 |
| Clicks | Number of times an ad was clicked |
| Cost per click (CPC) | Cost per click. Formula Ad spend / Clicks |
| Click-through rate (CTR) | How often an impression becomes a click. Formula: Clicks / Impressions |
| Click to install rate | Share of clicks that became installs. Formula: Installs / Clicks |
| Return on ad spend (ROAS) | Return on ad spend. Formula: Ads revenue / Ad spend |
| Revenue | Total money earned from attributed users. |
| In-app events | Definition |
| :-------------------- | :------------------------------------------------------------------ |
| Achieve level | User reaches a level/milestone |
| Add payment info | User enters or saves payment details |
| Add to cart | User adds an item to the shopping cart |
| Complete registration | User finishes sign-up (account creation) |
| Initiate checkout | User starts the checkout flow |
| Session | App open, resulting in a user session |
| View content | User views a key screen/content item |
| Start trial | User begins a free trial for a subscription |
| Subscription | First paid subscription period begins (with or without prior trial) |
| Purchase | One-time in-app purchase (consumable or non-consumable) |
## Connect to Meta Ads
The first step is to connect your Meta Ads account with Appstack. Follow these steps:
1. In Appstack, from the side menu, select **Integrations** > **Meta Ads**
2. Click on **Connect to Meta Ads**
3. In Meta's access integration flow, select the correct business portfolio that contains the proper ad accounts and apps
4. Ensure that you click on **Select all** ad accounts
5. Accept the requested permissions and click on **Save**
6. Click on **Got it** and then you will get redirected to Appstack's integration page
## Configure the Conversion API
Configuring the Conversion API with Appstack will allow you to run enhanced app campaigns using the assigned ads link.
The dataset receives only Appstack-attributed events. Choosing the wrong dataset will cause your app's events to be sent to the wrong destination.
Follow these steps if you don't have an existing dataset:
1. In the 'Connect your data' pop-up, click on **Create a new dataset.**
2. On the next screen, leave the CAPI toggle on, and don't select any category. Click on **Create.**
3. Choose the ad accounts you want to connect to this dataset and click on **Next.**
4. Close the pop-up.
**Information**
For additional information on how to get the Conversion API access token, follow the official instructions: [Get access token](https://developers.facebook.com/docs/marketing-api/conversions-api/get-started/#access-token)
1. Once the connection shows as **Active**, select the correct dataset ID from the dropdown in Appstack.
2. To generate the Conversion API access token, follow these steps:
* Go to your Meta Events Manager.
* Select the correct dataset from the drop-down.
* Click on **Settings** in the right sidebar.
* Scroll down to the **Conversions API** section.
* Click **Generate Access Token.**
* When prompted to select the dataset(s) the token will have access to, make sure to select **at least the same dataset** you're using in Appstack (you can select additional ones too, but the Appstack dataset must be included).
* **Copy** the generated token.
* **Paste** the access token in the Conversion API access token field.
3. Click on **Save** to confirm the dataset ID and access token, enabling the integration.
To successfully send in-app events to Meta Ads, select the in-app events from the dropdown button under the 'Appstack SDK events' column, and map each one to the corresponding Meta standard event.
Appstack will send the mapped standard event to Meta Ads (to the selected dataset) via the Conversion API, and this event will be used as the optimization goal when setting up the ad campaign.
**Information**
1. To complete this step successfully, you first need to receive an install event from the Appstack SDK.
2. Once the in-app events are selected, they will take between 30 and 90 minutes to appear in your dataset in the Meta Events Manager.
3. Appstack sends only the attributed events to the Meta Events Manager. If you haven't launched a campaign yet, or just reconnected the integration, it's expected to see little or no activity at first, events will start flowing in once you have a campaign running and generating attributed installs.
4. Events sent by Appstack are mapped to Meta's standard events, so they'll be recognized automatically in Meta Events Manager alongside any standard events already flowing into your dataset from other sources.
Follow these instructions to verify the Appstack domain in Meta Events Manager:
1. Inside the proper dataset, go to **Settings** and scroll down to the **Traffic Permissions - Websites** section.
2. Click on **Create an allow list.**
3. Look for the domain [appstack.tech](http://appstack.tech) and click **Add to allow list.**
4. Click on **Confirm.**
You must wait for the in-app events to arrive before doing this step.
1. **Copy the ad link** from the Appstack's integration page (located at the bottom of the page).
2. Inside Meta Ads Manager, click on **+ Create** to start the campaign creation flow.
3. Select **Sales** (recommended) as the campaign objective.
4. At the ad set level, under the 'Conversion' section, select **Website** only as the 'Conversion location', select the **correct dataset** (same as in Appstack), and for the 'Conversion event' select the event you want the campaign to optimize for.
5. At the ad set level, under the 'Placements' section, click on 'Show more settings' and select only **Mobile** and the **correct operating system** depending on the advertised app. Also, you must disable 'Audience Network' as a platform.
6. At the ad level, **paste the ad link** into the 'Website URL' box. Don't paste the URL parameters in the 'Tracking' section; copy them from the first step.
**Information**
1. App installs (appstack\_install) are available as a metric in Ads Manager only if they were selected as an optimization goal. You can still see them inside the Appstack dashboard.
2. To unlock the appstack\_installs metric, you need to launch a campaign that optimizes for this event. Once you do it, you will be able to unlock this metric for all future campaigns.
3. Remember to always select mobile-only and the correct operating system in the 'Placements' section at the ad set level.
4. If you optimize for value (ROAS) and your events include trials, Meta Events Manager will flag a *"Send higher quality price data for more accurate ROAS calculation"* notice saying the **value field is missing** on some events. This is expected. Trials have no purchase value, so they're sent without one, while value optimization expects a value greater than 0, and none of this breaks attribution or your campaign.
5. The same "value field missing" notice can also show up tied to install events specifically. This is expected too, installs don't carry a purchase value by definition.
6. If you're only sending attributed events and have low volume or a single subscription price point, Meta may flag your event values as looking artificially uniform or averaged, even though every event carries its real, per-transaction value. This is a known false positive and doesn't indicate a configuration issue.
## Pre-Launch EAC Checklist
| Error | How to fix |
| :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Authentication failed | 1. Go to your Meta Business Manager
2. Check that your user account has the correct permissions
3. Try to connect again in Appstack |
| Application not found | 1. Verify your app ID is correct in Meta Business Manager
2. Ensure your user account has access to the correct ad account
3. Check that the app is connected to the right ad account |
# Reddit
Source: https://docs.appstack.tech/Integrations/reddit
Integration is currently in progress. Soon, it will be ready for general availability.
# RevenueCat
Source: https://docs.appstack.tech/Integrations/revenuecat
You are an expert mobile monetization engineer helping me integrate RevenueCat with the Appstack SDK. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to wire the integration for my platform (Swift, Kotlin, React Native, or Flutter). When I paste this prompt and share my code, you should:
1. Show exactly where to call `setAppstackAttributionParams()` in my app lifecycle and produce ready-to-paste code for my platform, using the snippets below.
2. Ensure both the Appstack attribution params and the Appstack ID are assembled into the `params` map and forwarded to RevenueCat exactly as described.
3. Confirm that `setAppstackAttributionParams()` is called after `Purchases.configure` and before the first paywall load, and that the returned offerings are used immediately.
4. Propose concrete edits that minimize duplication and keep the integration in one clear place in my code.
5. Give me a short checklist of what you configured (IDs, attributes, lifecycle placement, ATT handling if iOS) so I can see that everything from the docs is covered.
***
## Reference: RevenueCat + Appstack integration
**RevenueCat SDK requirements**
Use a RevenueCat SDK version that includes `setAppstackAttributionParams()`:
| Platform | Package | Minimum version |
| :----------- | :----------------------- | :-------------- |
| iOS | `purchases-ios` | `5.61.0` |
| Android | `purchases-android` | `9.23.0` |
| React Native | `react-native-purchases` | `9.12.0` |
| Flutter | `purchases-flutter` | `9.14.0` |
**1. Call `setAppstackAttributionParams()` after `Purchases.configure`**
Build the `params` map from both `getAttributionParams()` and `getAppstackId()`, then pass it to RevenueCat. This single call sets `$appstackId`, campaign attribution attributes, click IDs, and device identifiers, and it syncs attributes while fetching fresh offerings before returning.
```swift theme={null}
// Swift
Purchases.configure(withAPIKey: "public_sdk_key")
Task {
let base = await AppstackAttributionSdk.shared.getAttributionParams() ?? [:]
var params = base
if let id = AppstackAttributionSdk.shared.getAppstackId() {
params["appstack_id"] = id
}
Purchases.shared.attribution.setAppstackAttributionParams(params) { offerings, error in
// Use `offerings` to present the correct paywall for this user
}
}
```
```kotlin theme={null}
// Kotlin
Purchases.configure(this, "public_sdk_key")
fun syncRevenueCatAttribution() {
val base = AppstackAttributionSdk.getAttributionParams()
val params = base.toMutableMap()
AppstackAttributionSdk.getAppstackId()?.let { params["appstack_id"] = it }
Purchases.sharedInstance.setAppstackAttributionParams(
params,
object : SyncAttributesAndOfferingsCallback {
override fun onSuccess(offerings: Offerings) {
// Use `offerings` to present the correct paywall for this user
}
override fun onError(error: PurchasesError) { /* handle error */ }
}
)
}
// Call syncRevenueCatAttribution() after Appstack initialization and before loading offerings or paywalls.
```
```typescript theme={null}
// React Native
Purchases.configure({ apiKey: "public_sdk_key" });
const base = (await AppstackSDK.getAttributionParams()) ?? {};
const params = { ...base };
const id = await AppstackSDK.getAppstackId();
if (id != null) {
params["appstack_id"] = id;
}
const offerings = await Purchases.setAppstackAttributionParams(params);
// Use `offerings` to present the correct paywall for this user
```
```dart theme={null}
// Flutter
await Purchases.configure(PurchasesConfiguration("public_sdk_key"));
final base = await AppstackPlugin.getAttributionParams() ?? {};
final params = Map.from(base);
final id = await AppstackPlugin.getAppstackId();
if (id != null) {
params['appstack_id'] = id;
}
final Offerings offerings = await Purchases.setAppstackAttributionParams(params);
// Use `offerings` to present the correct paywall for this user
```
**2. iOS App Tracking Transparency (iOS 14.5+)**
* If you request ATT permission to access the IDFA, call `setAppstackAttributionParams()` again after the customer grants permission, rebuilding `params` from the latest `getAttributionParams()` and `getAppstackId()` values. The `AdSupport` framework is required to collect the IDFA on iOS.
**3. Credentials in dashboards (non-code steps)**
* In Appstack: `Integrations` → `RevenueCat` → copy **webhook URL** and **authorization header**.
* In RevenueCat: `Integrations` → `Attribution` → `Appstack` → paste **webhook URL** and **authorization header**.
* Once active on RevenueCat's platform, it can take **30–60 minutes** to appear active in Appstack.
With the RevenueCat integration, you can:
1. Use RevenueCat in-app events to run enhanced app campaigns.
2. Unlock attribution-based paywall optimization.
**To successfully connect RevenueCat, you must:**
1. Have Owner/Admin access to an Appstack organization.
2. Have access to the correct RevenueCat account.
## Requirements
Use a RevenueCat SDK version that includes `setAppstackAttributionParams()`:
| Platform | Package | Minimum version |
| :----------- | :----------------------- | :-------------- |
| iOS | `purchases-ios` | `5.61.0` |
| Android | `purchases-android` | `9.23.0` |
| React Native | `react-native-purchases` | `9.12.0` |
| Flutter | `purchases-flutter` | `9.14.0` |
## Connect to RevenueCat
Follow the steps to ensure the RevenueCat integration works:
After configuring the Purchases SDK and before the first offerings or paywall load, call `setAppstackAttributionParams()` with the attribution data from the Appstack SDK. This single call sets the `$appstackId`, campaign attribution attributes (`$mediaSource`, `$campaign`, `$adGroup`, `$ad`, `$keyword`), click IDs, and device identifiers — no need to call `collectDeviceIdentifiers()` separately.
The call also syncs attributes to the RevenueCat backend and fetches fresh offerings before returning, so Appstack-based targeting is applied before your paywall loads.
On iOS, check `appstack_match_status` in the params before drawing conclusions from an empty-looking result. `failed` means the match has not resolved yet, so it is worth rebuilding `params` and calling again later. `not_configured` means the call ran before `configure(...)` finished — fix the ordering — or that the SDK is disabled, in which case retrying never helps.
Learn more about the RevenueCat x Appstack integration by [reading this article.](https://www.revenuecat.com/docs/integrations/attribution/appstack)
```swift Swift theme={null}
import AdSupport
// ...
Purchases.configure(withAPIKey: "public_sdk_key")
// ...
Task {
let base = await AppstackAttributionSdk.shared.getAttributionParams() ?? [:]
var params = base
if let id = AppstackAttributionSdk.shared.getAppstackId() {
params["appstack_id"] = id
}
// Forward to RevenueCat — syncs attributes and fetches fresh offerings
// so Appstack-based targeting is applied before the callback returns.
Purchases.shared.attribution.setAppstackAttributionParams(params) { offerings, error in
// Use `offerings` to present the correct paywall for this user
}
}
```
```kotlin Kotlin theme={null}
// ...
Purchases.configure(this, "public_sdk_key")
// ...
fun syncRevenueCatAttribution() {
val base = AppstackAttributionSdk.getAttributionParams()
val params = base.toMutableMap()
AppstackAttributionSdk.getAppstackId()?.let { params["appstack_id"] = it }
// Forward to RevenueCat — syncs attributes and fetches fresh offerings
// so Appstack-based targeting is applied before the callback returns.
Purchases.sharedInstance.setAppstackAttributionParams(
params,
object : SyncAttributesAndOfferingsCallback {
override fun onSuccess(offerings: Offerings) {
// Use `offerings` to present the correct paywall for this user
}
override fun onError(error: PurchasesError) { /* handle error */ }
}
)
}
// Call syncRevenueCatAttribution() after Appstack initialization and before loading offerings or paywalls.
```
```typescript React Native theme={null}
// ...
Purchases.configure({ apiKey: "public_sdk_key" });
// ...
const base = (await AppstackSDK.getAttributionParams()) ?? {};
const params = { ...base };
const id = await AppstackSDK.getAppstackId();
if (id != null) {
params["appstack_id"] = id;
}
// Forward to RevenueCat — syncs attributes and fetches fresh offerings
// so Appstack-based targeting is applied before the promise resolves.
const offerings = await Purchases.setAppstackAttributionParams(params);
// Use `offerings` to present the correct paywall for this user
```
```dart Flutter theme={null}
// ...
await Purchases.configure(PurchasesConfiguration("public_sdk_key"));
// ...
final base = await AppstackPlugin.getAttributionParams() ?? {};
final params = Map.from(base);
final id = await AppstackPlugin.getAppstackId();
if (id != null) {
params['appstack_id'] = id;
}
// Forward to RevenueCat — syncs attributes and fetches fresh offerings
// so Appstack-based targeting is applied before the await returns.
final Offerings offerings = await Purchases.setAppstackAttributionParams(params);
// Use `offerings` to present the correct paywall for this user
```
**RevenueCat SDK minimum versions**
`setAppstackAttributionParams()` is available in `purchases-ios` 5.61.0+, `purchases-android` 9.23.0+, `react-native-purchases` 9.12.0+, and `purchases-flutter` 9.14.0+. Earlier versions do not include the Appstack integration method.
**iOS App Tracking Transparency (iOS 14.5+)**
If you request App Tracking permission through ATT to access the IDFA, call `setAppstackAttributionParams()` again after the customer grants permission, rebuilding `params` from the latest `getAttributionParams()` and `getAppstackId()` values as in the code examples above. The `AdSupport` framework is required to collect the IDFA on iOS.
1. In Appstack, from the side menu, select **Integrations** > **RevenueCat.**
2. Copy the **webhook URL.**
3. Copy the **authorization header.**
1. In RevenueCat, from the dashboard, go to **Integrations** > **Attribution** > **Appstack.**
2. Paste the **webhook URL.**
3. Paste the **authorization header.**
After the integration is active on RevenueCat's platform, it can take 30-60 minutes to appear as active on Appstack's side.
The integration will show an error if less than 50% of events received over the last 24 hours include an Appstack ID. For RevenueCat, pass this under the `appstack_id` key when building `params`. When this threshold is not met, these events cannot be used in ad network integration pages.
## List of events
Below is a list of events you can forward to ad networks to optimize your campaigns.
| Name | Definition |
| :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rc_trial_started` | Fired when a user begins a free trial. Triggered on the initial purchase when the period type is `TRIAL`. |
| `rc_trial_qualified` | Fired when a free trial is still active two hours after it started — meaning the user did not cancel within the qualification window. |
| `rc_trial_converted` | Fired when a free trial successfully converts to a paid subscription. This happens on the first renewal after the trial period ends. |
| `rc_intro_started` | Fired when a user begins an intro offer — a paid trial at a discounted price (e.g., \$0.99 for the first week). Triggered on the initial purchase when the period type is `INTRO` |
| `rc_trial_intro_started` | Fired when either a free trial starts (`rc_trial_started`) or an intro offer starts (`rc_intro_started`). The event triggers as soon as at least one of these conditions is met. |
| `rc_trial_intro_qualified` | Fired when either a free trial qualifies (`rc_trial_qualified`) or an intro offer qualifies (`rc_intro_qualified`). The event triggers as soon as at least one of these conditions is met. |
| `rc_initial_purchase` | Fired when any of the following occur: a free trial starts (`rc_trial_started`), an intro offer starts (`rc_intro_started`), or a full-price subscription starts (`rc_subscription_started`). The event triggers as soon as at least one of these conditions is met. |
| `rc_initial_purchase_qualified` | Fired when any of the following occur: a trial qualified happens (`rc_trial_qualified`), an intro offer starts (`rc_intro_started`), or a full-price subscription starts (`rc_subscription_started`). The event triggers as soon as at least one of these conditions is met. |
| `rc_subscription_started` | Fired when a user starts a paid subscription at full price, with no trial or intro offer involved. Triggered on the initial purchase when the period type is `NORMAL` |
| `rc_subscription_renewed` | Fired on each successful renewal of an active subscription. Indicates the user was billed again and remains subscribed for another period. |
| `rc_in_app_purchase` | Fired when a user makes a one-time, non-subscription purchase — any product that is not an auto-renewing subscription (e.g., consumables or non-consumable in-app purchases). Unlike subscription events, this purchase does not renew automatically. |
## Troubleshooting
### **General tips**
1. Check SDK versions: confirm the RevenueCat SDK includes `setAppstackAttributionParams()` (`purchases-ios` 5.61.0+, `purchases-android` 9.23.0+, `react-native-purchases` 9.12.0+, `purchases-flutter` 9.14.0+). Earlier versions do not expose the method.
2. Check the ordering: call `setAppstackAttributionParams(...)` **after** `Purchases.configure` and **before** the first offerings or paywall load. Use the returned `offerings` to present the paywall rather than fetching them again.
3. Build the full `params` map: include **both** `getAttributionParams()` and `getAppstackId()` (stored under the `appstack_id` key). Passing only one of them, or a wrong key name, breaks attribution.
4. Handle ATT on iOS: if you request App Tracking permission, call `setAppstackAttributionParams(...)` again after the customer grants it, rebuilding `params` from fresh SDK values. The `AdSupport` framework must be linked to collect the IDFA.
5. Allow time for activation: after the integration is active in RevenueCat, it can take **30–60 minutes** to appear active in Appstack.
6. Contact support: if issues persist, reach out with specific error messages at [support@appstack.tech](mailto:support@appstack.tech).
### Common issues
| Issue | How to fix |
| :-------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "Less than 50% of events include an Appstack ID" error | 1. Confirm the app version that includes the Appstack SDK is already released on the store and that more than 24 hours have passed, so enough live installs have reported events.
2. Confirm `getAppstackId()` is added to `params` under the `appstack_id` key.
3. Confirm `setAppstackAttributionParams(...)` runs on every app start after `Purchases.configure`.
4. Allow up to 24 hours for coverage to recover once the fix is rolled out. Below this threshold, events cannot be used in ad network integration pages. |
| `setAppstackAttributionParams` not found / does not compile | 1. Update the RevenueCat SDK to the minimum version listed above.
2. Confirm the method is called on the correct object (`Purchases.shared.attribution` on iOS, `Purchases.sharedInstance` on Android). |
| CocoaPods reports a version conflict on a RevenueCat dependency | Something in your `Podfile` (or another pod) pins that dependency explicitly. The React Native and Flutter wrappers already pin the exact version they need, so remove your own pin and let the wrapper resolve it — do not try to force a different version. |
| Offerings or paywall not reflecting attribution | 1. Confirm the call runs **before** the first paywall load.
2. Use the `offerings` returned by the callback/await instead of a separate fetch.
3. Confirm the `params` map is populated (it is available after `configure`). |
| IDFA missing on iOS | 1. Link the `AdSupport` framework.
2. Request ATT permission, then call `setAppstackAttributionParams(...)` again with fresh values.
3. Test on a physical device — the IDFA is not available in the simulator. |
| Integration not showing as active in Appstack | 1. Re-check the **webhook URL** and **authorization header** pasted in RevenueCat match Appstack.
2. Wait 30–60 minutes after activation. |
You are an expert mobile monetization engineer reviewing my existing RevenueCat + Appstack integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Your goal is to **validate that my RevenueCat integration fully matches the documentation** and that event and attribution data can flow correctly between Appstack and RevenueCat. When I paste this prompt and share my platform-specific code and configuration, you should:
1. **SDK and initialization checks**
* Confirm that:
* The Appstack SDK is installed and initialized correctly for the platform (Swift, Kotlin, React Native, or Flutter).
* The RevenueCat SDK version supports Appstack: `purchases-ios` 5.61.0+, `purchases-android` 9.23.0+, `react-native-purchases` 9.12.0+, or `purchases-flutter` 9.14.0+.
* The RevenueCat `Purchases` SDK is configured via `Purchases.configure` before any attribution calls are made.
* Point out any missing dependencies, incorrect initialization order, or cases where `setAppstackAttributionParams()` is called before `Purchases.configure`.
2. **`setAppstackAttributionParams` integration**
* Locate calls to:
* `Purchases.shared.attribution.setAppstackAttributionParams(params)` (Swift)
* `Purchases.sharedInstance.setAppstackAttributionParams(params, ...)` (Kotlin)
* `Purchases.setAppstackAttributionParams(params)` (React Native)
* `Purchases.setAppstackAttributionParams(params)` (Flutter)
* Verify that:
* The call appears **after** `Purchases.configure` and **before** the first paywall load.
* The returned `offerings` object is used to present the paywall (not fetched again in a separate call).
* The call is not redundantly repeated on every screen — it should happen once at startup (and once more after ATT permission is granted on iOS).
* Propose concrete code changes if the call is missing, misplaced, or incorrectly parameterized.
3. **Attribution params and Appstack ID**
* Confirm that the `params` map is built from **both**:
* `getAttributionParams()` / `AppstackAttributionSdk.getAttributionParams()` / `AppstackSDK.getAttributionParams()` / `AppstackPlugin.getAttributionParams()`
* `getAppstackId()` stored under the key `"appstack_id"`
* Flag any integration that passes only one of these two sources, or uses a wrong key name.
* Remind me that the integration is disabled when less than 50% of events over the last 24 hours include an Appstack ID, and that this coverage only stabilizes once the app build with the Appstack SDK has been live on the store for more than 24 hours.
4. **iOS App Tracking Transparency handling**
* Check whether ATT permission is requested in the app.
* If it is, verify that `setAppstackAttributionParams()` is called a **second time** after the user grants permission, rebuilding `params` from fresh SDK values.
* Confirm the `AdSupport` framework is linked for IDFA collection.
5. **Validation report & checklist**
* Produce a clear report summarizing:
* What is correctly implemented and safe for production.
* What is missing, misconfigured, or risky, with platform-specific file names and snippets to change.
* End with a **RevenueCat-specific checklist** (SDK initialization order, `setAppstackAttributionParams` placement, params map completeness, offerings usage, ATT re-call on iOS, dashboard credentials) that I can use to confirm my integration fully matches the RevenueCat integration guide.
# Snapchat
Source: https://docs.appstack.tech/Integrations/snapchat
Integration is currently in progress. Soon, it will be ready for general availability.
# Using RevenueCat and Superwall together
Source: https://docs.appstack.tech/Integrations/subscription-platforms
Use RevenueCat as your subscription data source and Superwall for paywalls, with Appstack attribution wired into both.
Many teams run **both** subscription platforms at once: RevenueCat as the source of truth for subscription **data**, and Superwall to build and serve **paywalls**. This guide shows how the two fit together with Appstack so attribution flows correctly from install → paywall → subscription.
Subscription **data**: manages entitlements and subscription state, and forwards subscription events with the Appstack ID and attribution params to Appstack.
**Paywalls**: presents and experiments on paywalls, and can target them by the campaign a user came from using Appstack attribution params.
## Who does what
| Responsibility | RevenueCat | Superwall |
| :------------------------------------------------ | :--------: | :-----------------------: |
| Presents paywalls | — | ✅ |
| Manages entitlements / subscription state | ✅ | — |
| Source of subscription events sent to ad networks | ✅ | — |
| Receives the Appstack ID | ✅ | — |
| Receives Appstack attribution params | ✅ | ✅ (for paywall targeting) |
## How it works together
On app start, the Appstack SDK produces the Appstack ID and attribution params (campaign, click IDs, device identifiers). Both platforms read from these.
Call `setAppstackAttributionParams()` after `Purchases.configure` so RevenueCat attaches the Appstack ID and attribution to every subscriber. See [the RevenueCat guide](/Integrations/revenuecat) for the per-platform code. The call returns RevenueCat `offerings`, but in this setup Superwall presents the paywall — so you don't need to use those `offerings` to render one.
Pass the attribution params via `setUserAttributes(...)` so paywalls can be filtered by where the user came from. You can **skip** `setIntegrationAttributes(...)` (the Appstack ID) here — that only matters when Superwall is your subscription-event source, and RevenueCat is filling that role. See [the Superwall guide](/Integrations/superwall) for the per-platform code.
RevenueCat is the only platform forwarding subscription events to Appstack, so there's no double-counting to manage. Superwall contributes paywall targeting, and Appstack ties the attribution and subscription outcomes back to the acquisition source.
## Wiring both in code
The snippet below configures both platforms from one place, at startup, after the Appstack, RevenueCat, and Superwall SDKs are configured. The Appstack identity is read once, then handed to each platform differently:
* **RevenueCat** gets the attribution params **and** the Appstack ID (under `appstack_id`) — it's your subscription-event source.
* **Superwall** gets **only** the attribution params, as user attributes for paywall targeting. There's no `setIntegrationAttributes(...)` call, because Superwall isn't forwarding events here.
```swift Swift theme={null}
import AdSupport
// ...
Purchases.configure(withAPIKey: "public_sdk_key")
// ...configure Superwall per its docs...
Task {
// Read the Appstack identity once
let attribution = await AppstackAttributionSdk.shared.getAttributionParams() ?? [:]
// RevenueCat = subscription data source (include the Appstack ID)
var rcParams = attribution
if let id = AppstackAttributionSdk.shared.getAppstackId() {
rcParams["appstack_id"] = id
}
Purchases.shared.attribution.setAppstackAttributionParams(rcParams) { offerings, error in
// Attribution + subscription data now flow through RevenueCat.
// Superwall presents the paywall, so you can ignore these `offerings`.
}
// Superwall = paywalls only (attribution params for targeting; no Appstack ID)
Superwall.shared.setUserAttributes(attribution)
Superwall.shared.register(placement: "onboarding_paywall")
}
```
```kotlin Kotlin theme={null}
// ...
Purchases.configure(this, "public_sdk_key")
// ...configure Superwall per its docs...
fun wireAppstack() {
// Read the Appstack identity once
val attribution = AppstackAttributionSdk.getAttributionParams()
// RevenueCat = subscription data source (include the Appstack ID)
val rcParams = attribution.toMutableMap()
AppstackAttributionSdk.getAppstackId()?.let { rcParams["appstack_id"] = it }
Purchases.sharedInstance.setAppstackAttributionParams(
rcParams,
object : SyncAttributesAndOfferingsCallback {
override fun onSuccess(offerings: Offerings) {
// Superwall presents the paywall, so you can ignore these `offerings`.
}
override fun onError(error: PurchasesError) { /* handle error */ }
}
)
// Superwall = paywalls only (attribution params for targeting; no Appstack ID)
Superwall.instance.setUserAttributes(attribution)
Superwall.instance.register("onboarding_paywall")
}
// Call wireAppstack() after Appstack, RevenueCat, and Superwall are configured.
```
```typescript React Native / Expo theme={null}
// RevenueCat via react-native-purchases, Superwall via expo-superwall
import { useEffect } from "react";
import { useUser, usePlacement } from "expo-superwall";
// RevenueCat = subscription data source. Run once at startup.
async function configureRevenueCat() {
Purchases.configure({ apiKey: "public_sdk_key" });
const attribution = (await AppstackSDK.getAttributionParams()) ?? {};
const rcParams = { ...attribution };
const id = await AppstackSDK.getAppstackId();
if (id != null) {
rcParams["appstack_id"] = id;
}
await Purchases.setAppstackAttributionParams(rcParams);
// Superwall presents the paywall, so you can ignore the returned offerings.
}
// Superwall = paywalls only. Hooks stay at the component's top level.
function Paywall() {
const { update } = useUser();
const { registerPlacement } = usePlacement();
useEffect(() => {
(async () => {
const attribution = (await AppstackSDK.getAttributionParams()) ?? {};
await update(attribution); // attribution params for targeting; no Appstack ID
await registerPlacement({ placement: "onboarding_paywall" });
})();
}, [update, registerPlacement]);
// ...render your paywall UI
}
```
```dart Flutter theme={null}
// ...
await Purchases.configure(PurchasesConfiguration("public_sdk_key"));
// ...configure Superwall per its docs...
// Read the Appstack identity once
final attribution = await AppstackPlugin.getAttributionParams() ?? {};
// RevenueCat = subscription data source (include the Appstack ID)
final rcParams = Map.from(attribution);
final id = await AppstackPlugin.getAppstackId();
if (id != null) {
rcParams['appstack_id'] = id;
}
await Purchases.setAppstackAttributionParams(rcParams);
// Superwall presents the paywall, so you can ignore the returned offerings.
// Superwall = paywalls only (attribution params for targeting; no Appstack ID)
await Superwall.shared.setUserAttributes(attribution);
await Superwall.shared.register('onboarding_paywall');
```
## Set up order
Confirm events flow through the Appstack SDK page before connecting either platform.
Follow the [RevenueCat integration](/Integrations/revenuecat) to wire the SDK call and paste credentials.
Follow the [Superwall integration](/Integrations/superwall) to wire only `setUserAttributes`. Skip `setIntegrationAttributes(...)` and the dashboard credential steps.
Confirm the Appstack ID reaches ≥50% of RevenueCat's events so the integration stays active.
# Superwall
Source: https://docs.appstack.tech/Integrations/superwall
You are an expert mobile monetization engineer helping me integrate Superwall with the Appstack SDK. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to wire the integration for my platform (Swift, Kotlin, Expo, or Flutter). When I paste this prompt and share my code, you should:
1. Show exactly where to call the Appstack + Superwall integration methods in my app lifecycle and produce ready-to-paste code for my platform, using the snippets below.
2. Ensure the Appstack ID and attribution parameters are passed to Superwall exactly as described in the documentation.
3. Confirm that any required SDK versions and ordering (Appstack vs Superwall initialization) are respected.
4. Propose concrete edits that minimize duplication and keep the integration in one clear place in my code.
5. Give me a short checklist of what you configured (IDs, attributes, lifecycle placement) so I can see that everything from the docs is covered.
***
## Reference: Superwall + Appstack integration
**Superwall SDK requirements**
Use a Superwall SDK version that includes the Appstack integration attribute:
| Platform | Package | Minimum version |
| :------------------ | :------------------ | :-------------- |
| iOS | `Superwall-iOS` | `4.12.11` |
| Android | `Superwall-Android` | `2.7.5` |
| Flutter | `Superwall-Flutter` | `2.4.11` |
| React Native / Expo | `expo-superwall` | `1.0.5` |
For React Native apps, use `expo-superwall`. The legacy `react-native-superwall` package is archived.
**1. Pass the Appstack ID to Superwall**
Call these after both Appstack and Superwall are configured:
```swift theme={null}
// Swift
// Use Superwall-iOS version >= 4.12.11
Superwall.shared.setIntegrationAttributes([
IntegrationAttribute.appstackId: AppstackAttributionSdk.shared.getAppstackId()
])
```
```kotlin theme={null}
// Kotlin
// Use Superwall-Android version >= 2.7.5
Superwall.instance.setIntegrationAttributes(
mapOf(AttributionProvider.APPSTACK to AppstackAttributionSdk.getAppstackId())
)
```
```typescript theme={null}
// React Native / Expo
// Use expo-superwall version >= 1.0.5
const { setIntegrationAttributes } = useUser();
const appstackId = await AppstackSDK.getAppstackId();
await setIntegrationAttributes({ appstackId });
```
```dart theme={null}
// Flutter
// Use Superwall-Flutter version >= 2.4.11
await Superwall.shared.setIntegrationAttributes({
IntegrationAttribute.appstackId: await AppstackPlugin.getAppstackId(),
});
```
**2. Pass Appstack attribution params as Superwall user attributes**
Add this once before your first `Superwall.register` call so all placements include the attributes:
```swift theme={null}
// Swift
Task {
Superwall.shared.setUserAttributes(
await AppstackAttributionSdk.shared.getAttributionParams() ?? [:]
)
// Now, your placements will attach those user attributes,
// making them available for use in campaign filters.
Superwall.shared.register(placement: "onboarding_paywall")
}
```
```kotlin theme={null}
// Kotlin
fun prepareSuperwallPlacement() {
Superwall.instance.setUserAttributes(
AppstackAttributionSdk.getAttributionParams()
)
// Now, your placements will attach those user attributes,
// making them available for use in campaign filters.
Superwall.instance.register("onboarding_paywall")
}
// Call prepareSuperwallPlacement() after Appstack initialization and before this placement can be shown.
```
```typescript theme={null}
// Expo
const { update } = useUser();
const { registerPlacement } = usePlacement();
await update((await AppstackSDK.getAttributionParams()) ?? {});
// Now, your placements will attach those user attributes,
// making them available for use in campaign filters.
await registerPlacement({ placement: 'onboarding_paywall' });
```
```dart theme={null}
// Flutter
await Superwall.shared.setUserAttributes(
(await AppstackPlugin.getAttributionParams()) ?? {},
);
// Now, your placements will attach those user attributes,
// making them available for use in campaign filters.
await Superwall.shared.registerPlacement('onboarding_paywall');
```
**3. Credentials in dashboards (non-code steps)**
* In Appstack: `Integrations` → `Superwall` → copy **access token** and **app ID**.
* In Superwall: `Integrations` → `Appstack` → paste **access token** and **app ID**.
* Once active in Superwall, it can take **30–60 minutes** to appear active in Appstack.
* Avoid editing the integration in Superwall to prevent attribution issues.
With the Superwall integration, you can:
1. Use Superwall in-app events to run enhanced app campaigns.
2. Unlock attribution-based paywall optimization.
**To successfully connect Superwall, you must:**
1. Have Owner/Admin access to an Appstack organization.
2. Have access to the correct Superwall account.
## Requirements
Use a Superwall SDK version that includes the Appstack integration attribute:
| Platform | Package | Minimum version |
| :------------------ | :------------------ | :-------------- |
| iOS | `Superwall-iOS` | `4.12.11` |
| Android | `Superwall-Android` | `2.7.5` |
| Flutter | `Superwall-Flutter` | `2.4.11` |
| React Native / Expo | `expo-superwall` | `1.0.5` |
For React Native apps, use `expo-superwall`. The legacy `react-native-superwall` package is archived.
## Connect to Superwall
Follow the steps to ensure the Superwall integration works:
To successfully receive the Appstack ID, set the Appstack integration attribute after the calls to Superwall and Appstack configure methods:
```swift Swift theme={null}
// Use Superwall-iOS version >= 4.12.11
Superwall.shared.setIntegrationAttributes([
IntegrationAttribute.appstackId: AppstackAttributionSdk.shared.getAppstackId()
])
```
```kotlin Kotlin theme={null}
// Use Superwall-Android version >= 2.7.5
Superwall.instance.setIntegrationAttributes(
mapOf(AttributionProvider.APPSTACK to AppstackAttributionSdk.getAppstackId())
)
```
```typescript Expo theme={null}
// Use expo-superwall version >= 1.0.5
const { setIntegrationAttributes } = useUser();
const appstackId = await AppstackSDK.getAppstackId();
await setIntegrationAttributes({ appstackId });
```
```dart Flutter theme={null}
// Use Superwall-Flutter version >= 2.4.11
await Superwall.shared.setIntegrationAttributes({
IntegrationAttribute.appstackId: await AppstackPlugin.getAppstackId(),
});
```
To show paywalls based on where your users came from (paid ads), set Appstack's attribution params as [user attributes](https://superwall.com/docs/ios/quickstart/setting-user-properties) before your first `Superwall.register` call. You do not need to repeat this for every placement.
Learn more about how to use ad campaign data to show personalized paywalls and increase subscription revenue by [reading this article.](https://superwall.com/blog/show-paywalls-based-on-where-your-users-came-from-with-appstack-and)
```swift Swift theme={null}
Task {
Superwall.shared.setUserAttributes(
await AppstackAttributionSdk.shared.getAttributionParams() ?? [:]
)
// Now, your placements will attach those user attributes, making
// them available for use in campaign filters.
Superwall.shared.register(placement: "onboarding_paywall")
}
```
```kotlin Kotlin theme={null}
fun prepareSuperwallPlacement() {
Superwall.instance.setUserAttributes(
AppstackAttributionSdk.getAttributionParams()
)
// Now, your placements will attach those user attributes, making
// them available for use in campaign filters.
Superwall.instance.register("onboarding_paywall")
}
// Call prepareSuperwallPlacement() after Appstack initialization and before this placement can be shown.
```
```typescript Expo theme={null}
const { update } = useUser();
const { registerPlacement } = usePlacement();
await update((await AppstackSDK.getAttributionParams()) ?? {});
// Now, your placements will attach those user attributes, making
// them available for use in campaign filters.
await registerPlacement({ placement: 'onboarding_paywall' });
```
```dart Flutter theme={null}
// Use Superwall-Flutter version >= 2.4.11
await Superwall.shared.setUserAttributes(
(await AppstackPlugin.getAttributionParams()) ?? {},
);
// Now, your placements will attach those user attributes, making
// them available for use in campaign filters.
await Superwall.shared.registerPlacement("onboarding_paywall");
```
1. In Appstack, from the side menu, select **Integrations** > **Superwall.**
2. Copy the **access token.**
3. Copy the **app ID.**
1. In Superwall, from the side menu, select **Integrations** > **Appstack.**
2. Paste the **access token.**
3. Paste the **app ID.**
**Information**
1. After the integration is active on Superwall's platform, it can take 30-60 minutes to appear as active on Appstack's side.
2. It's recommended not to edit or modify the integration on Superwall's platform to avoid attribution issues.
The integration will show an error if less than 50% of events received over the last 24 hours include an `appstackId`. When this threshold is not met, these events cannot be used in ad network integration pages.
## List of events
Below is a list of events you can forward to ad networks to optimize your campaigns.
| Name | Definition |
| :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sw_trial_started` | Fired when a user begins a free trial. Triggered on the initial purchase when the period type is `TRIAL`. |
| `sw_trial_qualified` | Fired when a free trial is still active two hours after it started — meaning the user did not cancel within the qualification window. |
| `sw_trial_converted` | Fired when a free trial successfully converts to a paid subscription. This happens on the first renewal after the trial period ends. |
| `sw_intro_started` | Fired when a user begins an intro offer — a paid trial at a discounted price (e.g., \$0.99 for the first week). Triggered on the initial purchase when the period type is `INTRO` |
| `sw_trial_intro_started` | Fired when either a free trial starts (`sw_trial_started`) or an intro offer starts (`sw_intro_started`). The event triggers as soon as at least one of these conditions is met. |
| `sw_trial_intro_qualified` | Fired when either a free trial qualifies (`sw_trial_qualified`) or an intro offer qualifies (`sw_intro_qualified`). The event triggers as soon as at least one of these conditions is met. |
| `sw_initial_purchase` | Fired when any of the following occur: a free trial starts (`sw_trial_started`), an intro offer starts (`sw_intro_started`), or a full-price subscription starts (`sw_subscription_started`). The event triggers as soon as at least one of these conditions is met. |
| `sw_initial_purchase_qualified` | Fired when any of the following occur: a trial qualified happens (`sw_trial_qualified`), an intro offer starts (`sw_intro_started`), or a full-price subscription starts (`sw_subscription_started`). The event triggers as soon as at least one of these conditions is met. |
| `sw_subscription_started` | Fired when a user starts a paid subscription at full price, with no trial or intro offer involved. Triggered on the initial purchase when the period type is `NORMAL` |
| `sw_subscription_renewed` | Fired on each successful renewal of an active subscription. Indicates the user was billed again and remains subscribed for another period. |
| `sw_non_renewing_purchase` | Fired when a user makes a one-time, non-subscription purchase — any product that is not an auto-renewing subscription (e.g., consumables or non-consumable in-app purchases). Unlike subscription events, this purchase does not renew automatically. |
## Troubleshooting
### **General tips**
1. Check SDK versions: confirm the Superwall SDK meets the minimum that includes the Appstack integration attribute (`Superwall-iOS` 4.12.11+, `Superwall-Android` 2.7.5+, `Superwall-Flutter` 2.4.11+, `expo-superwall` 1.0.5+). React Native apps must use `expo-superwall`, not the archived `react-native-superwall`.
2. Check the ordering: `setIntegrationAttributes(...)` must run **after** both Appstack and Superwall are configured. If it runs before the Appstack SDK has an ID, no `appstackId` is attached.
3. Set attribution params once: call `setUserAttributes(...)` a single time **before** your first registration call — Swift `Superwall.shared.register(placement:)`, Kotlin `Superwall.instance.register(...)`, or Expo/Flutter `registerPlacement` — so every placement carries the attributes.
4. Allow time for activation: after the integration is active in Superwall, it can take **30–60 minutes** to appear active in Appstack.
5. Contact support: if issues persist, reach out with specific error messages at [support@appstack.tech](mailto:support@appstack.tech).
### Common issues
| Issue | How to fix |
| :------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| "Less than 50% of events include an `appstackId`" error | 1. Confirm the app version that includes the Appstack SDK is already released on the store and that more than 24 hours have passed, so enough live installs have reported events.
2. Confirm `setIntegrationAttributes(...)` runs on every app start after both SDKs are configured.
3. Confirm `getAppstackId()` returns a non-null value before the call.
4. Allow up to 24 hours for coverage to recover once the fix is rolled out. Below this threshold, events cannot be used in ad network integration pages. |
| `appstackId` never reaches Superwall | 1. Verify the call uses the correct integration attribute symbol (`IntegrationAttribute.appstackId` on Swift/Flutter, `AttributionProvider.APPSTACK` on Android) with the value from `getAppstackId()` exactly as shown in the docs.
2. Verify it is not being called before the Appstack SDK has initialized.
3. Verify it is not overwritten by a later `setIntegrationAttributes(...)` call with a null value. |
| Attribution params missing from campaign filters | 1. Confirm `setUserAttributes(...)` is called once before the first `register` / `registerPlacement`.
2. Confirm registrations happen **after** attributes are set.
3. Confirm `getAttributionParams()` returns campaign data. On iOS, read `appstack_match_status`: `organic`, `skipped` and `matched_no_params` mean there is genuinely nothing to filter on — `matched_no_params` is an attributed install whose link carried no tracking params. `failed` means the match has not resolved yet, so read it again later. `not_configured` is retryable only when it was read before `configure(...)` finished — if the SDK is disabled by an invalid API key or a kill switch, fix the configuration, because retrying never resolves it. |
| Integration not showing as active in Appstack | 1. Re-check the **access token** and **app ID** pasted in Superwall match Appstack.
2. Wait 30–60 minutes after activation.
3. Avoid editing the integration in Superwall, which can break attribution. |
You are an expert mobile monetization engineer reviewing my existing Superwall + Appstack integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Your goal is to **validate that my Superwall integration fully matches the documentation** and that event and attribution data can flow correctly between Appstack and Superwall. When I paste this prompt and share my platform-specific code and configuration, you should:
1. **SDK and version checks**
* Confirm that:
* The Appstack SDK is installed and initialized correctly for the platform (Swift, Kotlin, Expo, or Flutter).
* The Superwall SDK version supports Appstack: `Superwall-iOS` 4.12.11+, `Superwall-Android` 2.7.5+, `Superwall-Flutter` 2.4.11+, or `expo-superwall` 1.0.5+.
* React Native apps use `expo-superwall`, not the archived `react-native-superwall` package.
* Point out any missing dependencies, outdated versions, or incorrect initialization order (e.g. Superwall methods being used before Appstack has an ID).
2. **Appstack ID integration**
* Locate calls to:
* `Superwall.shared.setIntegrationAttributes(...)` (Swift)
* `Superwall.instance.setIntegrationAttributes(...)` (Kotlin)
* `setIntegrationAttributes({ appstackId })` from `expo-superwall` (React Native / Expo)
* `Superwall.shared.setIntegrationAttributes(...)` with `await AppstackPlugin.getAppstackId()` (Flutter)
* Verify that:
* They use the Appstack SDK methods (`getAppstackId` / `getAppstackId()`) exactly as shown in the docs.
* They are called **after** both SDKs are configured and not redundantly in multiple places.
* Propose concrete code changes if these calls are missing, misordered, or incorrectly parameterized.
* Remind me that the integration is disabled when less than 50% of events over the last 24 hours include an `appstackId`, and that this coverage only stabilizes once the app build with the Appstack SDK has been live on the store for more than 24 hours.
3. **User attributes / attribution params**
* Find where I call:
* `Superwall.shared.setUserAttributes(await AppstackAttributionSdk.shared.getAttributionParams() ?? [:])` inside `Task {}` or another async context (Swift)
* `Superwall.instance.setUserAttributes(AppstackAttributionSdk.getAttributionParams())` (Kotlin)
* `useUser().update((await AppstackSDK.getAttributionParams()) ?? {})` (Expo)
* `Superwall.shared.setUserAttributes((await AppstackPlugin.getAttributionParams()) ?? {})` (Flutter)
* Validate that:
* These lines run **once** before the first registration call (Swift `Superwall.shared.register(placement:)`, Kotlin `Superwall.instance.register(...)`, or Expo/Flutter `registerPlacement`).
* Registrations like `"onboarding_paywall"` happen **after** attributes are set so campaign filters can use them.
* Suggest exact placement and code if the attributes are missing or set too late.
4. **Validation report & checklist**
* Produce a clear report summarizing:
* What is correctly implemented and safe for production.
* What is missing, misconfigured, or risky, with platform-specific file names and snippets to change.
* End with a **Superwall-specific checklist** (SDK versions, Appstack ID integration attribute, attribution params as user attributes, register ordering, dashboard credentials) that I can use to confirm my integration fully matches the Superwall integration guide.
# TikTok
Source: https://docs.appstack.tech/Integrations/tiktok-ads
With the TikTok Ads integration, you can:
1. Run enhanced app campaigns.
2. Import all your ad campaigns (app and web).
3. Enable Appstack to audit TikTok's reporting capabilities.
4. Allow Appstack to send in-app events, postbacks (signals) back to TikTok Ads.
**To successfully connect TikTok Ads, you must:**
1. Have 'Administrator' access over the right TikTok Ads business center or ad account. 'Standard' access is not enough to make the integration work.
2. Ensure the app used in Appstack is connected to the correct ad account.
| Metrics | Definition |
| :------------------------ | :------------------------------------------------------------------------ |
| Ad spend | Money paid to run ads |
| Installs | New or old users who installed the app |
| Cost per install (CPI) | Cost per install from your ad campaigns. Formula: Ad spend / Installs |
| Installs per mille (IPM) | Installs per 1,000 impressions. Formula: (Installs / Impressions) x 1,000 |
| Impressions | Number of times an ad was shown |
| Cost per mille (CPM) | Cost per 1,000 impressions. Formula: (Ad spend / Impressions) x 1,000 |
| Clicks | Number of times an ad was clicked |
| Cost per click (CPC) | Cost per click. Formula Ad spend / Clicks |
| Click-through rate (CTR) | How often an impression becomes a click. Formula: Clicks / Impressions |
| Click to install rate | Share of clicks that became installs. Formula: Installs / Clicks |
| Return on ad spend (ROAS) | Return on ad spend. Formula: Ads revenue / Ad spend |
| Revenue | Total money earned from attributed users. |
| In-app events | Definition |
| :-------------------- | :------------------------------------------------------------------ |
| Achieve level | User reaches a level/milestone |
| Add payment info | User enters or saves payment details |
| Add to cart | User adds an item to the shopping cart |
| Complete registration | User finishes sign-up (account creation) |
| Initiate checkout | User starts the checkout flow |
| Session | App open, resulting in a user session |
| View content | User views a key screen/content item |
| Start trial | User begins a free trial for a subscription |
| Subscription | First paid subscription period begins (with or without prior trial) |
| Purchase | One-time in-app purchase (consumable or non-consumable) |
## Connect to TikTok Ads
The first step is to connect your TikTok Ads account with Appstack. Follow these steps:
1. In Appstack, from the side menu, select **Integrations** > **TikTok Ads.**
2. Click on **Connect to TikTok Ads.**
3. In TikTok's access integration flow, log in to your TikTok Business account with access to the correct ad account and apps.
4. **Accept** the requested permissions by clicking Confirm, and you will then be redirected to Appstack's integration page.
## Configure the Events API
Configuring the Events API with Appstack will allow you to run enhanced app campaigns using the assigned ads link.
The pixel ID is the destination that receives only Appstack-attributed events. Choosing the wrong pixel ID will cause your app's events to be sent to the wrong destination.
**Information**
For additional information on how to create the pixel ID and get the Events API access token: [Official documentation](https://ads.tiktok.com/help/article/how-to-create-and-access-tiktok-pixel-id?lang=en)
Follow these steps if you don't have an existing pixel:
1. At the Business Center level, go to 'Assets' and click **Add a pixel.**
2. Select 'New pixel' and click on **Next.**
3. Add your website URL, then click **Next.**
4. Select 'Manual setup' and click on **Next.**
5. Select 'Events API' only and click on **Next.**
6. Add a name to your pixel, then click **Create.**
7. In the 'Set up your business funnel' section, select E-commerce as the template, then click **Next.**
8. **Copy** the pixel ID (example of a pixel ID: D6DSQIJJ77U7GSLDGAY0).
9. **Generate an access token** and copy it (e.g., 2b5700268ay04e741f0045w5f5c17f442ad713fb).
10. Click on **Finish.**
11. Go back to the Business Center page, go to **Assets**, and click on the newly created pixel.
12. Click on **Link accounts**, select the correct ad account, then click on **Confirm.**
To find an existing pixel ID, follow these steps:
1. At the Business Center level, go to **Assets** and ensure that the pixel is connected to the correct ad account.
2. Click on **Open in Events Manager** to see the pixel ID and generate the access token.
3. Copy the pixel ID located at the top left under the pixel name.
4. On the same page, click on **Settings.**
5. Scroll down to the 'Events API' section and click on **Generate access token.**
6. **Copy** the access token.
It's recommended to use a new data source (pixel ID) to work with clean data
To successfully send in-app events to TikTok Ads:
1. Select the in-app events from the dropdown button under the **Appstack SDK events** column.
2. Map the selected in-app event to a TikTok standard event. For example, if you want to optimize your ad campaigns towards the SDK event **SUBSCRIBE**, you can select the TikTok standard event called **Subscribe**.
**Information**
1. To complete this step successfully, you first need to receive an install event from the Appstack SDK.
2. Once the in-app events are selected, they will take between 30 and 90 minutes to appear in your dataset in the TikTok Events Manager.
3. Appstack sends only attributed events to TikTok Events Manager. If you haven't launched a campaign yet, or just reconnected the integration, it's expected to see little or no activity at first, events will start flowing in once you have a campaign running and generating attributed installs.
4. TikTok does not support custom events for campaign optimization. Learn more on the official site: [TikTok events documentation](https://business-api.tiktok.com/portal/docs?id=1771101186666498).
5. We don't support the 'Lead generation' campaign objective. Because of this, 'StartTrial' can't be used as a conversion event for campaign optimization. Use 'Subscribe' instead.
1. **Copy the ad link** from the Appstack's integration page (located at the bottom of the page).
2. Inside TikTok Ads Manager, click on **+ Create** to start the campaign creation flow.
3. Select **Sales** as the campaign objective (recommended if optimizing for Purchase or Subscribe). We don't support 'Lead generation' campaigns. Then select **Website** as 'Sales destination' (don't select App) and choose between **Manual** or **Search** as the 'campaign type'.
4. At the ad set level, under the 'Optimization location' section, select **Website** as the 'Location', select the **correct dataset** (same as in Appstack), and for the 'Optimization event' select the **in-app event** you want the campaign to optimize for.
5. In the **Placements** section, at the device level, ensure that only TikTok is selected (avoid automatic placement).
6. In the **Targeting** section, ensure only one operating system is selected (iOS or Android).
7. In the **Bidding** section, you can select **Conversion** or **Value** as the 'Optimization goal.'
8. At the ad level, **paste the ad link** into the ‘Website URL’ box. Don’t paste the URL parameters in the ‘Tracking’ section, copied in the first step.
**Information**
1. Not selecting the specific operating system (device) at the ad set level will affect your ad performance, as users will click the ad links but not be able to download the app because the ad links are specific to each operating system (iOS or Android).
2. To enable value-based optimization (VBO), you need to have obtained at least 20 attributed, unique, complete payment events with value and currency over any consecutive 7-day period on TikTok. Learn more on the official site: [How to enable value-based optimization.](https://ads.tiktok.com/help/article/how-to-promote-a-website-using-value-based-optimization)
3. The Sales campaign objective doesn't support optimizing for 'Start trial'. Since we don't support 'Lead generation' campaigns, use 'Subscribe' as your optimization event instead.
## Pre-Launch EAC Checklist
SDK installed and app update released (if connecting a Subscription Platform, the update must include the SDK + snippet)
Events confirmed as flowing through the Appstack SDK page
Code snippet pasted following the docs instructions
Credentials copied and pasted
Update rolled out and Appstack ID coverage has reached 50% of events received
TikTok Ads connected
Events API configured
Pixel ID and Events API access token configured
Selected in-app events mapped to TikTok standard events
New campaign created with 'Sales' (Purchase/Subscribe) selected
Website set as 'Sales destination' and Manual or Search selected as 'Campaign type'
At the ad set level, Website selected as 'Optimization location' and correct dataset chosen
In-app event selected as the 'Optimization event'
At the device level, only TikTok selected in the Targeting section
Conversion or Value selected as the 'Optimization goal' in the Bidding section
Appstack ad link pasted ONLY into the 'Website URL' box at the ad level
## Troubleshooting
### **General tips**
1. Check your permissions: Ensure you have admin access to connect.
2. Verify app connections: Ensure your app is correctly connected to the ad accounts.
3. Update tokens: Generate fresh API keys and access tokens by reconnecting.
4. Missing metrics in TikTok: Ensure you select the web-based metric in TikTok Ads Manager to view total downloads and in-app events.
5. Domain mismatch warning: if TikTok shows a warning saying your ad link's domain doesn't match the domain associated with your data connection, this is expected with the Appstack setup and can be safely ignored, it doesn't affect attribution or tracking.
6. Contact support: If issues persist, reach out to our support team with specific error messages at [support@appstack.tech](mailto:support@appstack.tech).
### Error messages
| Error | How to fix |
| :-------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Authentication failed | 1. Go to your TikTok Business Center
2. Check that your user account has the correct permissions
3. Try to connect again in Appstack |
| Application not found | 1. Verify your app ID is correct in TikTok Business Center
2. Ensure your user account has access to the correct ad account
3. Check that the app is connected to the right ad account |
# Flutter
Source: https://docs.appstack.tech/SDKs/flutter
**Prefer to automate this?** The [Appstack CLI](/tooling/cli) detects your project, installs and configures the SDK, then verifies the result:
```bash theme={null}
npx appstack-cli integrate
```
Already integrated? `npx appstack-cli review` audits it without changing any files.
You are an expert Flutter engineer helping me integrate the Appstack Flutter plugin into my app. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to fully wire the plugin. When I paste this prompt and share my Dart and platform files, you should:
1. Provide the exact `pubspec.yaml` changes and `flutter pub` commands required, plus any iOS (`pod install`) and Android (repositories/Gradle) configuration, using the details below.
2. Tell me exactly where to initialize the plugin in my Flutter app and generate idiomatic Dart code to do it, based on the snippets below.
3. Validate my iOS and Android setup (versions, repositories, build settings) against the documentation and highlight anything missing.
4. Summarize the full checklist (install, configure, platform setup, event tracking) so we can confirm that everything from the docs has been applied.
***
## Reference: Appstack Flutter plugin
**Requirements**
* iOS: 13.0+ (15.0+ required for Apple Ads attribution), Xcode 14.0+
* Android: Min SDK 21, Target SDK 35+
* Flutter: 3.3.0+
* Dart: 2.18.0+
**pubspec.yaml & installation**
```bash theme={null}
flutter pub add appstack_plugin
flutter pub get
```
(Or add `appstack_plugin` under `dependencies` in `pubspec.yaml` using the **current** version from [pub.dev/packages/appstack\_plugin](https://pub.dev/packages/appstack_plugin).)
**iOS configuration**
```bash theme={null}
cd ios && pod install
```
* The iOS `AppstackSDK.xcframework` is bundled with the plugin; no extra dependencies required.
**Android configuration**
```gradle theme={null}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
```
**Quickstart example**
```dart theme={null}
import 'package:flutter/material.dart';
import 'package:appstack_plugin/appstack_plugin.dart';
import 'dart:io' show Platform;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configure the SDK
final apiKey = Platform.isIOS
? 'your-ios-api-key'
: 'your-android-api-key';
await AppstackPlugin.configure(apiKey);
// Enable Apple Ads attribution on iOS
if (Platform.isIOS) {
await AppstackPlugin.enableAppleAdsAttribution();
}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
void trackPurchase() {
AppstackPlugin.sendEvent(
EventType.purchase,
parameters: {'revenue': 29.99, 'currency': 'USD'}
);
}
@override
Widget build(BuildContext context) {
// ... your app
}
}
```
**Configuration parameters**
```dart theme={null}
await AppstackPlugin.configure('your-api-key-here');
// With optional named parameters
await AppstackPlugin.configure(
'your-api-key-here',
logLevel: 0, // 0=DEBUG, 1=INFO (default), 2=WARN, 3=ERROR
customerUserId: 'user_123',
);
```
**Customer user ID**
```dart theme={null}
await AppstackPlugin.setCustomerUserId('user_123'); // once the ID is known
```
**Sending events**
```dart theme={null}
// Track events without parameters
await AppstackPlugin.sendEvent(EventType.signUp);
await AppstackPlugin.sendEvent(EventType.levelComplete);
// Track events with parameters (including revenue)
await AppstackPlugin.sendEvent(
EventType.purchase,
parameters: {'revenue': 29.99, 'currency': 'USD'}
);
await AppstackPlugin.sendEvent(
EventType.subscribe,
parameters: {'revenue': 9.99, 'plan': 'monthly'}
);
// Custom events
await AppstackPlugin.sendEvent(
EventType.custom,
eventName: 'user_attributes',
parameters: {
'email': 'test@example.com',
'name': 'John Doe',
'phone_number': '+33060000000',
'date_of_birth': '2026-02-01',
},
);
```
**EventType values (recommended standard events):**
* Authentication: `EventType.login`, `EventType.signUp`, `EventType.register`
* Monetization: `EventType.purchase`, `EventType.addToCart`, `EventType.addToWishlist`, `EventType.initiateCheckout`, `EventType.startTrial`, `EventType.subscribe`
* Games: `EventType.levelStart`, `EventType.levelComplete`
* Engagement: `EventType.tutorialComplete`, `EventType.search`, `EventType.viewItem`, `EventType.viewContent`, `EventType.share`
* Custom: `EventType.custom`
**Enhanced app campaigns**
* For revenue events, send:
* `revenue` or `price` (number)
* `currency` (e.g. `EUR`, `USD`)
* To improve Meta matching, include when possible:
* `email`
* `name` (first + last name)
* `phone_number` (also accepted as `phone` or `phoneNumber`)
* `date_of_birth` (`YYYY-MM-DD`; also accepted as `birthdate`, `birthday`, or `dateOfBirth`)
* `gender`
* Appstack automatically encrypts these matching parameters before using them for attribution matching.
## **Repository**
Here, you will find the [pub.dev appstack\_plugin documentation](https://pub.dev/packages/appstack_plugin). Please use the latest available version of the SDK.
Current stable release: **2.7.0**, published 3 September 2026. See the [changelog](/changelog/flutter) for everything that changed.
## **Quickstart**
Use this path when you only need the minimum production integration:
1. Add `appstack_plugin` from pub.dev.
2. Run `flutter pub get` and `cd ios && pod install` for iOS projects.
3. Copy the **Production** API key from **SDK** in Appstack.
4. Call `AppstackPlugin.configure(...)` from `main()` before `runApp`.
5. Send standard events such as `EventType.login`, `EventType.signUp`, `EventType.purchase`, and `EventType.subscribe`.
6. Confirm events appear in the Appstack SDK page before enabling downstream integrations.
```dart theme={null}
import 'package:appstack_plugin/appstack_plugin.dart';
import 'dart:io' show Platform;
final apiKey = Platform.isIOS
? 'your-ios-production-api-key'
: 'your-android-production-api-key';
await AppstackPlugin.configure(apiKey);
await AppstackPlugin.sendEvent(
EventType.purchase,
parameters: {'revenue': 29.99, 'currency': 'USD'},
);
```
## **Requirements**
### **iOS**
* **iOS version:** 13.0+ (15.0+ required for Apple Ads attribution).
* **Xcode:** 14.0+
### **Android**
* **Minimum SDK:** Android 5.0 (API level 21).
* **Target SDK:** 35+
### **General**
* **Flutter:** 3.3.0+
* **Dart:** 2.18.0+
## **Initial setup**
From your project root:
```text theme={null}
flutter pub add appstack_plugin
flutter pub get
```
Or add `appstack_plugin` under `dependencies` in `pubspec.yaml` using the **current** version from [pub.dev](https://pub.dev/packages/appstack_plugin), then run `flutter pub get`.
**iOS Configuration**
Run pod install:
```text theme={null}
cd ios && pod install
```
**Note:** The iOS AppstackSDK.xcframework is bundled with the plugin; no additional dependencies are needed.
**Android Configuration**
Add the repository to your `android/build.gradle`:
```dart theme={null}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
```
No additional Android configuration is needed after adding the repository. You still need to initialize the plugin before using SDK methods.
Follow these steps to get the API key:
1. In Appstack, from the side menu, select **SDK** and ensure you are selecting the correct application.
2. Select the **Production** environment.
3. Copy the **API key**.
Examples:
```dart theme={null}
import 'package:flutter/material.dart';
import 'package:appstack_plugin/appstack_plugin.dart';
import 'dart:io' show Platform;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configure the SDK
final apiKey = Platform.isIOS
? 'your-ios-api-key'
: 'your-android-api-key';
await AppstackPlugin.configure(apiKey);
// Enable Apple Ads attribution on iOS
if (Platform.isIOS) {
await AppstackPlugin.enableAppleAdsAttribution();
}
runApp(MyApp());
}
class MyApp extends StatelessWidget {
void trackPurchase() {
AppstackPlugin.sendEvent(
EventType.purchase,
parameters: {'revenue': 29.99, 'currency': 'USD'}
);
}
@override
Widget build(BuildContext context) {
// ... your app
}
}
```
Initializes the SDK with your API key. Call this from `main()` before `runApp`.
**Parameters:**
* `apiKey` (String, required, positional): Your platform-specific API key from the Appstack dashboard.
* `logLevel` (int, default: 1): Console log verbosity — 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR.
* `customerUserId` (String?, default: null): Your own user identifier, attached to the event payload.
Returns: A future that completes when configuration is done. It throws if configuration fails.
**Example:**
```dart theme={null}
// Minimum configuration — the API key is the only required parameter
await AppstackPlugin.configure('your-api-key-here');
// With optional named parameters
await AppstackPlugin.configure(
'your-api-key-here',
logLevel: 0, // DEBUG
customerUserId: 'user_123',
);
```
The customer user ID is your own identifier for the signed-in user. Appstack attaches it to events so server-to-server events — which identify the user by this ID rather than by the install — can be joined back to the install that produced them.
Set it as soon as it is known. That can be before `configure` runs, at `configure` time via the `customerUserId` parameter, or later — most often once a login reveals it:
```dart theme={null}
await AppstackPlugin.setCustomerUserId('user_123');
```
* `customerUserId` (String, positional): your identifier for the signed-in user. Leading and trailing whitespace is trimmed.
* Returns a future that completes once the native SDK has accepted the ID, and throws if the platform call fails.
* Safe to call at any time. It applies to every event sent from then on, including ones the native SDK has buffered but not yet flushed.
* If you also pass a `customerUserId` to `configure(...)`, that value wins; passing none leaves the ID you already set in place.
* The call does not send anything by itself: make sure at least one event follows, or no mapping is ever formed.
* Calling `configure(...)` again to set the ID does not work — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored. Use `setCustomerUserId` instead.
Track user actions and revenue in your activities:
```dart theme={null}
// Track events without parameters
await AppstackPlugin.sendEvent(EventType.signUp);
await AppstackPlugin.sendEvent(EventType.levelComplete);
// Track events with parameters (including revenue)
await AppstackPlugin.sendEvent(
EventType.purchase,
parameters: {'revenue': 29.99, 'currency': 'USD'}
);
await AppstackPlugin.sendEvent(
EventType.subscribe,
parameters: {'revenue': 9.99, 'plan': 'monthly'}
);
// Custom events
await AppstackPlugin.sendEvent(
EventType.custom,
eventName: 'user_attributes',
parameters: {
'email': 'test@example.com',
'name': 'John Doe',
'phone_number': '+33060000000',
'date_of_birth': '2026-02-01',
},
);
```
**Available EventType values**
It is recommended to use standard events for a smoother experience.
`EventType.install` is tracked automatically on SDK initialization. Do not send it manually.
* `EventType.login`/ `EventType.signUp`/ `EventType.register` Authentication
* `EventType.purchase`/ `EventType.addToCart`/ `EventType.addToWishlist`/ `EventType.initiateCheckout`, `EventType.startTrial`/ `EventType.subscribe` Monetization
* `EventType.levelStart`/ `EventType.levelComplete` Game progression
* `EventType.tutorialComplete`/ `EventType.search`/ `EventType.viewItem`/ `EventType.viewContent`/ `EventType.share` Engagement
* `EventType.custom` For application-specific events
Tracks custom events with optional parameters:
* `eventType` - Event type from the EventType enum (required).
* `eventName` - Event name for custom events (optional, required when eventType is custom).
* `parameters` - Optional map of parameters (e.g., `{'revenue': 29.99, 'currency': 'USD'}`).
Returns: A future that resolves to `true` if event was sent successfully.
**Enhanced app campaigns**
When running enhanced app campaigns (EACs), it is highly recommended to send multiple parameters with the in-app event to improve matching quality.
For any event that represents revenue, we recommend sending:
1. `revenue` or `price` (number).
2. `currency`(string, e.g. `EUR`, `USD`).
```dart theme={null}
await AppstackPlugin.sendEvent(
EventType.purchase,
parameters: {'revenue': 4.99, 'currency': 'EUR'},
);
```
To improve matching quality on Meta, send events including the following parameters if you can fulfill them. Appstack automatically encrypts these matching parameters before using them for attribution matching.
1. `email`.
2. `name` (first + last name in the same field).
3. `phone_number` — also accepted as `phone` or `phoneNumber`.
4. `date_of_birth` (recommended format: `YYYY-MM-DD`) — also accepted as `birthdate`, `birthday`, or `dateOfBirth`.
5. `gender`.
## **Appstack ID and attribution params**
After `configure`, you can read the Appstack user ID and the attribution map for partner integrations (for example Superwall, RevenueCat).
```dart theme={null}
final String? appstackId = await AppstackPlugin.getAppstackId();
final Map attributionParams =
await AppstackPlugin.getAttributionParams() ?? {};
```
* **`getAppstackId()`** — Appstack user identifier when a partner expects `$appstackId` or similar.
* **`getAttributionParams()`** — Attribution payload to forward to partners. It waits for the attribution match to finish, so you do not need a fixed delay after launch.
A matched install returns something like:
```json theme={null}
{
"appstack_adnetwork": "meta",
"appstack_campaign": "summer_sale",
"appstack_adset": "lookalike_1pct",
"appstack_ad": "video_15s",
"appstack_id": "9f1c8b64-...",
"appstack_match_status": "matched"
}
```
`appstack_adnetwork` is `google`, `meta` or `tiktok`. It is absent when the click carried no recognised network param — for a standard link, read `media_source` instead. On Google the campaign, ad set and ad values are numeric IDs rather than names. `appstack_id` comes back on every result, including organic ones, so read `appstack_match_status` rather than testing whether the map is empty.
`appstack_match_status` tells you whether the match resolved:
| Value | Meaning |
| :------------------ | :-------------------------------------------------------------------------------------------------------------- |
| `matched` | Attributed, with campaign params included. |
| `matched_no_params` | Attributed, but the clicked link carried no tracking params. |
| `organic` | Appstack confirmed there is no attribution for this device. |
| `skipped` | Not an attributable install, so no request was made. |
| `failed` | The request did not complete (offline, timeout, server error) — read again later. |
| `not_configured` | Read before `configure(...)` completed, or the SDK is disabled (invalid API key, or a server-side kill switch). |
It is added on iOS from plugin `2.6.0`+. `failed` is always worth reading again — the SDK retries in-session and on the next launch. `not_configured` is worth re-reading only when it was read before `configure(...)` finished; a disabled SDK never resolves. The other four are final for the life of the install.
Check for the key rather than assuming it is there — **Android does not report it yet**.
## **Development setup**
### **Environment-based configuration**
Set up different API keys for different environments:
```dart theme={null}
// Use environment variables or configuration
final apiKey = Platform.isIOS
? const String.fromEnvironment('APPSTACK_IOS_API_KEY')
: const String.fromEnvironment('APPSTACK_ANDROID_API_KEY');
await AppstackPlugin.configure(apiKey);
```
## **Platform-specific considerations**
### **iOS**
**Apple Ads attribution:**
* Only works on iOS 15.0+ (below that the call is a no-op)
* Requires app installation from App Store or TestFlight.
* Attribution data appears within 24-48 hours.
* User consent may be required for detailed attribution (iOS 14.5+).
```dart theme={null}
if (Platform.isIOS) {
await AppstackPlugin.enableAppleAdsAttribution();
}
```
### **Android**
**Play Store Attribution**
* Install referrer data collected automatically.
* Attribution available immediately for Play Store installs.
* Works with Android 5.0+ (API level 21).
### **Cross-platform best practices**
```dart theme={null}
Future initializeSDK() async {
final apiKey = Platform.isIOS
? 'your-ios-api-key'
: 'your-android-api-key';
await AppstackPlugin.configure(apiKey);
if (Platform.isIOS) {
await AppstackPlugin.enableAppleAdsAttribution();
}
}
```
## **Security and privacy**
* Never commit API keys to version control.
* Use separate production and development keys.
* Do not put personally identifiable information in event names.
* Only send matching parameters such as `email`, `name`, `phone_number`, and `date_of_birth` when your app has the right consent and compliance basis. Appstack automatically encrypts these fields before using them for attribution matching.
* Prefer standard event types for common flows so event mapping remains consistent across Appstack and ad integrations.
## **Limitations**
### **Attribution timing**
* **iOS:** Apple Ads attribution data appears within 24-48 hours after install.
* **Android:** Install referrer data available immediately for Play Store installs.
* Attribution only available for apps installed from official stores.
### **Platform constraints**
* **iOS:** Requires iOS 13.0+; Apple Ads attribution requires iOS 15.0+.
* **Android:** Minimum API level 21 (Android 5.0).
* **Flutter:** 3.3.0+
* Some Apple Ads features may not work in development/simulator environments.
### **Event tracking**
* Event names are case-sensitive and standardized.
* For revenue events, always pass a `revenue` (or `price`) and a `currency` parameter.
* The SDK must be initialized from `main()` before `runApp` and before any tracking calls.
* `enableAppleAdsAttribution()` only works on iOS and returns false on Android.
* Network connectivity required for event transmission (events are queued offline).
## **Troubleshooting**
### **Configuration fails**
* Confirm the API key was copied from the correct app and environment in Appstack.
* Confirm `AppstackPlugin.configure(...)` runs from `main()` after `WidgetsFlutterBinding.ensureInitialized()` and before `runApp`.
* Confirm `flutter pub get` has completed successfully.
* For iOS, confirm `cd ios && pod install` has completed successfully.
### **Events do not appear**
* Confirm `configure(...)` completes before the first `sendEvent(...)` call.
* Confirm the device has network connectivity.
* Check that revenue events include a numeric `revenue` or `price` value and a valid `currency`.
* Allow a few minutes for events to appear in the dashboard.
### **iOS attribution does not appear**
* Confirm the app was installed from the App Store or TestFlight.
* Confirm the device runs iOS 15.0+.
* Call `AppstackPlugin.enableAppleAdsAttribution()` only on iOS after initialization.
* Allow 24-48 hours for attribution data to appear.
### **Android attribution does not appear**
* Confirm the app was installed from the Play Store.
* Confirm Android min SDK is 21+ and target SDK is 35+.
* Confirm the required repositories are available in your Android Gradle configuration.
## **Superwall**
To start using the Superwall integration, [click here](/Integrations/superwall) to see the correct SDK documentation.
## **Apple Ads**
To start using the Apple Ads integration, [click here](/Integrations/apple-ads) to see the correct SDK documentation.
## **Verification checklist**
* `appstack_plugin` is installed from pub.dev.
* `flutter pub get` has completed successfully.
* iOS dependencies are installed with `cd ios && pod install`.
* App meets Flutter 3.3.0+, Dart 2.18.0+, iOS 13.0+, Android min SDK 21, and target SDK 35+ requirements.
* Platform-specific API keys are configured for iOS and Android.
* Production API keys are used in production builds.
* Development API keys are used only in development builds.
* `configure(...)` runs once from `main()` before `runApp`.
* The customer user ID is set — via `configure(...)` or `setCustomerUserId(...)` — as soon as it is known, and at least one event follows it.
* iOS-only Apple Ads calls are guarded with `Platform.isIOS`.
* `EventType.install` is not sent manually.
* Login, signup, purchase, subscription, and other key app events use standard event types where possible.
* Revenue events include `revenue` or `price` and `currency`.
* Custom events use `EventType.custom` with a descriptive `eventName`.
* Appstack ID and attribution params are available before wiring partner integrations.
* Events are visible in the Appstack SDK page before launch.
## **Support**
For questions or issues:
1. Check the [GitHub Repository](https://github.com/appstack-tech/appstack-flutter-sdk).
2. Contact our support team at [support@appstack.tech](mailto:support@appstack.tech)
3. Open an issue in the repository.
You are an expert Flutter engineer reviewing my existing Appstack Flutter plugin integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my Dart and platform-specific code.
Your goal is to **validate that my integration fully matches the official Flutter SDK documentation** and identify any missing or incorrect steps. When I paste this prompt and share my project files, you should:
1. **Dependencies & environment**
* Inspect my `pubspec.yaml` and lockfile to confirm:
* The `appstack_plugin` dependency is present at a **current** version (check pub.dev rather than assuming a fixed constraint).
* My environment meets the requirements (Flutter 3.3.0+, Dart 2.18.0+, iOS 13.0+ / 15.0+ for Apple Ads, Android min SDK 21, target SDK 35+).
* Suggest exact `flutter pub` commands and any version adjustments needed if something is out of spec.
2. **iOS configuration (Pods)**
* Check that:
* `cd ios && pod install` has been run and the plugin is integrated.
3. **Android configuration**
* Review my Android build files to verify:
* Repositories include `google()` and `mavenCentral()` (and `jitpack.io` if required).
* Min/target SDK and other settings match the documented requirements.
* Note that the plugin should not require extra Android setup beyond repository configuration, but flag any obvious misconfigurations that could break the SDK.
4. **SDK initialization (Dart)**
* Locate where I call `AppstackPlugin.configure(...)` and verify:
* It is called from `main()` before `runApp` and before any events.
* Platform-specific API keys are used correctly (e.g. different keys for iOS and Android via `Platform.isIOS` checks or environment variables).
* The optional `logLevel` and `customerUserId` parameters are used appropriately for development vs production when present.
* No call still passes the deprecated `isDebug` or `endpointBaseUrl` arguments; both are no-ops in the plugin and should be dropped.
* Propose idiomatic Dart initialization code if my current setup is missing, duplicated, or fragile.
5. **Customer user ID**
* Check that the ID is set as soon as it is known, via `configure(...)` or `AppstackPlugin.setCustomerUserId(...)` (which is safe to call before or after `configure`), and that at least one event follows.
* Flag any attempt to set the ID by calling `configure` again — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored.
6. **Event tracking implementation**
* Find all uses of `AppstackPlugin.sendEvent(...)` and verify:
* Standard `EventType` values are used where appropriate (e.g. `EventType.purchase`, `EventType.signUp`, `EventType.subscribe`, `EventType.levelComplete`, etc.).
* Revenue events send `revenue` (or `price`) and `currency` in the `parameters` map.
* Custom events use `EventType.custom` with a descriptive `eventName` and, for EAC / Meta, rich attributes such as `email`, `name`, `phone_number`, and `date_of_birth`, noting that Appstack automatically encrypts these matching parameters before using them for attribution matching.
* Highlight missing or inconsistent event usage and suggest concrete `sendEvent` calls that fit my app’s flows.
7. **Platform-specific behavior & limitations**
* Validate that my code:
* Only calls `enableAppleAdsAttribution()` on iOS (guarded by `Platform.isIOS`).
* Respects documented limitations around attribution timing, official store installs, and simulator behavior.
* Initializes the SDK from `main()` before `runApp` and does not reconfigure it on every screen unnecessarily.
* Flag any code patterns that could conflict with these assumptions.
8. **Validation report & checklist**
* Produce a clear summary of:
* What is correctly implemented and safe to ship.
* What is missing, misconfigured, or risky, with specific Dart and platform file changes.
# Kotlin
Source: https://docs.appstack.tech/SDKs/kotlin
**Prefer to automate this?** The [Appstack CLI](/tooling/cli) detects your project, installs and configures the SDK, then verifies the result:
```bash theme={null}
npx appstack-cli integrate
```
Already integrated? `npx appstack-cli review` audits it without changing any files.
You are an expert Android engineer helping me integrate the Appstack Android SDK (Kotlin) into my app. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to fully wire the SDK. When I share my Gradle files and app code, you should:
1. Propose **exact Gradle changes** (repositories and `dependencies { ... }`) using the dependency coordinates below and the **current** artifact version from Maven Central.
2. Tell me exactly where to initialize the SDK (Application.onCreate) and generate idiomatic Kotlin code that matches my app's structure, using the snippets below.
3. Wire the documented configuration options (log level, customer user ID) when relevant.
4. Validate min/target SDK, Java/Gradle versions and any manifest or ProGuard/R8 needs against the requirements below, and point out gaps.
5. Summarize the full set of steps (install, configure, event tracking) so I can verify everything has been applied.
***
## Reference: Appstack Android SDK (Kotlin)
**Requirements**
* Min SDK: Android 5.0 (API 21), Target SDK: 35+, Java 17+, Gradle 8.0+ (built with Gradle 8.13 / Android Gradle Plugin 8.12)
* SDK artifact: [Maven Central](https://central.sonatype.com/artifact/tech.appstack.android-sdk/appstack-android-sdk) — use latest version.
**1. Gradle dependency**
```kotlin theme={null}
dependencies {
// Resolve the latest from Maven Central, then pin an explicit version for reproducible builds.
implementation("tech.appstack.android-sdk:appstack-android-sdk:+")
}
```
Prefer copying a **specific** version from the Maven Central page linked above once you know what you want to ship; avoid leaving `+` in production if your team requires locked versions.
**2. Initialize in Application.onCreate**
```kotlin theme={null}
import com.appstack.attribution.AppstackAttributionSdk
import com.appstack.attribution.EventType
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AppstackAttributionSdk.configure(
context = this,
apiKey = "your-android-api-key"
)
}
}
```
**3. Full configuration (optional params)**
```kotlin theme={null}
AppstackAttributionSdk.configure(
context = this,
apiKey = "your-api-key",
logLevel = LogLevel.INFO, // DEBUG, INFO (default), WARN, ERROR, NONE
customerUserId = "user_123"
)
```
* Only `context` and `apiKey` are required.
* For on-device diagnostics use `logLevel = LogLevel.DEBUG` (logcat tag `AppstackSdk`).
**4. Customer user ID**
```kotlin theme={null}
AppstackAttributionSdk.setCustomerUserId("user_123") // once the ID is known
```
**5. Event tracking**
* Simple events:
```kotlin theme={null}
AppstackAttributionSdk.sendEvent(EventType.SIGN_UP)
AppstackAttributionSdk.sendEvent(EventType.LOGIN)
```
* With parameters (e.g. revenue):
```kotlin theme={null}
AppstackAttributionSdk.sendEvent(
EventType.PURCHASE,
parameters = mapOf("revenue" to 29.99, "currency" to "USD")
)
```
* Custom events:
```kotlin theme={null}
AppstackAttributionSdk.sendEvent(
EventType.CUSTOM,
name = "user_attributes",
parameters = mapOf(
"email" to "test@example.com",
"name" to "first_name last_name",
"phone_number" to "+33060000000",
"date_of_birth" to "2026-02-01"
)
)
```
**EventType values (use standard when possible):** LOGIN, SIGN\_UP/REGISTER, PURCHASE, SUBSCRIBE, ADD\_TO\_CART, ADD\_TO\_WISHLIST, INITIATE\_CHECKOUT, START\_TRIAL, LEVEL\_START, LEVEL\_COMPLETE, TUTORIAL\_COMPLETE, SEARCH, VIEW\_ITEM, VIEW\_CONTENT, SHARE, CUSTOM.
**Revenue / EAC:** For revenue events send `revenue` or `price` (number) and `currency` (string). For better Meta matching, include when possible: `email`, `name`, `phone_number` (or `phone`/`phoneNumber`), `date_of_birth` (YYYY-MM-DD, or `birthdate`/`birthday`/`dateOfBirth`), `gender`. Appstack automatically encrypts these matching parameters before using them for attribution matching.
**Limitations:** Init must happen in Application.onCreate before any tracking. Attribution for Play Store installs; network required (events sent before the SDK is ready are buffered and replayed).
## **Repository**
Here, you will find the [Maven Central Android SDK documentation](https://central.sonatype.com/artifact/tech.appstack.android-sdk/appstack-android-sdk). Please, use the latest version of the SDK available.
Current stable release: **1.8.0**, published 2 September 2026. See the [changelog](/changelog/kotlin) for everything that changed.
## **Quickstart**
Use this path when you only need the minimum production integration:
1. Add the SDK dependency from Maven Central.
2. Copy the **Production** API key from **SDK** in Appstack.
3. Call `AppstackAttributionSdk.configure(...)` from `Application.onCreate()`.
4. Send standard events such as `EventType.LOGIN`, `EventType.SIGN_UP`, `EventType.PURCHASE`, and `EventType.SUBSCRIBE`.
5. Confirm events appear in the Appstack SDK page before enabling downstream integrations.
```kotlin theme={null}
import android.app.Application
import com.appstack.attribution.AppstackAttributionSdk
import com.appstack.attribution.EventType
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AppstackAttributionSdk.configure(
context = this,
apiKey = "your_production_api_key"
)
AppstackAttributionSdk.sendEvent(
EventType.PURCHASE,
parameters = mapOf("revenue" to 29.99, "currency" to "USD")
)
}
}
```
## **Requirements**
1. Minimum SDK: Android 5.0 (API level 21).
2. Target SDK: 35+
3. Java Version: 17+
4. Gradle: 8.0+ (the SDK is built with Gradle 8.13 and Android Gradle Plugin 8.12)
## **Initial setup**
Add the SDK dependency to your app's `build.gradle.kts`:
```kotlin theme={null}
dependencies {
// Resolve latest from Maven Central, then prefer pinning an explicit version for release builds.
implementation("tech.appstack.android-sdk:appstack-android-sdk:+")
}
```
No additional Gradle configuration is needed after adding the dependency. You still need to initialize the SDK in `Application.onCreate()` before sending events.
Follow these steps to get the API key:
1. In Appstack, from the side menu, select **SDK** and ensure you are selecting the correct application.
2. Select the **Production** environment.
3. Copy the **API key**.
Examples:
Configure the SDK in your `Application` class:
```kotlin theme={null}
import com.appstack.attribution.AppstackAttributionSdk
import com.appstack.attribution.EventType
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
AppstackAttributionSdk.configure(
context = this,
apiKey = "your-android-api-key"
)
}
}
```
Initialize the SDK with your **API key**. Must be called in `Application.onCreate()` before any other SDK methods.
Parameters:
* `context` (Context, required): Application context.
* `apiKey` (String, required): Your Appstack API key.
* `logLevel` (LogLevel, default: `LogLevel.INFO`): Console log verbosity. One of `DEBUG`, `INFO`, `WARN`, `ERROR`, `NONE`.
* `listener` (InitListener?, default: null): Optional callback invoked on initialization success or error.
* `customerUserId` (String?, default: null): Your own user identifier, attached to the event payload.
Examples:
```kotlin theme={null}
// Minimum configuration — context and the API key are the only required parameters
AppstackAttributionSdk.configure(
context = this,
apiKey = "your-api-key"
)
// With optional parameters
AppstackAttributionSdk.configure(
context = this,
apiKey = "your-api-key",
logLevel = LogLevel.INFO,
customerUserId = "user_123"
)
```
`logLevel` only controls logcat output (tag `AppstackSdk`); it does not change what the SDK sends.
The customer user ID is your own identifier for the signed-in user. Appstack attaches it to events so server-to-server events — which identify the user by this ID rather than by the install — can be joined back to the install that produced them.
Set it as soon as it is known. That can be before `configure` runs, at `configure` time via the `customerUserId` parameter, or later — most often once a login reveals it:
```kotlin theme={null}
AppstackAttributionSdk.setCustomerUserId("user_123")
```
* `customerUserId` (String): your identifier for the signed-in user. Leading and trailing whitespace is trimmed.
* Callable from any thread. It applies to every event sent from then on, including any `sendEvent` buffered during startup.
* A call made before `configure(...)` is staged and applied once the SDK initializes. If you also pass a `customerUserId` to `configure(...)`, that value wins; passing none leaves the ID you already set in place.
* The call does not send anything by itself: make sure at least one event follows, or no mapping is ever formed.
* Calling `configure(...)` again to set the ID does not work — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored. Use `setCustomerUserId` instead.
Track user actions and revenue in your activities:
```kotlin theme={null}
// Track events without parameters
AppstackAttributionSdk.sendEvent(EventType.SIGN_UP)
AppstackAttributionSdk.sendEvent(EventType.LOGIN)
// Track events with parameters (including revenue)
AppstackAttributionSdk.sendEvent(
EventType.PURCHASE,
parameters = mapOf("revenue" to 29.99, "currency" to "USD")
)
// Custom events
AppstackAttributionSdk.sendEvent(
EventType.CUSTOM,
name = "user_attributes",
parameters = mapOf(
"email" to "test@example.com",
"name" to "first_name last_name",
"phone_number" to "+33060000000",
"date_of_birth" to "2026-02-01"
)
)
```
**Available EventType values**
It is recommended to use standard events for a smoother experience.
`EventType.INSTALL` is tracked automatically on SDK initialization. Do not send it manually.
* `EventType.LOGIN` User login.
* `EventType.SIGN_UP` / `EventType.REGISTER` User registration.
* `EventType.PURCHASE` Purchase transactions.
* `EventType.SUBSCRIBE` Subscription events.
* `EventType.ADD_TO_CART`, `EventType.ADD_TO_WISHLIST`, `EventType.INITIATE_CHECKOUT` E-commerce events.
* `EventType.START_TRIAL` Trial start.
* `EventType.LEVEL_START`/ `EventType.LEVEL_COMPLETE` Game progression.
* `EventType.TUTORIAL_COMPLETE`, `EventType.SEARCH`, `EventType.VIEW_ITEM`, `EventType.VIEW_CONTENT`, `EventType.SHARE` Engagement events.
* `EventType.CUSTOM` For any other custom events.
Tracks custom events with optional parameters:
* `event` Event type from EventType enum (required).
* `name` Event name for custom events (optional, required when event is CUSTOM).
* `parameters` - Optional map of parameters (e.g., `mapOf("revenue" to 29.99, "currency" to "USD")`).
**Enhanced app campaigns**
When running enhanced app campaigns (EACs), it is highly recommended to send multiple parameters with the in-app event to improve matching quality.
For any event that represents revenue, we recommend sending:
1. `revenue` or `price` (number).
2. `currency` (string, e.g. `EUR`, `USD`).
```kotlin theme={null}
AppstackAttributionSdk.sendEvent(
EventType.PURCHASE,
parameters = mapOf("revenue" to 4.99, "currency" to "EUR")
)
```
To improve matching quality on Meta, send events including the following parameters if you can fulfill them. Appstack automatically encrypts these matching parameters before using them for attribution matching.
1. `email`.
2. `name` (first + last name in the same field).
3. `phone_number` — also accepted as `phone` or `phoneNumber`.
4. `date_of_birth` (recommended format: `YYYY-MM-DD`) — also accepted as `birthdate`, `birthday`, or `dateOfBirth`.
5. `gender`.
## **Appstack ID and attribution params**
After `configure`, you can read the Appstack user ID and the attribution map for partner integrations (for example Superwall, RevenueCat).
```kotlin theme={null}
val appstackId = AppstackAttributionSdk.getAppstackId()
// Inside a coroutine: waits for the initial attribution match to finish
val attributionParams = AppstackAttributionSdk.awaitAttributionParams()
```
* **`getAppstackId()`** — Appstack user identifier when a partner expects `$appstackId` or similar.
* **`awaitAttributionParams()`** — `suspend` function that waits for the initial attribution match, then returns the attribution payload to forward to partners. Prefer this over a fixed delay after launch.
* **`getAttributionParams()`** — Non-suspending variant that returns whatever is cached right now. Called immediately after `configure(...)` it can still be empty, because the match runs asynchronously.
Both accept an optional raw Play Install Referrer string, which is parsed when no matched attribution is cached yet.
A matched install returns something like:
```kotlin theme={null}
// {
// "appstack_adnetwork" to "meta", // "google", "meta" or "tiktok"
// "appstack_campaign" to "summer_sale",
// "appstack_adset" to "lookalike_1pct",
// "appstack_ad" to "video_15s",
// "appstack_id" to "9f1c8b64-..."
// }
```
Android also returns `deeplink_id` and `gclid`, and the raw per-network params the click carried.
`appstack_adnetwork` is `google`, `meta` or `tiktok`. It is absent when the click carried no recognised network param — for a standard link, read `media_source` instead. On Google the campaign, ad set and ad values are numeric IDs rather than names. `appstack_id` comes back on every result, including organic ones, so use a non-empty `deeplink_id` as the match check rather than testing whether the map is empty.
The `appstack_match_status` key that the iOS SDK adds is **not reported on Android yet**.
## **Development setup**
If you want to test the SDK against the Appstack development environment before shipping, follow these extra steps:
1. In Appstack, from the side menu, select **SDK**, switch to the **Development** environment, and copy the **Development API key**. This key is separate from your production key.
2. In your `configure(...)` call, use the development key and raise the log level:
```kotlin theme={null}
AppstackAttributionSdk.configure(
context = this,
apiKey = "your_development_api_key",
logLevel = LogLevel.DEBUG
)
```
The API key is what selects the environment — there is no flag to set. A development key routes to the development environment, a production key to production; both use the same endpoint.
**On-device diagnostics**
`LogLevel.DEBUG` prints the SDK's initialization, attribution, and event traffic to logcat under the tag `AppstackSdk`:
```text theme={null}
adb logcat -s AppstackSdk
```
Keep the debug log level behind a build-type check so release builds stay quiet:
```kotlin theme={null}
AppstackAttributionSdk.configure(
context = this,
apiKey = "your-api-key",
logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.INFO
)
```
## **Security and privacy**
* Never commit API keys to version control.
* Use separate production and development keys, and make sure release builds ship the production key.
* Keep `LogLevel.DEBUG` behind a `BuildConfig.DEBUG` guard so release builds stay quiet.
* Do not put personally identifiable information in event names.
* Only send matching parameters such as `email`, `name`, `phone_number`, and `date_of_birth` when your app has the right consent and compliance basis. Appstack automatically encrypts these fields before using them for attribution matching.
* Prefer standard event types for common flows so event mapping remains consistent across Appstack and ad integrations.
## **Limitations**
### **Platform constraints**
* Android 5.0+ required (API level 21).
* Attribution only works for Play Store installations.
* Network connectivity is required at the moment an event is sent. There is no durable offline queue: an event tracked while the device is offline is dropped, not stored for later. Transient failures on a request that did go out (network errors, HTTP 429/500/502/503/504) are retried in-flight with exponential backoff.
### **Event tracking**
* The SDK must be initialized in `Application.onCreate()` before tracking calls.
* Custom event names should be descriptive and consistent.
* Events sent before the SDK is ready are buffered in memory and replayed once initialization completes.
* `INSTALL` is tracked automatically on SDK initialization. Do not send it manually — the SDK drops such calls.
* Revenue events should include `revenue` or `price` and `currency`.
## **Troubleshooting**
### **Configuration fails**
* Confirm the API key was copied from the correct app and environment in Appstack.
* Confirm the SDK dependency resolves from Maven Central.
* Confirm your `Application` class is registered in `AndroidManifest.xml`.
* Confirm your app declares the `INTERNET` permission in `AndroidManifest.xml` — without it `configure(...)` reports an error through `InitListener.onError()` and `getLastInitError()`.
### **Events do not appear**
* Confirm `configure(...)` runs in `Application.onCreate()` before the first `sendEvent(...)` call.
* Confirm the device has network connectivity.
* Confirm the app was installed from the Play Store when testing attribution.
* Check that revenue events include a numeric `revenue` or `price` value and a valid `currency`.
* Allow a few minutes for events to appear in the dashboard.
### **No SDK logs appear**
* Confirm `configure(...)` passes `logLevel = LogLevel.DEBUG`.
* Filter logcat on the SDK tag: `adb logcat -s AppstackSdk`.
## **Superwall**
To start using the Superwall integration, [click here](/Integrations/superwall) to see the correct SDK documentation.
## **Verification checklist**
* Dependency installed with `tech.appstack.android-sdk:appstack-android-sdk`.
* App meets min SDK 21, target SDK 35+, Java 17+, and Gradle 8.0+ requirements.
* Maven Central is available to Gradle.
* Production API key is used in release builds.
* Development API key is used only in development builds.
* `Application` class is registered in `AndroidManifest.xml`.
* `configure(...)` runs once from `Application.onCreate()`.
* The customer user ID is set — via `configure(...)` or `setCustomerUserId(...)` — as soon as it is known, and at least one event follows it.
* `INSTALL` is not sent manually.
* Login, signup, purchase, subscription, and other key app events use standard event types where possible.
* Revenue events include `revenue` or `price` and `currency`.
* Custom events use `EventType.CUSTOM` with a descriptive `name`.
* Appstack ID and attribution params are available before wiring partner integrations.
* Events are visible in the Appstack SDK page before launch.
## **Support**
For questions or issues:
1. Check the [GitHub Repository](https://github.com/appstack-tech/appstack-android-sdk).
2. Contact our support team at [support@appstack.tech](mailto:support@appstack.tech)
3. Open an issue in the repository.
You are an expert Android engineer reviewing my existing Appstack Android SDK (Kotlin) integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Your goal is to **validate that my integration fully matches the official Kotlin SDK documentation** and identify any missing or incorrect steps. When I paste this prompt and share my project files, you should:
1. **Gradle & environment**
* Inspect my Gradle files (project and app module) to confirm:
* The Appstack SDK dependency is present with the correct Maven coordinates (`tech.appstack.android-sdk:appstack-android-sdk`) and a valid version.
* Minimum SDK is at least API 21, target SDK is 35+, Java is 17+, and Gradle is 8.0+.
* Required repositories (including Maven Central) are configured so the SDK can resolve.
* Call out any mismatches or improvements needed.
2. **SDK initialization**
* Locate my `Application` class and verify:
* It is registered in the `AndroidManifest.xml`.
* `AppstackAttributionSdk.configure(...)` is called in `Application.onCreate()` before any event tracking.
* The call passes a valid `context` and `apiKey`, and uses the optional `logLevel`, `listener`, and `customerUserId` parameters appropriately when present.
* No call still passes the deprecated `isDebug` or `endpointBaseUrl` arguments; both are ignored by the SDK and should be dropped.
* Suggest exact code changes if initialization is missing, in the wrong place, or misconfigured.
3. **Customer user ID**
* Check that the ID is set as soon as it is known, via `configure(...)` or `setCustomerUserId(...)` (which is safe to call before or after `configure`), and that at least one event follows.
* Flag any attempt to set the ID by calling `configure` again — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored.
4. **Logging configuration**
* Check whether verbose logging is limited to debug builds, for example `logLevel = if (BuildConfig.DEBUG) LogLevel.DEBUG else LogLevel.INFO` in `configure(...)`.
* Warn if `LogLevel.DEBUG` is left enabled for release builds and propose safe patterns.
* Flag any leftover `showDebugOverlay(...)` call: the debug overlay was removed from the SDK and the call no longer exists.
5. **Event tracking implementation**
* Find all uses of `AppstackAttributionSdk.sendEvent(...)` and verify:
* Standard `EventType` values are used where appropriate (e.g. `SIGN_UP`, `LOGIN`, `PURCHASE`, `SUBSCRIBE`, `ADD_TO_CART`, `ADD_TO_WISHLIST`, `INITIATE_CHECKOUT`, `START_TRIAL`, `LEVEL_START`, `LEVEL_COMPLETE`, `TUTORIAL_COMPLETE`, `SEARCH`, `VIEW_ITEM`, `VIEW_CONTENT`, `SHARE`).
* Revenue-related events (such as `PURCHASE`) send `revenue` or `price` and `currency` parameters.
* Custom events use `EventType.CUSTOM` with a meaningful `name` and, when possible, parameters such as `email`, `name`, `phone_number`, and `date_of_birth` to support enhanced app campaigns, noting that Appstack automatically encrypts these matching parameters before using them for attribution matching.
* Highlight any missing or inconsistent events and propose concrete event calls that fit my app’s structure and flows.
6. **Limitations & platform assumptions**
* Confirm that my integration respects the documented limitations:
* App supports Android 5.0+ (API 21+).
* I understand attribution works for Play Store installs, and that events tracked while the device is offline are dropped rather than queued for later — so I should not rely on the SDK to deliver events recorded without connectivity.
* Flag any edge cases in my setup that might conflict with these assumptions.
7. **Validation report & checklist**
* Produce a clear report summarizing:
* What is already correctly implemented and safe to ship.
* What is missing, misconfigured, or risky, with specific file names, functions, and code snippets to change.
* End with a **checklist of verification steps** (Gradle, Application init, log level, key event tracking patterns) that I can tick off to confirm the integration fully matches the Kotlin SDK documentation.
# React Native
Source: https://docs.appstack.tech/SDKs/react-native
**Prefer to automate this?** The [Appstack CLI](/tooling/cli) detects your project, installs and configures the SDK, then verifies the result:
```bash theme={null}
npx appstack-cli integrate
```
Already integrated? `npx appstack-cli review` audits it without changing any files.
You are an expert React Native engineer helping me integrate the Appstack React Native SDK into my app. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to fully wire the SDK. When I paste this prompt and share my project files, you should:
1. Provide the exact `npm`/`yarn` commands and any native steps (`pod install`, Gradle changes) required by the docs, using the installation details below.
2. Show precisely where to call `configure` in my React Native app startup code, producing ready-to-paste code based on the snippets below.
3. Check that environment-specific API keys (iOS vs Android) and platform conditionals are set up correctly.
4. List the complete sequence of steps you applied so I can confirm that every requirement from the documentation has been implemented.
***
## Reference: Appstack React Native SDK
**Requirements**
* iOS: 15.0+, Xcode 14.0+
* Android: Min SDK 21, Target SDK 35+, Java 17+
* React Native: 0.72.0+
* Node.js: 16.0+
**Installation**
```bash theme={null}
npm install react-native-appstack-sdk
cd ios && pod install # Only needed for iOS
```
(Install the **latest** published version from npm unless your project requires a specific range.)
**Android configuration**
* No extra Android setup is required beyond installing the package. You still need to call `configure` before using SDK methods.
**Quickstart example**
```javascript theme={null}
import { useEffect } from 'react';
import { Platform } from 'react-native';
import AppstackSDK, { EventType } from 'react-native-appstack-sdk';
const App = () => {
useEffect(() => {
const init = async () => {
const apiKey = Platform.OS === 'ios'
? process.env.APPSTACK_IOS_API_KEY
: process.env.APPSTACK_ANDROID_API_KEY;
await AppstackSDK.configure(apiKey);
// Request tracking permission and enable Apple Ads Attribution
if (Platform.OS === 'ios') {
await AppstackSDK.enableAppleAdsAttribution();
}
};
init();
}, []);
const trackPurchase = () => {
AppstackSDK.sendEvent(EventType.PURCHASE, { revenue: 29.99, currency: 'USD' });
};
// ... your app
};
```
**Configuration**
```javascript theme={null}
const success = await AppstackSDK.configure('your-api-key-here');
if (!success) {
console.error('SDK configuration failed');
}
```
**Customer user ID**
```javascript theme={null}
await AppstackSDK.setCustomerUserId('user_123'); // once the ID is known
```
**Sending events**
```javascript theme={null}
// Track events without parameters
await AppstackSDK.sendEvent(EventType.LOGIN);
await AppstackSDK.sendEvent(EventType.SIGN_UP);
// Track events with parameters (including revenue)
await AppstackSDK.sendEvent(
EventType.PURCHASE, { revenue: 29.99, currency: 'USD' }
);
await AppstackSDK.sendEvent(
EventType.SUBSCRIBE, { revenue: 9.99, plan: 'monthly' }
);
// Custom events: pass the name directly
await AppstackSDK.sendEvent('user_attributes', {
email: 'test@example.com',
name: 'John Doe',
phone_number: '+33060000000',
date_of_birth: '2026-02-01',
});
```
**EventType values (recommended standard events):**
* Authentication: `LOGIN`, `SIGN_UP`, `REGISTER`
* Monetization: `PURCHASE`, `ADD_TO_CART`, `ADD_TO_WISHLIST`, `INITIATE_CHECKOUT`, `START_TRIAL`, `SUBSCRIBE`
* Games: `LEVEL_START`, `LEVEL_COMPLETE`
* Engagement: `TUTORIAL_COMPLETE`, `SEARCH`, `VIEW_ITEM`, `VIEW_CONTENT`, `SHARE`
There is no `CUSTOM` value: to send a custom event, pass its name as the first argument.
**`sendEvent` parameters**
* `event`: a standard `EventType` (recommended; its string name also works, case-insensitively), or any other string to send a custom event by that name
* `parameters`: optional object (e.g. `{ revenue: 29.99, currency: 'USD' }`). Keys valued `null` or `undefined` are stripped
`sendEvent` resolves `void`: it confirms the call reached native, not that the event was delivered.
**Enhanced app campaigns**
* For revenue events, send:
* `revenue` or `price` (number)
* `currency` (e.g. `EUR`, `USD`)
* To improve Meta matching, include when possible:
* `email`
* `name` (first + last name)
* `phone_number` (also accepted as `phone` or `phoneNumber`)
* `date_of_birth` (`YYYY-MM-DD`; also accepted as `birthdate`, `birthday`, or `dateOfBirth`)
* `gender`
* Appstack encrypts these matching parameters on the device before they are sent (iOS 17+ and Android; on iOS 15–16 they are encrypted server-side instead). No code change is required.
## **Repository**
Here, you will find the [npmjs.org react-native-appstack-sdk documentation](https://www.npmjs.com/package/react-native-appstack-sdk). Please use the latest available version of the SDK.
Current stable release: **3.2.0**, published 4 September 2026. See the [changelog](/changelog/react-native) for everything that changed.
## **Quickstart**
Use this path when you only need the minimum production integration:
1. Install `react-native-appstack-sdk`.
2. Run `cd ios && pod install` for iOS projects.
3. Copy the **Production** API key from **SDK** in Appstack.
4. Call `AppstackSDK.configure(...)` from app startup code.
5. Send standard events such as `'LOGIN'`, `'SIGN_UP'`, `'PURCHASE'`, and `'SUBSCRIBE'`.
6. Confirm events appear in the Appstack SDK page before enabling downstream integrations.
```javascript theme={null}
import { Platform } from 'react-native';
import AppstackSDK, { EventType } from 'react-native-appstack-sdk';
const apiKey = Platform.OS === 'ios'
? process.env.APPSTACK_IOS_API_KEY
: process.env.APPSTACK_ANDROID_API_KEY;
await AppstackSDK.configure(apiKey);
await AppstackSDK.sendEvent(
EventType.PURCHASE,
{ revenue: 29.99, currency: 'USD' }
);
```
## **Migrating from 2.x**
The 3.0.0 release changed the two calls every integration makes. The public surface is otherwise the same, and the JavaScript API is unchanged by the TurboModule work underneath.
A 2.x-style call to either method now throws an error naming its replacement, rather than silently misbehaving. Both changes are mechanical, but neither is caught by a type check alone if you call from plain JavaScript.
**`configure` takes an options object**
The positional signature, deprecated in 2.6.0, is gone. `isDebug` and `endpointBaseUrl` are removed outright: neither was ever forwarded to the native SDKs, so nothing is lost by dropping them.
```javascript theme={null}
// 2.x
await AppstackSDK.configure(apiKey, false, undefined, 0, 'user_123');
// 3.x
await AppstackSDK.configure(apiKey, { logLevel: 0, customerUserId: 'user_123' });
```
**`sendEvent` takes two arguments**
The middle `eventName` argument is gone, and with it `EventType.CUSTOM`. Pass a standard `EventType` for a standard event, or your own name for a custom one:
```javascript theme={null}
// 2.x // 3.x
sendEvent('PURCHASE', null, { revenue: 4.99 }) -> sendEvent(EventType.PURCHASE, { revenue: 4.99 })
sendEvent('CUSTOM', 'user_attributes', params) -> sendEvent('user_attributes', params)
sendEvent('CUSTOM', 'APP_OPENED') -> sendEvent('APP_OPENED')
```
**Other changes to check**
* `sendEvent` resolves `void` instead of `true`. The old `true` only ever meant "the call reached native" and was returned even when the event was then dropped. Stop branching on the result; `await` still works unchanged.
* `logLevel` must be `0`, `1`, `2` or `3`. A fractional value such as `1.5` previously passed validation and was silently truncated.
* Parameter values of `null` or `undefined` are stripped before the call reaches native, so both platforms observe the same map. `0`, `false` and `''` are real values and are preserved.
* `parameters` is typed as `Record`. A `Date`, a class instance or a function is now a compile error rather than a value mangled in transit.
* If you called `new NativeEventEmitter(NativeModules.AppstackReactNative)` on the legacy architecture, remove it. The iOS module no longer subclasses `RCTEventEmitter`; it never emitted any events.
* A manual `sendEvent('INSTALL')` is dropped before reaching native, logging an error and resolving normally. The same applies to the SDK's internal lifecycle events, which are not part of the public `EventType` API.
Older 2.x guidance remains available on the [2.x page](/SDKs/react-native/v2), selectable from the version dropdown at the top of this page.
## **Requirements**
### **iOS**
* **iOS version:** 15.0+
* **Xcode:** 14.0+
* **React Native:** 0.72.0+
### **Android**
* **Minimum SDK:** Android 5.0 (API level 21).
* **Target SDK:** 35+
* **Java Version:** 17+
### **General**
* **Node.js:** 16.0+
* **React Native architecture:** both are supported. Since 3.0.0 the SDK is a real TurboModule generated by React Native's codegen, with the legacy architecture still served by the same package and no configuration to choose between them.
On iOS, React Native 0.87 Swift Package Manager support is **experimental** as of 3.2.0. CocoaPods support is unchanged and remains the supported path.
## **Initial setup**
```text theme={null}
npm install react-native-appstack-sdk
cd ios && pod install # Only needed for iOS
```
Follow these steps to get the API key:
1. In Appstack, from the side menu, select **SDK** and ensure you are selecting the correct application.
2. Select the **Production** environment.
3. Copy the **API key**.
Examples:
```javascript theme={null}
import { useEffect } from 'react';
import { Platform } from 'react-native';
import AppstackSDK, { EventType } from 'react-native-appstack-sdk';
const App = () => {
useEffect(() => {
const init = async () => {
const apiKey = Platform.OS === 'ios'
? process.env.APPSTACK_IOS_API_KEY
: process.env.APPSTACK_ANDROID_API_KEY;
await AppstackSDK.configure(apiKey);
// Request tracking permission and enable Apple Ads Attribution
if (Platform.OS === 'ios') {
await AppstackSDK.enableAppleAdsAttribution();
}
};
init();
}, []);
const trackPurchase = () => {
AppstackSDK.sendEvent(EventType.PURCHASE, { revenue: 29.99, currency: 'USD' });
};
// ... your app
};
```
Initializes the SDK with your API key. Call this from app startup code before any other SDK methods.
Parameters:
* `apiKey` (string, required): Your platform-specific API key from the Appstack dashboard.
* `options` (object, optional): `logLevel` and `customerUserId`. See the note below.
Returns: A promise that resolves to `true` if configuration was successful.
Example:
```javascript theme={null}
const success = await AppstackSDK.configure('your-api-key-here');
if (!success) {
console.error('SDK configuration failed');
}
```
`configure` takes an optional second argument, an options object with `logLevel` (one of `0` DEBUG, `1` INFO, `2` WARN, `3` ERROR; default `1`) and `customerUserId`:
```javascript theme={null}
await AppstackSDK.configure('your-api-key-here', {
logLevel: 0,
customerUserId: 'user_123',
});
```
Only `0`, `1`, `2` and `3` are accepted — a fractional `logLevel` is rejected.
The customer user ID is your own identifier for the signed-in user. Appstack attaches it to events so server-to-server events — which identify the user by this ID rather than by the install — can be joined back to the install that produced them.
Set it as soon as it is known. That can be before `configure` runs, at `configure` time via the `customerUserId` parameter, or later — most often once a login reveals it:
```javascript theme={null}
await AppstackSDK.setCustomerUserId('user_123');
```
* `customerUserId` (string): your identifier for the signed-in user. Leading and trailing whitespace is trimmed.
* Returns a promise that resolves once the native SDK has accepted the ID, and rejects if the native call fails.
* Safe to call at any time. It applies to every event sent from then on, including ones the native SDK has buffered but not yet flushed.
* If you also pass a `customerUserId` to `configure(...)`, that value wins; passing none leaves the ID you already set in place.
* The call does not send anything by itself: make sure at least one event follows, or no mapping is ever formed.
* Calling `configure(...)` again to set the ID does not work — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored. Use `setCustomerUserId` instead.
Track user actions and revenue in your activities:
```javascript theme={null}
// Track events without parameters
await AppstackSDK.sendEvent(EventType.LOGIN);
await AppstackSDK.sendEvent(EventType.SIGN_UP);
// Track events with parameters (including revenue)
await AppstackSDK.sendEvent(
EventType.PURCHASE, { revenue: 29.99, currency: 'USD' }
);
await AppstackSDK.sendEvent(
EventType.SUBSCRIBE, { revenue: 9.99, plan: 'monthly' }
);
// Custom events: pass the name directly
await AppstackSDK.sendEvent('user_attributes', {
email: "test@example.com",
name: "John Doe",
phone_number: "+33060000000",
date_of_birth: "2026-02-01"
});
```
**Available EventType values**
It is recommended to use standard events for a smoother experience.
`INSTALL` is tracked automatically on SDK initialization. Do not send it manually: a manual `INSTALL` is dropped before it reaches native, logging an error and resolving normally, so it cannot inflate your install count.
* `LOGIN`/ `SIGN_UP `/ `REGISTER` Authentication.
* `PURCHASE`/ `ADD_TO_CART`/ `ADD_TO_WISHLIST`/ `INITIATE_CHECKOUT`/ `START_TRIAL`/ `SUBSCRIBE` Monetization.
* `LEVEL_START`/ `LEVEL_COMPLETE` Game progression.
* `TUTORIAL_COMPLETE`/`SEARCH`/ `VIEW_ITEM`/ `VIEW_CONTENT`/ `SHARE` Engagement.
For an application-specific event, pass its name as the first argument instead of an `EventType`. There is no `CUSTOM` value in 3.x.
Parameters:
* `event` A standard `EventType` (recommended; its string name also works, case-insensitively), or any other string to send a custom event by that name.
* `parameters` Optional parameters object (e.g., `{ revenue: 29.99, currency: 'USD' }`). Keys valued `null` or `undefined` are stripped, so both platforms observe the same map. Values must be JSON-safe: a `Date`, a class instance or a function is a type error.
Returns: A promise that resolves to `void` once the call reaches native. It does **not** confirm delivery — the native SDKs still drop events when disabled, offline, or buffering.
**Enhanced app campaigns**
When running enhanced app campaigns (EACs), it is highly recommended to send multiple parameters with the in-app event to improve matching quality.
For any event that represents revenue, we recommend sending:
1. `revenue` or `price` (number).
2. `currency`(string, e.g. `EUR`, `USD`).
```javascript theme={null}
await AppstackSDK.sendEvent(
EventType.PURCHASE,
{ revenue: 4.99, currency: 'EUR' }
);
```
To improve matching quality on Meta, send events including the following parameters if you can fulfill them. Appstack encrypts these matching parameters on the device before they are sent (iOS 17+ and Android; on iOS 15–16 they are encrypted server-side instead), so no code change is required. Names that must stay readable — among them `currency` and `revenue` — are excluded, and revenue reporting is unaffected.
1. `email`.
2. `name` (first + last name in the same field).
3. `phone_number` — also accepted as `phone` or `phoneNumber`.
4. `date_of_birth` (recommended format: `YYYY-MM-DD`) — also accepted as `birthdate`, `birthday`, or `dateOfBirth`.
5. `gender`.
## **Appstack ID and attribution params**
After `configure`, you can read the Appstack user ID and the attribution object for partner integrations (for example Superwall, RevenueCat).
```javascript theme={null}
const appstackId = await AppstackSDK.getAppstackId();
const attributionParams = (await AppstackSDK.getAttributionParams()) ?? {};
```
* **`getAppstackId()`** — Appstack user identifier when a partner expects `$appstackId` or similar.
* **`getAttributionParams()`** — Attribution payload to forward to partners. It waits for the attribution match to finish, so you do not need a fixed delay after launch.
A matched install returns something like:
```json theme={null}
{
"appstack_adnetwork": "meta",
"appstack_campaign": "summer_sale",
"appstack_adset": "lookalike_1pct",
"appstack_ad": "video_15s",
"appstack_id": "9f1c8b64-...",
"appstack_match_status": "matched"
}
```
`appstack_adnetwork` is `google`, `meta` or `tiktok`. It is absent when the click carried no recognised network param — for a standard link, read `media_source` instead. On Google the campaign, ad set and ad values are numeric IDs rather than names. `appstack_id` comes back on every result, including organic ones, so read `appstack_match_status` rather than testing whether the result is empty.
`appstack_match_status` tells you whether the match resolved:
| Value | Meaning |
| :------------------ | :-------------------------------------------------------------------------------------------------------------- |
| `matched` | Attributed, with campaign params included. |
| `matched_no_params` | Attributed, but the clicked link carried no tracking params. |
| `organic` | Appstack confirmed there is no attribution for this device. |
| `skipped` | Not an attributable install, so no request was made. |
| `failed` | The request did not complete (offline, timeout, server error) — read again later. |
| `not_configured` | Read before `configure(...)` completed, or the SDK is disabled (invalid API key, or a server-side kill switch). |
It is added on iOS from SDK `2.6.0`+. `failed` is always worth reading again — the SDK retries in-session and on the next launch. `not_configured` is worth re-reading only when it was read before `configure(...)` finished; a disabled SDK never resolves. The other four are final for the life of the install.
Check for the key rather than assuming it is there — **Android does not report it yet**.
## **Development setup**
### **Environment-based configuration**
Set up different API keys for different environments:
```javascript theme={null}
// .env.development
APPSTACK_IOS_API_KEY=your_ios_dev_key
APPSTACK_ANDROID_API_KEY=your_android_dev_key
// .env.production
APPSTACK_IOS_API_KEY=your_ios_prod_key
APPSTACK_ANDROID_API_KEY=your_android_prod_key
```
```javascript theme={null}
import Config from 'react-native-config';
const apiKey = Platform.OS === 'ios'
? Config.APPSTACK_IOS_API_KEY
: Config.APPSTACK_ANDROID_API_KEY;
await AppstackSDK.configure(apiKey);
```
## **Platform-specific considerations**
### **iOS**
**Apple Ads attribution:**
* Only works on iOS 15.0+ (below that the call is a no-op)
* Requires app installation from App Store or TestFlight.
* Attribution data appears within 24-48 hours.
* User consent may be required for detailed attribution (iOS 14.5+).
```javascript theme={null}
import { Platform } from 'react-native';
const iosVersion = Number.parseFloat(String(Platform.Version));
if (Platform.OS === 'ios' && iosVersion >= 15.0) {
await AppstackSDK.enableAppleAdsAttribution();
}
```
### **Android**
**Play Store attribution:**
* Install referrer data collected automatically.
* Attribution available immediately for Play Store installs.
* Works with Android 5.0+ (API level 21).
### **Cross-platform best practices**
```javascript theme={null}
const initializeSDK = async () => {
const apiKey = Platform.select({
ios: process.env.APPSTACK_IOS_API_KEY,
android: process.env.APPSTACK_ANDROID_API_KEY,
default: process.env.APPSTACK_DEFAULT_API_KEY
});
if (!apiKey) {
console.error('Appstack API key not configured');
return;
}
const configured = await AppstackSDK.configure(apiKey);
if (configured && Platform.OS === 'ios') {
await AppstackSDK.enableAppleAdsAttribution();
}
};
```
## **Security and privacy**
### **API key protection**
* Never commit API keys to version control.
* Use environment variables or secure configuration.
* Use different keys for development and production.
* Do not put personally identifiable information in event names.
* Only send matching parameters such as `email`, `name`, `phone_number`, and `date_of_birth` when your app has the right consent and compliance basis. Appstack encrypts these fields on the device before they are sent.
* Prefer standard event types for common flows so event mapping remains consistent across Appstack and ad integrations.
```javascript theme={null}
// ✅ Good - Use environment variables
const apiKey = Config.APPSTACK_API_KEY;
// ❌ Avoid - Hardcoded keys
const apiKey = "ak_live_1234567890abcdef"; // DON'T DO THIS
```
### **Data privacy**
* Event names and revenue data are transmitted securely over HTTPS.
* No personally identifiable information (PII) should be included in event names.
* The SDK does not collect device identifiers beyond what's required for attribution.
## **Limitations**
### **Attribution timing**
* **iOS:** Apple Ads attribution data appears within 24-48 hours after install.
* **Android:** Install referrer data available immediately for Play Store installs.
* Attribution only available for apps installed from official stores.
### **Platform constraints**
* **iOS:** Requires iOS 15.0+
* **Android:** Minimum API level 21 (Android 5.0).
* **React Native:** 0.72.0+
* Some Apple Ads features may not work in development/simulator environments.
### **Event tracking**
* Standard event names are resolved case-insensitively: `'purchase'` and `'PURCHASE'` both send the standard `PURCHASE`. Custom event names are recorded exactly as given, so their casing is preserved.
* Parameters are passed as an object and can include any key-value pairs.
* For revenue events, always pass a `revenue` (or `price`) and a `currency` parameter.
* The SDK must be initialized from app startup code before any tracking calls.
* Network connectivity required for event transmission (events are queued offline).
### **Technical limitations**
* `enableAppleAdsAttribution()` only works on iOS and will do nothing on Android.
* `sendEvent` resolves `void`; nothing about delivery is observable from JavaScript. Do not branch on its result.
* `isDebug` and `endpointBaseUrl` were removed in 3.0.0 along with the positional `configure` signature. A 2.x-style call now throws an error naming the replacement.
## **Troubleshooting**
### **Common Issues**
**Configuration fails:**
```javascript theme={null}
// Check if API key is valid
const success = await AppstackSDK.configure(apiKey);
if (!success) {
console.error('Invalid API key or network issue');
}
```
**Events not appearing in dashboard:**
* Check network connectivity.
* Verify the API key is correct for the platform.
* Events may take a few minutes to appear in the dashboard.
**iOS Attribution not working:**
* Ensure iOS version is 15.0+
* Verify the app is installed from the App Store or TestFlight.
* Allow 24-48 hours for attribution data to appear.
## **Verification checklist**
* `react-native-appstack-sdk` is installed.
* iOS dependencies are installed with `cd ios && pod install`.
* App meets React Native 0.72.0+, Node 16.0+, iOS 15.0+, Android min SDK 21, target SDK 35+, and Java 17+ requirements.
* Platform-specific API keys are configured for iOS and Android.
* Production API keys are used in production builds.
* Development API keys are used only in development builds.
* `configure(...)` runs once from app startup code before any events.
* The customer user ID is set — via `configure(...)` or `setCustomerUserId(...)` — as soon as it is known, and at least one event follows it.
* iOS-only Apple Ads calls are guarded with `Platform.OS === 'ios'`.
* `INSTALL` is not sent manually.
* Login, signup, purchase, subscription, and other key app events use standard event names where possible.
* Revenue events include `revenue` or `price` and `currency`.
* Custom events pass the event name directly as the first argument to `sendEvent`.
* Appstack ID and attribution params are available before wiring partner integrations.
* Events are visible in the Appstack SDK page before launch.
## **Superwall**
To start using the Superwall integration, [click here](/Integrations/superwall) to see the correct SDK documentation.
## **Apple Ads**
To start using the Apple Ads integration, [click here](/Integrations/apple-ads) to see the correct SDK documentation.
## **Support**
For questions or issues:
1. Check the [GitHub Repository](https://github.com/appstack-tech/react-native-appstack-sdk).
2. Contact our support team at [support@appstack.tech](mailto:support@appstack.tech)
3. Open an issue in the repository.
You are an expert React Native engineer reviewing my existing Appstack React Native SDK integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Your goal is to **validate that my integration fully matches the official React Native SDK documentation** and identify any missing or incorrect steps. When I paste this prompt and share my project files, you should:
1. **Installation & environment**
* Inspect my project for:
* The `react-native-appstack-sdk` dependency is present in `package.json` / the lockfile at a **current** release (not necessarily an old pinned version—compare with npm).
* Required environment versions: React Native 0.72.0+, Node 16+, iOS 15.0+, Android min SDK 21, target SDK 35+, Java 17+.
* `pod install` having been run for iOS (Podfile and Pods state).
* Call out any mismatches and give exact `npm`/`yarn` and `pod` commands or Gradle tweaks needed.
2. **iOS configuration (Apple Ads)**
* Verify that Apple Ads attribution is only enabled on iOS and, if present, is called via `AppstackSDK.enableAppleAdsAttribution()` in a sensible place (after initialization and any ATT permission logic).
3. **Android configuration**
* Confirm that:
* No extra Android configuration is required beyond installing the package (per docs), but that my Gradle files still meet the documented requirements (minSdk 21+, target 35+, Java 17+, `mavenCentral()` etc.).
* Flag any obvious Android config issues that could prevent the SDK from initializing or sending events.
4. **SDK initialization**
* Locate where I call `AppstackSDK.configure(apiKey)` and verify:
* It runs once from app startup code before any events are sent.
* It uses the 3.x options-object form, `configure(apiKey, { logLevel, customerUserId })`. Flag any positional 2.x call such as `configure(apiKey, false, undefined, 0, 'user_123')` — it now throws — and rewrite it.
* Platform-specific API keys are used correctly (e.g. `APPSTACK_IOS_API_KEY` vs `APPSTACK_ANDROID_API_KEY` via `Platform` or config libraries).
* The success value is checked (where appropriate) and configuration failures are handled or logged.
* Propose a clean, idiomatic initialization pattern if mine is missing, duplicated, or fragile.
5. **Customer user ID**
* Check that the ID is set as soon as it is known, via `configure(...)` or `AppstackSDK.setCustomerUserId(...)` (which is safe to call before or after `configure`), and that at least one event follows.
* Flag any attempt to set the ID by calling `configure` again — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored.
6. **Event tracking implementation**
* Flag every three-argument `sendEvent(type, name, parameters)` call and every use of `'CUSTOM'` or `EventType.CUSTOM`: both were removed in 3.0.0 and now reject. Rewrite them as `sendEvent(EventType.X, parameters)` for standard events, or `sendEvent('my_event_name', parameters)` for custom ones.
* Flag any code branching on `sendEvent`'s return value: it resolves `void` in 3.x, not `true`.
* Find all uses of `AppstackSDK.sendEvent(...)` and check:
* That standard event types are used where possible (`'LOGIN'`, `'SIGN_UP'`, `'REGISTER'`, `'PURCHASE'`, `'SUBSCRIBE'`, `'ADD_TO_CART'`, `'ADD_TO_WISHLIST'`, `'INITIATE_CHECKOUT'`, `'START_TRIAL'`, `'LEVEL_START'`, `'LEVEL_COMPLETE'`, `'TUTORIAL_COMPLETE'`, `'SEARCH'`, `'VIEW_ITEM'`, `'VIEW_CONTENT'`, `'SHARE'`).
* Revenue events send `revenue` (or `price`) and `currency` in the `parameters` object.
* Custom events pass the event name directly as `sendEvent`'s first argument (there is no `CUSTOM` type in 3.x) and, for EAC / Meta, carry rich parameters such as `email`, `name`, `phone_number`, and `date_of_birth`, noting that Appstack encrypts these matching parameters on the device before they are sent.
* Point out missing or inconsistent events and suggest concrete `sendEvent` calls that match my app’s user flows.
7. **Limitations & best practices**
* Validate that my code respects key constraints:
* SDK is configured from app startup code before any tracking calls.
* iOS-only features like `enableAppleAdsAttribution()` are guarded with `Platform.OS === 'ios'`.
* Event names are uppercase and case-sensitive, and PII is handled appropriately.
* Flag any anti-patterns (e.g. reconfiguring the SDK repeatedly, sending events before init, insecure handling of API keys).
8. **Validation report & checklist**
* Produce a concise report summarizing:
* What is correctly implemented and safe across iOS and Android.
* What is missing or misconfigured with specific files and code snippets to change.
# Swift
Source: https://docs.appstack.tech/SDKs/swift
**Prefer to automate this?** The [Appstack CLI](/tooling/cli) detects your project, installs and configures the SDK, then verifies the result:
```bash theme={null}
npx appstack-cli integrate
```
Already integrated? `npx appstack-cli review` audits it without changing any files.
You are an expert iOS engineer helping me integrate the Appstack Swift SDK into my app. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to fully wire the SDK. When I paste this prompt and share my project files, you should:
1. Determine the correct installation method and give me the **exact commands** (SPM / Xcode UI) using the package information below, pinning the dependency to the **latest stable release** from the repository (not an assumed old version).
2. Tell me **exactly which files and functions** to edit (e.g. `AppDelegate` or the `@main` SwiftUI app) and propose ready-to-paste code that includes the Appstack `configure` call and any required imports, based on the snippets below.
3. Before finalizing, restate the full checklist of steps you applied (install, configure, permissions, events) so I can confirm everything from the docs is covered.
***
## Reference: Appstack Swift SDK
**Requirements**
* iOS 13.0+
* Xcode 14.0+
* Swift 5.0+
**Swift Package Manager**
* Package URL: `https://github.com/appstack-tech/ios-appstack-sdk.git`
* **Xcode:** File → Add Package Dependencies… → paste the URL → pick the **latest** stable version (or an appropriate range) from the package picker.
* **Package.swift:** Add `.package(url: "https://github.com/appstack-tech/ios-appstack-sdk.git", from: "X.Y.Z")` where `X.Y.Z` is the **current** release from that repository’s releases/tags (refresh from GitHub when integrating).
**Initialization examples**
*AppDelegate*
```swift theme={null}
import UIKit
import AppTrackingTransparency
import AppstackSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure Appstack SDK
AppstackAttributionSdk.shared.configure(apiKey: "your_api_key")
// Request tracking permission and enable Apple Ads Attribution
if #available(iOS 15.0, *) {
ATTrackingManager.requestTrackingAuthorization { status in
AppstackASAAttribution.shared.enableAppleAdsAttribution()
}
}
return true
}
}
```
*SwiftUI*
```swift theme={null}
import SwiftUI
import AppstackSDK
@main
struct MyApp: App {
init() {
AppstackAttributionSdk.shared.configure(apiKey: "your_api_key")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
**Configuration parameters**
`configure(apiKey:logLevel:customerUserId:)` — only `apiKey` is required.
```swift theme={null}
// Minimum configuration
AppstackAttributionSdk.shared.configure(apiKey: "your_api_key")
// With optional parameters
AppstackAttributionSdk.shared.configure(
apiKey: "your_api_key",
logLevel: .info, // .off, .error, .info (default), .debug
customerUserId: "user_123"
)
```
**Customer user ID**
```swift theme={null}
AppstackAttributionSdk.shared.setCustomerUserId("user_123") // once the ID is known
```
**Sending events**
```swift theme={null}
// Standard events using EventType enum
AppstackAttributionSdk.shared.sendEvent(event: .LOGIN)
AppstackAttributionSdk.shared.sendEvent(
event: .PURCHASE,
parameters: ["revenue": 29.99, "currency": "USD"]
)
AppstackAttributionSdk.shared.sendEvent(
event: .SUBSCRIBE,
parameters: ["revenue": 9.99, "currency": "USD"]
)
// Custom events
AppstackAttributionSdk.shared.sendEvent(
event: .CUSTOM,
name: "user_attributes",
parameters: [
"email": "test@example.com",
"name": "first_name last_name",
"phone_number": "+33060000000",
"date_of_birth": "2026-02-01"
]
)
```
**EventType values (use standard when possible):**
* Auth/account: `LOGIN`, `SIGN_UP`, `REGISTER`
* Monetization: `PURCHASE`, `ADD_TO_CART`, `ADD_TO_WISHLIST`, `INITIATE_CHECKOUT`, `START_TRIAL`, `SUBSCRIBE`
* Games: `LEVEL_START`, `LEVEL_COMPLETE`
* Engagement: `TUTORIAL_COMPLETE`, `SEARCH`, `VIEW_ITEM`, `VIEW_CONTENT`, `SHARE`
* Catch-all: `CUSTOM`
**Enhanced app campaigns**
* For revenue events, send:
* `revenue` or `price` (number)
* `currency` (e.g. `EUR`, `USD`)
* To improve Meta matching, include when possible:
* `email`
* `name` (first + last name)
* `phone_number` (also accepted as `phone` or `phoneNumber`)
* `date_of_birth` (`YYYY-MM-DD`; also accepted as `birthdate`, `birthday`, or `dateOfBirth`)
* `gender`
* Appstack automatically encrypts these matching parameters before using them for attribution matching.
## **Repository**
Here, you will find the [GitHub iOS Appstack SDK documentation](https://github.com/appstack-tech/ios-appstack-sdk). Please use the latest available version of the SDK.
Current stable release: **4.6.0**, published 2 September 2026. See the [changelog](/changelog/swift) for everything that changed.
## **Quickstart**
Use this path when you only need the minimum production integration:
1. Add the Swift package from `https://github.com/appstack-tech/ios-appstack-sdk.git`.
2. Copy the **Production** API key from **SDK** in Appstack.
3. Call `AppstackAttributionSdk.shared.configure(...)` during app startup.
4. Send standard events such as `.LOGIN`, `.SIGN_UP`, `.PURCHASE`, and `.SUBSCRIBE`.
5. Confirm events appear in the Appstack SDK page before enabling downstream integrations.
```swift theme={null}
import AppstackSDK
AppstackAttributionSdk.shared.configure(apiKey: "your_production_api_key")
AppstackAttributionSdk.shared.sendEvent(
event: .PURCHASE,
parameters: ["revenue": 29.99, "currency": "USD"]
)
```
## **Requirements**
1. iOS 13.0+
2. Xcode 14.0+
3. Swift 5.0+
## **Initial setup**
You can install the SDK via **Swift Package Manager (SPM)** by adding the following dependency to your `Package.swift` file:
```swift theme={null}
dependencies: [
// Set `from:` to the latest stable semver from the repo’s releases (do not leave a stale pin).
.package(url: "https://github.com/appstack-tech/ios-appstack-sdk.git", from: "X.Y.Z")
]
```
Or directly from Xcode:
1. Go to **File > Add Packages**.
2. Enter the repository URL: `https://github.com/appstack-tech/ios-appstack-sdk.git`.
3. Select the desired version and click **Add Package**.
Follow these steps to get the API key:
1. In Appstack, from the side menu, select **SDK** and ensure you are selecting the correct application.
2. Select the **Production** environment.
3. Copy the **API key**.
*AppDelegate*
```swift theme={null}
import UIKit
import AppTrackingTransparency
import AppstackSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure Appstack SDK
AppstackAttributionSdk.shared.configure(apiKey: "your_api_key")
// Request tracking permission and enable Apple Ads Attribution
if #available(iOS 15.0, *) {
ATTrackingManager.requestTrackingAuthorization { status in
AppstackASAAttribution.shared.enableAppleAdsAttribution()
}
}
return true
}
}
```
*SwiftUI*
```swift theme={null}
import SwiftUI
import AppstackSDK
@main
struct MyApp: App {
init() {
AppstackAttributionSdk.shared.configure(apiKey: "your_api_key")
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
The `AppstackAttributionSdk.shared.configure()` method supports the following parameters:
1. `apiKey` (String, required): Your Appstack API key.
2. `logLevel` (LogLevel, default: `.info`): Console log verbosity. One of `.off`, `.error`, `.info`, `.debug`.
3. `customerUserId` (String?, default: nil): Your own user identifier, attached to the event payload.
**Configuration examples**
```swift theme={null}
// Minimum configuration — the API key is the only required parameter
AppstackAttributionSdk.shared.configure(apiKey: "your_api_key")
// With optional parameters
AppstackAttributionSdk.shared.configure(
apiKey: "your_api_key",
logLevel: .info,
customerUserId: "user_123"
)
```
**Log levels**
`logLevel` only controls integrator-facing console output; it does not change what the SDK sends.
| Level | Output |
| -------- | ------------------------------------------------------------------------ |
| `.off` | No SDK logs. |
| `.error` | Actionable errors only, such as invalid API keys or incorrect SDK usage. |
| `.info` | Errors plus high-level SDK lifecycle confirmations. |
| `.debug` | Info logs plus sanitized integration troubleshooting details. |
The customer user ID is your own identifier for the signed-in user. Appstack attaches it to events so server-to-server events — which identify the user by this ID rather than by the install — can be joined back to the install that produced them.
Set it as soon as it is known. That can be before `configure` runs, at `configure` time via the `customerUserId` parameter, or later — most often once a login reveals it:
```swift theme={null}
AppstackAttributionSdk.shared.setCustomerUserId("user_123")
```
* `customerUserId` (String): your identifier for the signed-in user. Leading and trailing whitespace is trimmed.
* Safe to call from any thread. It applies to every event sent from then on, including ones already buffered.
* If you also pass a `customerUserId` to `configure(...)`, that value wins; passing none leaves the ID you already set in place.
* The call does not send anything by itself: make sure at least one event follows, or no mapping is ever formed.
* Calling `configure(...)` again to set the ID does not work — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored. Use `setCustomerUserId` instead.
**Important notes**
1. Initialize the SDK during app startup before sending events
2. Event names must match those defined in the Appstack platform
3. Parameters are passed as a dictionary \[String: Any] and can include any key-value pairs (e.g., revenue, currency, quantity)
4. Revenue parameters support automatic type conversion (Double, Int, Float, String)
5. Revenue ranges are configured in the Appstack platform and automatically synchronized
**Predefined event values**
The SDK provides better type safety with predefined event types:
```swift theme={null}
// Standard events using EventType enum
AppstackAttributionSdk.shared.sendEvent(event: .LOGIN)
AppstackAttributionSdk.shared.sendEvent(
event: .PURCHASE,
parameters: ["revenue": 29.99, "currency": "USD"]
)
AppstackAttributionSdk.shared.sendEvent(
event: .SUBSCRIBE,
parameters: ["revenue": 9.99, "currency": "USD"]
)
// Custom events
AppstackAttributionSdk.shared.sendEvent(
event: .CUSTOM,
name: "user_attributes",
parameters: [
"email": "test@example.com",
"name": "first_name last_name",
"phone_number": "+33060000000",
"date_of_birth": "2026-02-01"
]
)
```
**Available EventType values**
It is recommended to use standard events for a smoother experience.
The `INSTALL` event is tracked automatically on SDK initialization. Do not send it manually — `sendEvent(...)` logs and drops it.
```swift theme={null}
public enum EventType: String {
// Authentication & account
case LOGIN
case SIGN_UP
case REGISTER
// Monetization
case PURCHASE
case ADD_TO_CART
case ADD_TO_WISHLIST
case INITIATE_CHECKOUT
case START_TRIAL
case SUBSCRIBE
// Games / progression
case LEVEL_START
case LEVEL_COMPLETE
// Engagement
case TUTORIAL_COMPLETE
case SEARCH
case VIEW_ITEM
case VIEW_CONTENT
case SHARE
// Catch-all
case CUSTOM // For custom events
}
```
**Enhanced app campaigns**
When running enhanced app campaigns (EACs), it is highly recommended to send multiple parameters with the in-app event to improve matching quality.
For any event that represents revenue, we recommend sending:
1. `revenue` or `price` (number).
2. `currency` (string, e.g. `EUR`, `USD`).
```swift theme={null}
AppstackAttributionSdk.shared.sendEvent(
event: .PURCHASE,
parameters: ["revenue": 4.99, "currency": "EUR"]
)
```
To improve matching quality on Meta and TikTok, send events including the following parameters if you can fulfill them. Appstack automatically encrypts these matching parameters before using them for attribution matching.
1. `email`.
2. `name` (first + last name in the same field).
3. `phone_number` — also accepted as `phone` or `phoneNumber`.
4. `date_of_birth` (recommended format: `YYYY-MM-DD`) — also accepted as `birthdate`, `birthday`, or `dateOfBirth`.
5. `gender`.
## **Appstack ID and attribution params**
After `configure`, you can read the Appstack user ID and the attribution map for partner integrations (for example Superwall, RevenueCat).
```swift theme={null}
let appstackId = AppstackAttributionSdk.shared.getAppstackId()
let attributionParams = await AppstackAttributionSdk.shared.getAttributionParams()
```
* **`getAppstackId()`** — Appstack user identifier when a partner expects `$appstackId` or similar.
* **`getAttributionParams()`** — Attribution payload to forward to partners. It waits for the attribution match to finish, so you do not need a fixed delay after launch.
A matched install returns something like:
```swift theme={null}
// [
// "appstack_adnetwork": "meta", // "google", "meta" or "tiktok"
// "appstack_campaign": "summer_sale",
// "appstack_adset": "lookalike_1pct",
// "appstack_ad": "video_15s",
// "appstack_id": "9f1c8b64-...",
// "appstack_match_status": "matched"
// ]
```
`appstack_adnetwork` is `google`, `meta` or `tiktok`. It is absent when the click carried no recognised network param — for a standard link, read `media_source` instead. On Google the campaign, ad set and ad values are numeric IDs rather than names. `appstack_id` comes back on every result, including organic ones, so read `appstack_match_status` rather than testing whether the map is empty.
`appstack_match_status` tells you whether the match resolved:
| Value | Meaning |
| :------------------ | :-------------------------------------------------------------------------------------------------------------- |
| `matched` | Attributed, with campaign params included. |
| `matched_no_params` | Attributed, but the clicked link carried no tracking params. |
| `organic` | Appstack confirmed there is no attribution for this device. |
| `skipped` | Not an attributable install, so no request was made. |
| `failed` | The request did not complete (offline, timeout, server error) — read again later. |
| `not_configured` | Read before `configure(...)` completed, or the SDK is disabled (invalid API key, or a server-side kill switch). |
Read it with `AppstackAttributionSdk.attributionMatchStatusKey` (SDK `4.5.0`+). `failed` is always worth reading again — the SDK retries in-session and on the next launch. `not_configured` is worth re-reading only when it was read before `configure(...)` finished; a disabled SDK never resolves. The other four are final for the life of the install.
## **Development setup**
If you want to test the SDK against the Appstack development environment before shipping, follow these extra steps:
1. In Appstack, from the side menu, select **SDK**, switch to the **Development** environment, and copy the **Development API key**. This key is separate from your production key.
2. In your `configure(...)` call, use the development key and raise the log level:
```swift theme={null}
AppstackAttributionSdk.shared.configure(
apiKey: "your_development_api_key",
logLevel: .debug
)
```
The API key is what selects the environment — there is no flag to set. A development key routes to the development environment, a production key to production; both use the same endpoint.
## **Security and privacy**
* Never commit API keys to version control.
* Use separate production and development keys, and make sure release builds ship the production key.
* Do not put personally identifiable information in event names.
* Only send matching parameters such as `email`, `name`, `phone_number`, and `date_of_birth` when your app has the right consent and compliance basis. Appstack automatically encrypts these fields before using them for attribution matching.
* Prefer standard event types for common flows so event mapping remains consistent across Appstack and ad integrations.
## **Advanced configuration**
### **SDK behavior**
The SDK automatically:
* Fetches configuration from Appstack servers.
* Manages conversion value updates based on event tracking.
* Handles revenue range matching for conversion optimization.
* Processes events in time-based windows (0-2 days, 3-7 days, 8-35 days).
* Queues events when the configuration is not ready.
### **Event processing**
* Events are processed asynchronously to avoid blocking the main thread.
* The SDK queues events if the configuration is not yet loaded.
* Revenue parameters are automatically validated and converted to numeric values.
* Events are matched against configured revenue ranges in real-time.
## **Limitations**
### **Attribution timing**
* Apple Ads attribution requires iOS 15.0+ (`AppstackASAAttribution` is annotated `@available(iOS 15.0, *)`).
* Apple Ads attribution requires app installation from the App Store or TestFlight.
* Attribution data can take 24-48 hours to appear.
* Some attribution behavior may not be available in development or simulator environments.
### **Event tracking**
* The SDK must be initialized during app startup before any tracking calls.
* `INSTALL` is tracked automatically on SDK initialization. Do not send it manually — the SDK drops such calls.
* Revenue events should include `revenue` or `price` and `currency`.
* Event processing is asynchronous and can queue events while configuration is loading.
## **Troubleshooting**
### **Configuration fails**
* Confirm the API key was copied from the correct app and environment in Appstack.
* Check that the SDK package is installed from the correct repository URL.
### **Events do not appear**
* Confirm `configure(...)` runs before the first `sendEvent(...)` call.
* Confirm the device has network connectivity.
* Check that revenue events include a numeric `revenue` or `price` value and a valid `currency`.
* Allow a few minutes for events to appear in the dashboard.
### **iOS install event does not appear**
* If you have a StoreKit test configuration enabled and are using a production API key, the SDK will not record the install.
* For StoreKit test configuration runs, use the development API key.
* For production-key validation, disable the StoreKit test configuration.
### **Apple Ads attribution does not appear**
* Confirm the app was installed from the App Store or TestFlight.
* Confirm the device runs iOS 15.0+.
* Call `AppstackASAAttribution.shared.enableAppleAdsAttribution()` after initialization and in the appropriate ATT flow.
* Allow 24-48 hours for attribution data to appear.
## **Superwall**
To start using the Superwall integration, [click here](/Integrations/superwall) to see the correct SDK documentation.
## **Apple Ads**
To start using the Apple Ads integration, [click here](/Integrations/apple-ads) to see the correct SDK documentation.
## **Verification checklist**
* Swift package installed from `https://github.com/appstack-tech/ios-appstack-sdk.git`.
* App target meets iOS 13.0+, Xcode 14.0+, and Swift 5.0+ requirements.
* Production API key is used in release builds.
* Development API key is used only in development builds.
* `configure(...)` runs once during app startup.
* The customer user ID is set — via `configure(...)` or `setCustomerUserId(...)` — as soon as it is known, and at least one event follows it.
* `INSTALL` is not sent manually.
* Login, signup, purchase, subscription, and other key app events use standard event types where possible.
* Revenue events include `revenue` or `price` and `currency`.
* Custom events use `.CUSTOM` with a descriptive `name`.
* Appstack ID and attribution params are available before wiring partner integrations.
* Apple Ads attribution is enabled only on supported iOS versions and in the correct permission flow.
* Events are visible in the Appstack SDK page before launch.
## **Support**
For questions or issues:
1. Check the [GitHub Repository](https://github.com/appstack-tech/ios-appstack-sdk).
2. Contact our support team at [support@appstack.tech](mailto:support@appstack.tech).
3. Open an issue in the repository.
You are an expert iOS engineer reviewing my existing Appstack Swift SDK integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Your goal is to **validate that my integration fully matches the official Swift SDK documentation** and identify any missing or incorrect steps. When I paste this prompt and share my project files, you should:
1. **Package & environment**
* Inspect my SwiftPM / Xcode configuration to confirm:
* The Appstack SDK package is added with the correct URL (`https://github.com/appstack-tech/ios-appstack-sdk.git`) and a **current** dependency rule (`from:` / version range) matching the latest stable release, not an outdated pinned version.
* My project meets the requirements (iOS 13.0+, Xcode 14.0+, Swift 5.0+).
* Point out any version or package setup issues and propose exact fixes.
2. **SDK initialization (AppDelegate / SwiftUI)**
* Locate how and where I call:
* `AppstackAttributionSdk.shared.configure(...)`
* `AppstackASAAttribution.shared.enableAppleAdsAttribution()` (where applicable)
* Verify that:
* Initialization happens in app startup (`application(_:didFinishLaunchingWithOptions:)` or `@main` SwiftUI `init`).
* The required `apiKey` is set, and the optional `logLevel` and `customerUserId` parameters are used appropriately. Flag any call that still passes the deprecated `isDebug` or `endpointBaseUrl` arguments — both are ignored by the SDK and should be dropped.
* For iOS 15.0+, Apple Ads attribution is enabled in the correct permission flow, e.g. inside the ATTrackingManager callback.
* Suggest precise code changes if initialization is missing, duplicated, or in the wrong place.
3. **Customer user ID**
* Check that the ID is set as soon as it is known, via `configure(...)` or `setCustomerUserId(...)` (which is safe to call before or after `configure`), and that at least one event follows.
* Flag any attempt to set the ID by calling `configure` again — a repeat `configure(...)` is a no-op and its `customerUserId` is ignored.
4. **Event tracking implementation**
* Find all uses of `AppstackAttributionSdk.shared.sendEvent(...)` and verify:
* Standard `EventType` cases are used for login, signup, purchases, subscriptions, etc.
* Revenue events (`.PURCHASE`, `.SUBSCRIBE`, etc.) send `revenue` (or `price`) and `currency` in the `parameters` dictionary.
* Custom events use `.CUSTOM` with a descriptive `name` and, for EAC / Meta optimization, parameters such as `email`, `name`, `phone_number`, and `date_of_birth`, noting that Appstack automatically encrypts these matching parameters before using them for attribution matching.
* Call out missing or inconsistent events and propose concrete `sendEvent` calls that fit my app flows.
5. **Advanced behavior & limitations**
* Validate that my integration respects documented behavior:
* SDK is initialized during app startup before sending events.
* I understand that events are queued and processed asynchronously and that revenue ranges and conversion windows are handled by the SDK.
* Flag any patterns in my code (e.g. blocking calls, repeated reconfiguration) that could conflict with these assumptions.
6. **Validation report & checklist**
* Produce a clear report summarizing:
* What is already correctly implemented and production-ready.
* What is missing, misconfigured, or risky, with specific files, functions, and suggested code changes.
# Unity
Source: https://docs.appstack.tech/SDKs/unity
**Prefer to automate this?** The [Appstack CLI](/tooling/cli) detects your project, installs and configures the SDK, then verifies the result:
```bash theme={null}
npx appstack-cli integrate
```
Already integrated? `npx appstack-cli review` audits it without changing any files.
You are an expert Unity engineer helping me integrate the Appstack Unity SDK into my game. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my codebase.
Use the reference below to fully wire the package. When I paste this prompt and share my Unity project files, you should:
1. Provide the exact `Packages/manifest.json` (or Package Manager) steps required to install `com.appstack.unity-sdk` from OpenUPM, including the scoped registry entry.
2. Tell me whether to use auto-initialization (**Edit → Project Settings → Appstack**) or manual `AppstackSDK.Configure(...)`, and generate idiomatic C# code for the manual path when it fits my bootstrap flow.
3. Validate my iOS and Android player settings (minimum iOS version, min/target API level, Java version, EDM4U or manual Gradle dependency) against the documentation and highlight anything missing.
4. Summarize the full checklist (install, platform setup, initialization, event tracking) so we can confirm that everything from the docs has been applied.
***
## Reference: Appstack Unity SDK
**Requirements**
* Unity: 6 (`6000.0`) or newer
* iOS: 15.0+
* Android: API level 21+, target API level 34+, Java 17+
* Android builds need either External Dependency Manager for Unity (EDM4U) or the manual Gradle dependency setup. The Appstack package does not install EDM4U.
**Installation (OpenUPM)**
1. Add `https://package.openupm.com` as a scoped registry for `com.appstack`.
2. In Unity, open **Window → Package Manager**.
3. Select **+ → Add package by name** and enter `com.appstack.unity-sdk`.
Or add the scoped registry and dependency directly to `Packages/manifest.json`:
```json theme={null}
{
"scopedRegistries": [
{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": ["com.appstack"]
}
],
"dependencies": {
"com.appstack.unity-sdk": "1.3.0"
}
}
```
Use the **current** version from [openupm.com/packages/com.appstack.unity-sdk](https://openupm.com/packages/com.appstack.unity-sdk/).
**iOS configuration**
* Set **Edit → Project Settings → Player → iOS → Target minimum iOS Version** to `15.0` or newer.
* The iOS dependency is resolved automatically by the Unity build postprocessor. No manual Xcode framework setup is required.
**Android configuration**
Option 1 (recommended): install [EDM4U](https://github.com/googlesamples/unity-jar-resolver). Appstack's Android dependency is then resolved automatically. If automatic resolution is off, run **Assets → External Dependency Manager → Android Resolver → Resolve**.
Option 2: add the repository and dependency manually to your Gradle templates:
```gradle theme={null}
repositories {
mavenCentral()
}
dependencies {
implementation "tech.appstack.android-sdk:appstack-android-sdk:1.8.0"
}
```
No manual R8 or ProGuard configuration is required; Appstack adds its keep rules to the generated Android project automatically.
**Automatic initialization (no code)**
Open **Edit → Project Settings → Appstack**, select **Create Appstack Settings**, and enter the development and production API keys for each platform you ship. Appstack initializes before the first scene, without a GameObject or startup script. Installing the package alone creates no settings and changes no runtime behavior.
**Manual initialization**
```csharp theme={null}
using System.Collections.Generic;
using Appstack;
using UnityEngine;
public sealed class AppstackInitializer : MonoBehaviour
{
[SerializeField] private string iosApiKey;
[SerializeField] private string androidApiKey;
private void Start()
{
#if UNITY_IOS && !UNITY_EDITOR
string apiKey = iosApiKey;
#elif UNITY_ANDROID && !UNITY_EDITOR
string apiKey = androidApiKey;
#else
string apiKey = "your-api-key";
#endif
AppstackSDK.Configure(apiKey);
#if UNITY_IOS && !UNITY_EDITOR
AppstackSDK.EnableAppleAdsAttribution();
#endif
AppstackSDK.SendEvent(
EventType.PURCHASE,
parameters: new Dictionary
{
{ "revenue", 29.99 },
{ "currency", "USD" }
});
}
}
```
**Configuration parameters**
```csharp theme={null}
AppstackSDK.Configure(
apiKey: "your-platform-api-key",
logLevel: 1, // 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR
customerUserId: "optional-user-id"
);
```
**Customer user ID**
```csharp theme={null}
AppstackSDK.SetCustomerUserId("user-123"); // once the ID is known
```
**Sending events**
```csharp theme={null}
// Standard events
AppstackSDK.SendEvent(EventType.SIGN_UP);
AppstackSDK.SendEvent(EventType.LEVEL_COMPLETE);
// With parameters (including revenue)
AppstackSDK.SendEvent(EventType.PURCHASE, parameters: new Dictionary
{
{ "revenue", 29.99 },
{ "currency", "USD" }
});
// Custom events
AppstackSDK.SendEvent(
EventType.CUSTOM,
eventName: "user_attributes",
parameters: new Dictionary
{
{ "email", "test@example.com" },
{ "name", "John Doe" },
{ "phone_number", "+33060000000" },
{ "date_of_birth", "2026-02-01" }
});
```
**EventType values (recommended standard events):**
* Authentication: `EventType.LOGIN`, `EventType.SIGN_UP`, `EventType.REGISTER`
* Monetization: `EventType.PURCHASE`, `EventType.ADD_TO_CART`, `EventType.ADD_TO_WISHLIST`, `EventType.INITIATE_CHECKOUT`, `EventType.START_TRIAL`, `EventType.SUBSCRIBE`
* Games: `EventType.LEVEL_START`, `EventType.LEVEL_COMPLETE`
* Engagement: `EventType.TUTORIAL_COMPLETE`, `EventType.SEARCH`, `EventType.VIEW_ITEM`, `EventType.VIEW_CONTENT`, `EventType.SHARE`
* Custom: `EventType.CUSTOM` (requires `eventName`)
`EventType.INSTALL` is emitted automatically by the native SDKs; `SendEvent(EventType.INSTALL)` is a no-op.
**Enhanced app campaigns**
* For revenue events, send:
* `revenue` or `price` (number)
* `currency` (e.g. `EUR`, `USD`)
* To improve Meta matching, include when possible:
* `email`
* `name` (first + last name)
* `phone_number` (also accepted as `phone` or `phoneNumber`)
* `date_of_birth` (`YYYY-MM-DD`; also accepted as `birthdate`, `birthday`, or `dateOfBirth`)
* `gender`
* Appstack automatically encrypts these matching parameters before using them for attribution matching.
## **Repository**
Here, you will find the [Appstack Unity SDK repository](https://github.com/appstack-tech/appstack-unity-sdk) and the [OpenUPM package page](https://openupm.com/packages/com.appstack.unity-sdk/). Please use the latest available version of the SDK.
Current stable release: **1.3.0**, published 3 September 2026. See the [changelog](/changelog/unity) for everything that changed.
## **Quickstart**
Use this path when you only need the minimum production integration:
1. Add `https://package.openupm.com` as a scoped registry for `com.appstack`, then add `com.appstack.unity-sdk` from the Unity Package Manager.
2. Install EDM4U so the Android native dependency resolves automatically, and set the minimum iOS version to 15.0.
3. Copy the **Production** API key from **SDK** in Appstack, for each platform you ship.
4. Open **Edit → Project Settings → Appstack**, select **Create Appstack Settings**, and paste the keys — or call `AppstackSDK.Configure(...)` once at startup instead.
5. Send standard events such as `EventType.LOGIN`, `EventType.SIGN_UP`, `EventType.PURCHASE`, and `EventType.SUBSCRIBE`.
6. Confirm events appear in the Appstack SDK page before enabling downstream integrations.
```csharp theme={null}
using System.Collections.Generic;
using Appstack;
#if UNITY_IOS && !UNITY_EDITOR
string apiKey = "your-ios-production-api-key";
#elif UNITY_ANDROID && !UNITY_EDITOR
string apiKey = "your-android-production-api-key";
#else
string apiKey = "your-api-key"; // Editor or fallback
#endif
AppstackSDK.Configure(apiKey);
AppstackSDK.SendEvent(EventType.PURCHASE, parameters: new Dictionary
{
{ "revenue", 29.99 },
{ "currency", "USD" }
});
```
## **Requirements**
### **iOS**
* **iOS version:** 15.0+
* **Target minimum iOS Version** in **Player → iOS** must be `15.0` or newer.
### **Android**
* **Minimum API level:** 21 (Android 5.0).
* **Target API level:** 34+
* **Java:** 17+
* **Native dependency:** EDM4U (recommended) or the manual Gradle setup.
### **General**
* **Unity:** 6 (`6000.0`) or newer.
## **Initial setup**
**OpenUPM (recommended)**
1. Add `https://package.openupm.com` as a scoped registry for `com.appstack`.
2. In Unity, open **Window → Package Manager**.
3. Select **+ → Add package by name** and enter `com.appstack.unity-sdk`.
See the [OpenUPM getting-started guide](https://openupm.com/docs/getting-started.html) for scoped-registry instructions, or edit `Packages/manifest.json` directly:
```json theme={null}
{
"scopedRegistries": [
{
"name": "package.openupm.com",
"url": "https://package.openupm.com",
"scopes": ["com.appstack"]
}
],
"dependencies": {
"com.appstack.unity-sdk": "1.3.0"
}
}
```
Use the **current** version from the [OpenUPM package page](https://openupm.com/packages/com.appstack.unity-sdk/).
**iOS Configuration**
1. Open **Edit → Project Settings → Player → iOS**.
2. Set **Target minimum iOS Version** to `15.0` or newer.
3. Build the iOS player normally.
**Note:** Appstack resolves its iOS dependency automatically during the Unity build and embeds the framework in the application. No manual Xcode framework or Apple system-framework configuration is required.
**Android Configuration**
The Unity package contains the Appstack C# and JNI bridge code, but it does not bundle the native Appstack Android SDK. Choose one of the following before building for Android.
*Option 1 (recommended): EDM4U*
Install [External Dependency Manager for Unity (EDM4U)](https://github.com/googlesamples/unity-jar-resolver). Appstack's Android dependency declaration is then discovered and resolved automatically. The Appstack Unity package does not install EDM4U for you. If automatic resolution is disabled, run **Assets → External Dependency Manager → Android Resolver → Resolve**.
*Option 2: manual Gradle configuration*
Add the following repository and dependency to the Gradle templates used by your Unity project:
```gradle theme={null}
repositories {
mavenCentral()
}
dependencies {
implementation "tech.appstack.android-sdk:appstack-android-sdk:1.8.0"
}
```
The dependency must be available to the `unityLibrary` module that compiles Android plugins.
**Minification:** no manual R8 or ProGuard configuration is required. Appstack adds its bridge keep rules to the generated Android project automatically, and the native SDK provides its own consumer rules. You do not need to enable Unity's **Custom Proguard File** setting for Appstack.
Follow these steps to get the API key:
1. In Appstack, from the side menu, select **SDK** and ensure you are selecting the correct application.
2. Select the **Production** environment.
3. Copy the **API key**.
Repeat for each platform you ship, and for the **Development** environment if you want separate keys for development builds.
**Automatic initialization (no code)**
Open **Edit → Project Settings → Appstack**, select **Create Appstack Settings**, and enter the development and production API keys for each platform. Appstack initializes before the first scene loads, without requiring a GameObject or startup script.
* **Auto Initialize** — turn auto-initialization on or off.
* **Environment** — `Automatic` uses the development key for Unity Development Builds and the production key for other builds. `Development` and `Production` pin every build to that environment.
* **Allow Production Fallback** — lets a development build use its production key when no development key is configured. Off by default. Production builds never fall back to a development key.
* **Log Level** — `0=DEBUG`, `1=INFO`, `2=WARN`, `3=ERROR`.
* **Enable iOS** / **Enable Android** — enable each platform independently, each with its own **Development API Key** and **Production API Key**.
* **Enable Apple Ads Attribution** — enables Apple Ads attribution as part of iOS auto-initialization.
Only the current build target is validated: an iOS build does not require Android keys. A build fails only when auto-initialization and its current target platform are enabled but no key can be resolved for that build, so the app never ships with Appstack unexpectedly disabled.
Creating settings opts the project into auto-initialization. Installing the package alone creates no settings and changes no runtime behavior.
**Manual initialization**
Leave the settings asset absent, or turn off **Auto Initialize**, when a consent flow, a custom bootstrap order, or remotely supplied configuration must come first. Call `Configure` once during application startup and before any other SDK method:
```csharp theme={null}
using System.Collections.Generic;
using Appstack;
using UnityEngine;
public sealed class AppstackInitializer : MonoBehaviour
{
[SerializeField] private string iosApiKey;
[SerializeField] private string androidApiKey;
private void Start()
{
#if UNITY_IOS && !UNITY_EDITOR
string apiKey = iosApiKey;
#elif UNITY_ANDROID && !UNITY_EDITOR
string apiKey = androidApiKey;
#else
string apiKey = "your-api-key";
#endif
AppstackSDK.Configure(apiKey);
#if UNITY_IOS && !UNITY_EDITOR
AppstackSDK.EnableAppleAdsAttribution();
#endif
AppstackSDK.SendEvent(
EventType.PURCHASE,
parameters: new Dictionary
{
{ "revenue", 29.99 },
{ "currency", "USD" }
});
}
}
```
The first successful automatic or manual configuration wins. Repeating the same configuration is a silent no-op; a conflicting repeat is ignored with a warning that does not expose either API key. A failed configuration attempt does not lock the wrapper and can be retried.
Initializes the SDK with your API key. Call it once during application startup, before any other SDK method.
**Parameters:**
* `apiKey` Your platform-specific API key from the Appstack dashboard.
* `logLevel` Optional log level: 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR (default: 1). iOS has no dedicated warning level, so `WARN` behaves like `ERROR` there.
* `customerUserId` Optional customer user ID.
**Example:**
```csharp theme={null}
AppstackSDK.Configure("your-api-key-here");
// With all parameters
AppstackSDK.Configure(
apiKey: "your-api-key-here",
logLevel: 0, // DEBUG
customerUserId: "user-123"
);
```
You can check whether the SDK ended up disabled (for example after an invalid API key):
```csharp theme={null}
if (AppstackSDK.IsSdkDisabled())
Debug.LogWarning("Appstack SDK is disabled – check your API key.");
```
The customer user ID is your own identifier for the signed-in user. Appstack attaches it to events so server-to-server events — which identify the user by this ID rather than by the install — can be joined back to the install that produced them.
Set it as soon as it is known. That can be before `Configure` runs, at `Configure` time via the `customerUserId` parameter, or later — most often once a login reveals it:
```csharp theme={null}
AppstackSDK.SetCustomerUserId("user-123");
```
* Callable at any time. It applies to every event sent from then on, including ones the native SDK has buffered but not yet flushed.
* If you also pass a `customerUserId` to `Configure`, that value wins; passing none leaves the ID you already set in place.
* The call does not send anything by itself: make sure at least one event follows, or no mapping is ever formed.
* Calling `Configure` again to set the ID does not work — a repeat `Configure` is a no-op and its `customerUserId` is ignored. Use `SetCustomerUserId` instead.
Track user actions and revenue from your scripts:
```csharp theme={null}
// Track events without parameters
AppstackSDK.SendEvent(EventType.SIGN_UP);
AppstackSDK.SendEvent(EventType.LEVEL_COMPLETE);
// Track events with parameters (including revenue)
AppstackSDK.SendEvent(EventType.PURCHASE, parameters: new Dictionary
{
{ "revenue", 29.99 },
{ "currency", "USD" }
});
AppstackSDK.SendEvent(EventType.SUBSCRIBE, parameters: new Dictionary
{
{ "revenue", 9.99 },
{ "plan", "monthly" }
});
// Custom events
AppstackSDK.SendEvent(
EventType.CUSTOM,
eventName: "user_attributes",
parameters: new Dictionary
{
{ "email", "test@example.com" },
{ "name", "John Doe" },
{ "phone_number", "+33060000000" },
{ "date_of_birth", "2026-02-01" }
});
```
**Available EventType values**
It is recommended to use standard events for a smoother experience.
`EventType.INSTALL` is tracked automatically by the native SDKs on first launch. Do not send it manually — `SendEvent(EventType.INSTALL)` is a no-op.
* `EventType.LOGIN`/ `EventType.SIGN_UP`/ `EventType.REGISTER` Authentication
* `EventType.PURCHASE`/ `EventType.ADD_TO_CART`/ `EventType.ADD_TO_WISHLIST`/ `EventType.INITIATE_CHECKOUT`/ `EventType.START_TRIAL`/ `EventType.SUBSCRIBE` Monetization
* `EventType.LEVEL_START`/ `EventType.LEVEL_COMPLETE` Game progression
* `EventType.TUTORIAL_COMPLETE`/ `EventType.SEARCH`/ `EventType.VIEW_ITEM`/ `EventType.VIEW_CONTENT`/ `EventType.SHARE` Engagement
* `EventType.CUSTOM` For application-specific events
Tracks standard and custom events with optional parameters:
* `eventType` - Event type from the `EventType` enum (required).
* `eventName` - Event name required for `CUSTOM` events; ignored for standard events.
* `parameters` - Optional dictionary of parameters (e.g. `{ "revenue", 29.99 }`, `{ "currency", "USD" }`).
Event parameters may contain strings, Booleans, finite numeric values, nulls, nested string-keyed dictionaries, and arrays. Unsupported objects and non-finite numbers such as `NaN` or infinity throw an `ArgumentException` before the event is sent.
**Enhanced app campaigns**
When running enhanced app campaigns (EACs), it is highly recommended to send multiple parameters with the in-app event to improve matching quality.
For any event that represents revenue, we recommend sending:
1. `revenue` or `price` (number).
2. `currency`(string, e.g. `EUR`, `USD`).
```csharp theme={null}
AppstackSDK.SendEvent(EventType.PURCHASE, parameters: new Dictionary
{
{ "revenue", 4.99 },
{ "currency", "EUR" }
});
```
To improve matching quality on Meta, send events including the following parameters if you can fulfill them. Appstack automatically encrypts these matching parameters before using them for attribution matching.
1. `email`.
2. `name` (first + last name in the same field).
3. `phone_number` — also accepted as `phone` or `phoneNumber`.
4. `date_of_birth` (recommended format: `YYYY-MM-DD`) — also accepted as `birthdate`, `birthday`, or `dateOfBirth`.
5. `gender`.
## **Appstack ID and attribution params**
After configuration, you can read the Appstack user ID and the attribution parameters, so you can forward them to any partner SDK that accepts attribution data.
```csharp theme={null}
string appstackId = AppstackSDK.GetAppstackId();
AppstackSDK.GetAttributionParams(
onSuccess: parameters =>
{
foreach (var kv in parameters)
Debug.Log($"Attribution: {kv.Key} = {kv.Value}");
},
onError: error => Debug.LogError($"Attribution error: {error}")
);
```
* **`GetAppstackId()`** — Appstack user identifier when a partner expects `$appstackId` or similar.
* **`GetAttributionParams(onSuccess, onError)`** — Attribution payload to forward to partners. `onSuccess` fires once the attribution match has finished, so you do not need a fixed delay after launch.
A matched install returns something like:
```json theme={null}
{
"appstack_adnetwork": "meta",
"appstack_campaign": "summer_sale",
"appstack_adset": "lookalike_1pct",
"appstack_ad": "video_15s",
"appstack_id": "9f1c8b64-...",
"appstack_match_status": "matched"
}
```
`appstack_adnetwork` is `google`, `meta` or `tiktok`. It is absent when the click carried no recognised network param — for a standard link, read `media_source` instead. On Google the campaign, ad set and ad values are numeric IDs rather than names. `appstack_id` comes back on every result, including organic ones, so read `appstack_match_status` rather than testing whether the dictionary is empty.
`appstack_match_status` tells you whether the match resolved:
| Value | Meaning |
| :------------------ | :-------------------------------------------------------------------------------------------------------------- |
| `matched` | Attributed, with campaign params included. |
| `matched_no_params` | Attributed, but the clicked link carried no tracking params. |
| `organic` | Appstack confirmed there is no attribution for this device. |
| `skipped` | Not an attributable install, so no request was made. |
| `failed` | The request did not complete (offline, timeout, server error) — read again later. |
| `not_configured` | Read before `configure(...)` completed, or the SDK is disabled (invalid API key, or a server-side kill switch). |
It is added on iOS from Unity SDK `1.3.0`+. `failed` is always worth reading again — the SDK retries in-session and on the next launch. `not_configured` is worth re-reading only when it was read before `configure(...)` finished; a disabled SDK never resolves. The other four are final for the life of the install.
Check for the key rather than assuming it is there — **Android does not report it yet**.
Callbacks are delivered on the synchronization context captured when `GetAttributionParams` is called, when one is available. Calling it from Unity's main thread lets the callbacks safely update Unity objects.
## **Development setup**
### **Environment-based configuration**
With auto-initialization, keep **Environment** on `Automatic`: Unity Development Builds use the development key and other builds use the production key. For manual initialization, resolve the key yourself:
```csharp theme={null}
#if UNITY_IOS && !UNITY_EDITOR
string apiKey = Debug.isDebugBuild ? "ios-development-key" : "ios-production-key";
#elif UNITY_ANDROID && !UNITY_EDITOR
string apiKey = Debug.isDebugBuild ? "android-development-key" : "android-production-key";
#else
string apiKey = "your-api-key"; // Editor or fallback
#endif
AppstackSDK.Configure(apiKey, logLevel: Debug.isDebugBuild ? 0 : 1);
```
### **Editor and unsupported platforms**
In the Unity Editor and on non-iOS/Android platforms, SDK methods are no-ops or return safe defaults: `GetAppstackId()` returns `null` and `IsSdkDisabled()` returns `true`. These platforms do not call native code, so verify your integration on a device or in a store build.
## **Platform-specific considerations**
### **iOS**
**Apple Ads attribution:**
* Requires iOS 15.0+ and an App Store or TestFlight installation.
* Attribution data appears within 24-48 hours.
* User consent may be required for detailed attribution.
* Simulator and ordinary development installs do not represent the production attribution flow.
```csharp theme={null}
#if UNITY_IOS && !UNITY_EDITOR
AppstackSDK.EnableAppleAdsAttribution();
#endif
```
With auto-initialization, enable **Enable Apple Ads Attribution** in **Edit → Project Settings → Appstack** instead of calling the method yourself.
### **Android**
**Play Store Attribution**
* Install referrer data collected automatically.
* Attribution available immediately for Play Store installs.
* Works with Android 5.0+ (API level 21).
## **Security and privacy**
* Never commit API keys to version control. The password fields in **Project Settings → Appstack** mask keys visually only: values stay plaintext in the settings asset and in version control.
* Unity includes the entire `Resources` asset in player builds, so every configured key — including development keys and keys for the other mobile platform — may be present in a production player. Treat them as application ingestion credentials, not administrative secrets.
* Use separate production and development keys.
* Do not put personally identifiable information in event names.
* Only send matching parameters such as `email`, `name`, `phone_number`, and `date_of_birth` when your app has the right consent and compliance basis. Appstack automatically encrypts these fields before using them for attribution matching.
* Prefer standard event types for common flows so event mapping remains consistent across Appstack and ad integrations.
## **Limitations**
### **Attribution timing**
* **iOS:** Apple Ads attribution data appears within 24-48 hours after install.
* **Android:** Install referrer data available immediately for Play Store installs.
* Attribution only available for apps installed from official stores.
### **Platform constraints**
* **Unity:** 6 (`6000.0`) or newer.
* **iOS:** requires iOS 15.0+.
* **Android:** minimum API level 21, target API level 34+, Java 17+.
* Android builds require EDM4U or the manual Gradle dependency; the package does not bundle the native Android SDK.
* SDK methods are no-ops in the Editor and on non-iOS/Android platforms.
### **Event tracking**
* Event names are case-sensitive and standardized.
* For revenue events, always pass a `revenue` (or `price`) and a `currency` parameter.
* The SDK must be configured — automatically or manually — before any tracking call.
* `SendEvent(EventType.INSTALL)` is ignored; install is tracked automatically.
* `EnableAppleAdsAttribution()` only applies on iOS and is a no-op on Android.
* Custom events require an `eventName`; unsupported parameter values throw an `ArgumentException` before the event is sent.
* Network connectivity required for event transmission (events are queued offline).
## **Troubleshooting**
### **Configuration fails**
* Confirm the API key was copied from the correct app and environment in Appstack.
* Confirm `AppstackSDK.Configure(...)` runs once at startup before any other SDK method, or that auto-initialization settings exist with a key for the current target and environment.
* Check `AppstackSDK.IsSdkDisabled()` and the Unity Console with `logLevel: 0` (DEBUG).
* Remember that a repeat `Configure` is ignored, so a second call cannot change the API key, log level, or customer user ID.
### **Events do not appear**
* Confirm the build runs on a device: SDK methods are no-ops in the Editor.
* Confirm configuration completed before the first `SendEvent(...)` call.
* Confirm the device has network connectivity.
* Check that revenue events include a numeric `revenue` or `price` value and a valid `currency`.
* Allow a few minutes for events to appear in the dashboard.
### **iOS build cannot find `AppstackSDK`**
* Delete the generated Xcode project and export it again from Unity.
* Check the Unity Console for postprocessing errors.
* Confirm the generated Xcode project lists the `AppstackSDK` package product on both the `UnityFramework` and application targets.
* Confirm the build machine can reach GitHub to resolve Swift packages.
### **Gradle cannot resolve the Appstack Android SDK**
* Run **Assets → External Dependency Manager → Android Resolver → Resolve** again.
* Confirm Maven Central is available in the generated Gradle repositories.
* Confirm the build uses Java 17 or newer.
* Inspect the Unity Console and Gradle output for the original resolution error.
### **A build fails with an Appstack configuration error**
* Auto-initialization is enabled for the current target platform but no key resolves for that build. Add the missing key in **Edit → Project Settings → Appstack**, disable that platform, or turn off **Auto Initialize**.
* For a development build with only a production key, either add a development key or enable **Allow Production Fallback**.
## **Apple Ads**
To start using the Apple Ads integration, [click here](/Integrations/apple-ads) to see the correct SDK documentation.
## **Verification checklist**
* `com.appstack.unity-sdk` is installed from OpenUPM at a current version.
* Project meets Unity 6+, iOS 15.0+, Android API level 21+ / target 34+, and Java 17+ requirements.
* **Target minimum iOS Version** is set to 15.0 or newer.
* EDM4U is installed and resolved, or the manual Gradle dependency is configured for the `unityLibrary` module.
* Platform-specific API keys are configured for iOS and Android.
* Production API keys are used in production builds; development keys only in development builds.
* The SDK is configured exactly once — through auto-initialization settings or a single `Configure(...)` at startup.
* iOS-only Apple Ads calls are guarded with `#if UNITY_IOS && !UNITY_EDITOR`, or enabled through the settings asset.
* `EventType.INSTALL` is not sent manually.
* Login, signup, purchase, subscription, and other key app events use standard event types where possible.
* Revenue events include `revenue` or `price` and `currency`.
* Custom events use `EventType.CUSTOM` with a descriptive `eventName`.
* The customer user ID is set — via `Configure` or `SetCustomerUserId(...)` — as soon as it is known, and at least one event follows it.
* Appstack ID and attribution params are available before wiring partner integrations.
* Events are visible in the Appstack SDK page before launch.
## **Support**
For questions or issues:
1. Check the [GitHub Repository](https://github.com/appstack-tech/appstack-unity-sdk).
2. Contact our support team at [support@appstack.tech](mailto:support@appstack.tech)
3. Open an issue in the repository.
You are an expert Unity engineer reviewing my existing Appstack Unity SDK integration. You are running inside an IDE assistant such as Cursor or Claude Code and you can see my C# scripts and Unity project settings.
Your goal is to **validate that my integration fully matches the official Unity SDK documentation** and identify any missing or incorrect steps. When I paste this prompt and share my project files, you should:
1. **Package & environment**
* Inspect `Packages/manifest.json` to confirm:
* The `com.appstack` scoped registry points at `https://package.openupm.com`.
* `com.appstack.unity-sdk` is present at a **current** version (check the OpenUPM package page rather than assuming a fixed version).
* The Unity Editor version is 6 (`6000.0`) or newer.
* Suggest the exact manifest or Package Manager steps needed if something is out of spec.
2. **iOS configuration**
* Check that **Target minimum iOS Version** is `15.0` or newer in the iOS player settings.
* Note that the iOS dependency resolves automatically through the Unity Xcode postprocessor, and flag anything in my project (custom postprocessors, manual framework wiring) that could conflict with it.
3. **Android configuration**
* Verify min API level 21+, target API level 34+, and a Java 17+ toolchain.
* Confirm either EDM4U is installed and resolved, or my Gradle templates add Maven Central and `tech.appstack.android-sdk:appstack-android-sdk` to the `unityLibrary` module.
* Note that no manual R8/ProGuard keep rules are needed, and flag any custom Proguard configuration that strips the Appstack bridge.
4. **SDK initialization (C#)**
* Determine whether I use auto-initialization (an `AppstackSettings` asset under `Assets/Appstack/Resources`) or manual `AppstackSDK.Configure(...)`, and verify I am not doing both in conflicting ways.
* For auto-initialization, review the settings: per-platform enablement, environment mode, **Allow Production Fallback**, log level, and Apple Ads attribution.
* For manual initialization, verify `Configure(...)` runs once at startup before any other SDK method, that platform keys are selected with `#if UNITY_IOS` / `#if UNITY_ANDROID`, and that no code relies on a repeat `Configure` to change the key, log level, or customer user ID.
* Propose idiomatic C# initialization code if my current setup is missing, duplicated, or fragile.
5. **Customer user ID**
* Check that the ID is set as soon as it is known, via `Configure` or `SetCustomerUserId(...)` (which is safe to call before or after `Configure`), and that at least one event follows.
* Flag any attempt to set the ID by calling `Configure` again — a repeat `Configure` is a no-op and its `customerUserId` is ignored.
6. **Event tracking implementation**
* Find all `AppstackSDK.SendEvent(...)` calls and verify:
* Standard `EventType` values are used where appropriate (`PURCHASE`, `SIGN_UP`, `SUBSCRIBE`, `LEVEL_COMPLETE`, etc.).
* Revenue events send `revenue` (or `price`) and `currency` in the parameters dictionary.
* Custom events use `EventType.CUSTOM` with a descriptive `eventName` and, for EAC / Meta, rich attributes such as `email`, `name`, `phone_number`, and `date_of_birth`, noting that Appstack automatically encrypts these matching parameters before using them for attribution matching.
* Parameter values are JSON-representable — no non-finite numbers or unsupported object types that would throw `ArgumentException`.
* `EventType.INSTALL` is never sent manually.
* Highlight missing or inconsistent event usage and suggest concrete `SendEvent` calls that fit my game's flows.
7. **Platform-specific behavior & limitations**
* Validate that `EnableAppleAdsAttribution()` is only called on iOS device builds (or enabled through the settings asset).
* Confirm my code tolerates Editor and unsupported platforms, where methods are no-ops, `GetAppstackId()` returns `null`, and `IsSdkDisabled()` returns `true`.
* Check that `GetAttributionParams(...)` is called from the main thread when its callbacks touch Unity objects.
* Respect documented limitations around attribution timing and official store installs.
8. **Validation report & checklist**
* Produce a clear summary of:
* What is correctly implemented and safe to ship.
* What is missing, misconfigured, or risky, with specific C# file and project-setting changes.
# Exports API
Source: https://docs.appstack.tech/api/export
Retrieve all attributed events for your app within a given time window. Supports multiple output formats for data pipeline and analytics integrations.
## Authentication
Every request must include an `Authorization` header containing your Appstack API key. You can find your API key in the Appstack dashboard under your app settings.
```text theme={null}
Authorization:
```
API keys are scoped to a single app. The response will only include events for the app associated with the key.
## Endpoint
```text theme={null}
GET https://api.appstack.tech/api/v1/export
```
## Request
### Query Parameters
Unix timestamp (seconds) defining the start of the export window. Returns all events from this timestamp up to the current time.
Example: `1700000000`
Output format for the response. Accepted values:
| Value | Description |
| --------- | --------------------------------------------------- |
| `json` | Structured JSON object (default) |
| `ndjson` | Newline-delimited JSON — one event per line |
| `csv` | Comma-separated values, returned as a file download |
| `tsv` | Tab-separated values, returned as a file download |
| `xml` | XML document, returned as a file download |
| `xlsx` | Excel workbook (.xlsx), returned as a file download |
| `parquet` | Apache Parquet binary, returned as a file download |
Maximum number of events to return in a single response. Must be between `1`
and `10000` (the maximum page size). Use together with `offset` to page through
large result sets.
Number of events to skip before returning results. Page through the full window
by incrementing `offset` in steps of `limit`.
Results are ordered by `event_time` (ascending). A response is the last page
when it contains fewer than `limit` events — keep requesting with an increasing
`offset` until then.
### Example request
```bash theme={null}
curl -X GET "https://api.appstack.tech/api/v1/export?timestamp=1700000000&format=json" \
-H "Authorization: "
```
## Response
For `format=json`, the endpoint returns a JSON object. For all other formats, the response is streamed as a file download with the appropriate `Content-Disposition` header.
### JSON response
```json theme={null}
{
"data": [
{
"event_id": "abc123",
"event_time": "2024-11-14T12:00:00Z",
"event_name": "appstack_purchase",
"appstack_id": "as_xyz789",
"media_source": "meta",
"campaign_id": "12345678",
"campaign_name": "iOS - Broad - US",
"adset_id": "98765432",
"adset_name": "Interest - Football",
"ad_id": "11223344",
"ad_name": "Video - 15s",
"matching_type": "network",
"click_to_first_open_hours": 1,
"confidence_score": "high",
"country": "US",
"os": "ios",
"app_id": "6741685163",
"app_name": "My app name",
"install_type": "new_install",
"revenue": 9.99,
"currency": "USD",
"idfv": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"maid": "38400000-8cf0-11bd-b23e-10b96e40000d",
"customer_user_id": "user_42",
"revenue_usd": 9.99
}
],
"total_count": 1
}
```
### Response fields
Array of attributed events.
Unique identifier for the event.
ISO 8601 timestamp of when the event occurred.
Prefixed event name in the format `appstack_`. For example: `appstack_purchase`, `appstack_level_complete`.
Appstack's unique installation identifier for the device.
The ad network or source attributed to this event. Examples: `meta`, `google`, `tiktok`, `organic`.
ID of the attributed campaign.
Name of the attributed campaign.
ID of the attributed ad set.
Name of the attributed ad set.
ID of the attributed ad.
Name of the attributed ad.
The attribution method used. Possible values: `network`, `geo`.
Number of hours between the attributed click and the user's first app open.
Confidence level of the attribution match. Possible values: `low`, `medium`, `high`.
ISO 3166-1 alpha-2 country code of the user at the time of the event.
Operating system. Possible values: `ios`, `android`.
Bundle ID or package name of the app (e.g. `com.example.app`).
Human-readable name of the app.
iOS only. Indicates whether this was a fresh install or a reinstall. Possible values: `new_install`, `reinstall_same_device`.
Decrypted revenue amount associated with the event.
ISO 4217 currency code for the revenue field (e.g. `USD`, `EUR`).
iOS Identifier for Vendor. Present on iOS events when available. Used by Amplitude and similar SDKs as `device_id` to merge server-side events with in-app SDK events for the same device.
Mobile Advertising ID — GAID on Android, IDFA on iOS when consented. Present when available. Used by Amplitude and similar SDKs as `device_id` on Android to merge server-side events with in-app SDK events.
Your own user identifier, as set through the Appstack SDK. `null` when the app never identified the user.
Revenue converted to USD using the exchange rate of the event date. `null` for non-revenue events.
Number of events returned in this response (i.e. in the current page).
## Errors
| Status | Code | Description |
| ------ | --------------------- | ------------------------------------------------------------------------------------------- |
| `401` | Unauthorized | Missing or invalid `Authorization` header. |
| `422` | Unprocessable Entity | `timestamp` is missing, `format` is not a valid value, or `limit`/`offset` is out of range. |
| `500` | Internal Server Error | Unexpected error while fetching or building the export. |
### Error response shape
```json theme={null}
{
"error": "Unauthorized",
"detail": "Authorization header is required"
}
```
# Flutter SDK changelog
Source: https://docs.appstack.tech/changelog/flutter
Every release of the Appstack Flutter SDK, newest first.
Releases of the `appstack_plugin` pub.dev package, newest first. Always integrate against the latest
stable release — [`appstack-cli review`](/tooling/cli) will tell you what you
are on.
Install, configure, and send your first events.
**Changed**
* **Updated the Appstack iOS SDK to 4.6.0**
* **Updated the Appstack Android SDK to 1.8.0**
* **Custom `sendEvent()` parameters are encrypted on the device before being sent.** Parameter keys not in the backend's plaintext allowlist are sealed with RFC 9180 HPKE. The allowlist is served in remote config; no app or `configure()` change is required, and with no config block both SDKs send plaintext as before. `transaction_details`, deeplink user data, identifiers and other top-level event fields are not encrypted, and revenue is extracted before encryption, so revenue reporting is unchanged. A value that cannot be sealed is omitted and the event still sends. Requires iOS 17+; on iOS 15–16 values are encrypted server-side as before. Applies at Android's API 21 floor. No new dependency on either platform; the Android AAR grows \~36 KB.
**Fixed**
All three are iOS SDK 4.6.0 fixes; Android is unchanged.
* **A `null` in `sendEvent()`'s `parameters` no longer drops the event on iOS.** Nulls are omitted from the payload; nulls inside lists are kept. This matches Android's existing behaviour.
* **`sendEvent()` no longer crashes on iOS when a parameter value cannot be JSON-serialized.** The key is dropped and logged natively; the event sends with the rest. From Dart this covers `double.nan`, `double.infinity` and typed-data lists (`Uint8List`, `Int32List`, `Int64List`, `Float64List`). `DateTime`, `Uri`, `Set` and arbitrary objects are unaffected — they throw `ArgumentError` at the platform channel and never reach the SDK.
* **A `null` in the attribution match response no longer discards the whole response on iOS.** One null query parameter could previously cost an install its attribution data.
No Dart API changed. `sendEvent()`'s doc comment now describes null handling, the parameter value types that cross the platform channel, and the encryption.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-flutter-sdk/releases/tag/2.7.0)
**Added**
* `AppstackPlugin.setCustomerUserId(customerUserId)` — sets or clears the customer user ID after `configure()`, bridging the native iOS/Android setter of the same name. Use it when a login reveals the ID; calling `configure()` a second time does not work, as a repeat `configure()` is a no-op and ignores its `customerUserId`. Clear it on logout so the previous user's ID stops being attached to later events. Passing `null` (or a blank string) clears the stored ID — unlike `configure()`, which treats a blank value as "not provided" because it never clears. Safe to call at any time; last write wins.
**Changed**
* **Updated the Appstack iOS SDK to 4.5.0** — on iOS, `getAttributionParams()` no longer comes back empty: the map now always carries an `appstack_match_status` key reporting the attribution outcome (`matched`, `matched_no_params`, `organic`, `skipped`, `failed` or `not_configured`). Only `failed` is worth re-reading later; the rest are settled answers. The Dart signature is unchanged — the result is still nullable, so existing null checks keep working — but code that read an empty result as "not attributed" should switch to the status key. Treat the key as iOS-only for now: Android 1.7.0 does not add it, so an empty map still means "nothing yet" there. Also fixed on iOS: a `setCustomerUserId()` call made immediately after `configure()` is no longer overwritten by the `customerUserId` passed to `configure()`, and a blank customer user ID is treated as absent rather than sent as an empty string.
* **Updated the Appstack Android SDK to 1.7.0** — adds the native `setCustomerUserId` setter that `AppstackPlugin.setCustomerUserId()` bridges on Android. No new permission is required.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-flutter-sdk/releases/tag/2.6.0)
**Changed**
* **Updated the Appstack iOS SDK to 4.4.1** — attribution matching now includes additional network context to improve match diagnostics.
* **Updated the Appstack Android SDK to 1.6.0** — attribution matching now includes additional device and network context to improve match accuracy, and the Play install referrer is now resolved before the match request so attribution can be determined deterministically. No new permission is required.
* **`EventType.install` is no longer sent when passed to `sendEvent()`** — both native SDKs already track the install automatically, so sending it by hand previously double-counted installs. Such calls are now logged and discarded natively; the Dart call still succeeds.
**Fixed**
* **Android: a remotely disabled app no longer fires the attribution match request** — previously `enabled=false` suppressed only the event POSTs, and a fresh install still called `/attribution/match`. A disabled app now makes no attribution network calls at all.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-flutter-sdk/releases/tag/2.5.0)
**Changed**
* **Updated the Appstack iOS SDK to 4.4.0**
* **Updated the Appstack Android SDK to 1.5.0**
* Raised the minimum supported iOS version to **15.0** (required by iOS SDK 4.4.0). This applies to both integration paths — the Swift Package Manager package (`Package.swift`) and the CocoaPods podspec.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-flutter-sdk/releases/tag/2.4.0)
# Kotlin SDK changelog
Source: https://docs.appstack.tech/changelog/kotlin
Every release of the Appstack Kotlin SDK, newest first.
Releases of the `tech.appstack.android-sdk:appstack-android-sdk` artifact, newest first. Always integrate against the latest
stable release — [`appstack-cli review`](/tooling/cli) will tell you what you
are on.
Install, configure, and send your first events.
**Added**
* Custom event parameters are encrypted on the device before being sent, so personal data such as an
email or phone number leaves the app already protected. Parameter names that need to stay readable
— for example `currency`, `revenue` and campaign fields — are excluded, and that list is
controlled server-side, so no code change is required in your app. Revenue is still read out of the
parameters before encryption, so revenue reporting is unchanged, as are purchase details and
deeplink user data. A value that cannot be encrypted is omitted from the event, and the rest of the
event still sends. Works down to the SDK's minimum API level 21, adds no new dependency, and grows
the release AAR by roughly 36 KB.
**Added**
* `setCustomerUserId(customerUserId: String?)` sets the customer user id after `configure()`, for
when the id is only known once the user logs in. It applies to every event sent from that point on,
including events already buffered but not yet delivered. Passing `null` or a blank string clears
the id, which matters on logout — previously a stale id stayed on the device and tagged the next
user's events. Safe to call from any thread, before or after `configure()`; the last write wins, so
a non-blank `customerUserId` passed to a later `configure()` takes precedence over a call made
before it. `configure()` only ever sets an id, never clears one, so passing it `null` leaves
whatever `setCustomerUserId()` established in place. Matches the iOS SDK's `setCustomerUserId(_:)`
in both name and null-clears behavior.
**Fixed**
* A `setCustomerUserId(...)` call made immediately after `configure()` is no longer overwritten by
the value passed to `configure()`.
* A repeat `configure()` call now logs a warning instead of silently discarding the
`customerUserId` passed to it. A second `configure()` has always been a no-op; it now points at
`setCustomerUserId()`.
**Added**
* Attribution matching now sends additional permissionless device context to improve match accuracy:
network availability, carrier and SIM metadata, memory and storage capacity, CPU core count, device
uptime and the preferred-language list. Raw IP addresses and persistent telephony identifiers are
never collected, no new runtime permission is required, and any signal that is unavailable is
simply omitted. The full list is documented under **Data Privacy** in the README.
* Play installs are now matched deterministically where possible: the install referrer is fetched
before the attribution match request and sent along with it, instead of falling back to
probabilistic matching.
* A failed Play Store install referrer fetch is retried up to three times per launch. The install
event is sent only once ever, so a single transient referrer-service failure on first launch could
previously cost that install its deterministic attribution permanently.
**Changed**
* `sendEvent(EventType.INSTALL)` is now ignored, since the SDK already tracks installs
automatically. Sending it by hand previously double-counted installs; such calls are now logged at
debug level and discarded. Automatic install events are unaffected.
**Fixed**
* An app disabled through remote config now makes no attribution network calls at all. Previously
only the event uploads were suppressed and the attribution match request still fired on fresh
installs.
**Added**
* Events sent before the SDK finishes initializing are buffered and delivered in order once it is
ready, instead of being dropped.
* A missing `INTERNET` permission in the consuming app is now reported explicitly through
`InitListener.onError()` and `getLastInitError()`.
**Changed**
* The supported API surface is now explicit: `AppstackAttributionSdk`, `EventType`, `LogLevel`,
`InitListener`, `HttpException` and `AuthenticationException`. Implementation classes moved to
`com.appstack.attribution.internal.*` and are no longer visible to consumers. Code that referenced
those implementation details directly has to move to the supported API.
* `configure()` now takes `context`, `apiKey`, `logLevel`, `listener` and `customerUserId`.
Deprecated overloads keep older call shapes compiling.
**Deprecated**
* `isDebug` and `endpointBaseUrl` on `configure()` are deprecated and now ignored. Use
`logLevel = LogLevel.DEBUG` for on-device diagnostics (logcat tag `AppstackSdk`). The deprecated
overloads will be removed in the next major version.
**Fixed**
* Events could go undelivered from minified release builds on 1.4.0 and 1.4.1: the ProGuard rules
shipped with those versions named classes the SDK no longer had, so R8 stripped what the payloads
needed and delivery failed silently. The rules now match the classes actually shipped. If you
minify your release builds and are on 1.4.x, upgrade.
**Removed**
* `showDebugOverlay()` and the debug overlay it presented are gone. The overlay only repeated what
`logLevel` logging already shows. The `is_debug` field is no longer sent with events.
* WorkManager is no longer pulled into your app. The SDK dropped its periodic background refresh job
and now declares `androidx.work` as a compile-only dependency, so consumers no longer inherit
WorkManager, Room and SQLite transitively. Apps upgrading from an older version have the legacy job
cancelled once, on a best-effort basis; apps that never shipped WorkManager are unaffected.
# React Native SDK changelog
Source: https://docs.appstack.tech/changelog/react-native
Every release of the Appstack React Native SDK, newest first.
Releases of the `react-native-appstack-sdk` npm package, newest first. Always integrate against the latest
stable release — [`appstack-cli review`](/tooling/cli) will tell you what you
are on.
Install, configure, and send your first events.
**Added**
* **iOS:** Added experimental React Native 0.87 Swift Package Manager support. The hand-written manifest splits the Swift bridge from the Objective-C++ TurboModule, wires React Native's generated code and header products, and resolves the exact native Appstack SDK version from its public Swift package. CocoaPods support is unchanged.
[Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/3.2.0)
**Added**
* **Custom event parameters are encrypted on the device before they are sent.** Values such as an email address, phone number or name are encrypted in the app rather than on arrival. Parameter names that must stay readable — among them `currency`, `revenue` and campaign fields — are excluded, and that list is controlled by Appstack's backend. No JavaScript change is required and `sendEvent` call sites are unaffected.
* On iOS this requires iOS 17 or later; on iOS 15 and 16 values are encrypted server-side as before. On Android it works down to the SDK's minimum API level 21.
* Revenue is read out of the parameters before encryption, so revenue reporting is unchanged, as are purchase `transaction_details` and deeplink user data.
* A value that cannot be encrypted is omitted from the event, and the remaining parameters are still sent.
* On Android it adds no new dependency and grows the release AAR by roughly 36 KB. On iOS it is built into the vendored framework.
* **iOS:** Events carry a diagnostic recording whether Apple Ads attribution was enabled and whether the AdServices token fetch is pending, succeeded or failed. The SDK sends this diagnostic automatically once token resolution completes, rather than on the next `sendEvent` (native `4.5.2`). It has no JavaScript API.
* **iOS:** The vendored framework ships an Apple privacy manifest declaring its required-reason API usage (native `4.5.1`). No additions to the app's own `PrivacyInfo.xcprivacy` are required.
**Changed**
* **iOS:** Updated `AppstackSDK.xcframework` to `4.6.0`, which brings the on-device parameter encryption above along with the parameter and attribution fixes below.
* **Android:** Updated the native Appstack Android SDK dependency to `1.8.0`, which brings the on-device parameter encryption above.
* A manual `sendEvent('ASA_ATTRIBUTION')` is now dropped rather than sent as a custom event. iOS `4.6.0` adds `ASA_ATTRIBUTION` to its native event enum and emits it automatically once AdServices token resolution completes, so it joins `INSTALL`, `FIRST_OPEN` and `FIRST_OPEN_GUARDED` in the set the wrapper refuses to forward. It is not part of the public `EventType` API on either platform.
**Fixed**
* **iOS:** A `null` nested inside a parameter no longer drops the event. Null values are omitted from the payload and nulls inside arrays are preserved, matching Android. Top-level `null` and `undefined` have been stripped in JavaScript since 3.0.0, but that stripping is shallow, so a null inside an object or array still reached native, where the event was discarded.
* **iOS:** A parameter holding a value JSON cannot represent no longer crashes the app. This covers `NaN` and infinite numbers, a `Date`, `URL`, `Set` or class instance. Such keys are dropped individually and named in an error log, and the event is sent with its remaining parameters. `NaN` and `Infinity` are typed as `number`, so they satisfy the `JsonValue` type on `parameters`.
* **iOS:** A single `null` in the attribution match response no longer discards the response. One null query parameter could previously cost an install its attribution data, which surfaced in JavaScript as an empty `getAttributionParams()` result.
[Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/3.1.0)
**Changed**
* **TurboModule migration.** The SDK is now a real TurboModule generated by React Native's codegen, with full backwards compatibility for the legacy architecture. Previously the package shipped no `codegenConfig` at all, so codegen never ran and the module was reached through React Native's legacy interop layer even on the New Architecture. The public JavaScript API is unchanged.
* Added a `codegenConfig` block (`RNAppstackSdkSpec`) and a real TurboModule spec at `src/NativeAppstackReactNative.ts`; the JavaScript entry point now resolves the native module through `TurboModuleRegistry`, which works on both architectures.
* **iOS:** `AppstackReactNative` conforms to the generated `NativeAppstackReactNativeSpec` protocol and implements `getTurboModule:` under `RCT_NEW_ARCH_ENABLED`, falling back to `RCTBridgeModule` on the legacy architecture.
* **Android:** the module now extends the generated `NativeAppstackReactNativeSpec` on the New Architecture (with a `BaseReactPackage` that reports `isTurboModule`), and keeps the `ReactContextBaseJavaModule` implementation on the legacy architecture. Shared logic moved to `AppstackReactNativeModuleImpl`.
* **Breaking:** `configure()` now rejects a fractional `logLevel`. `logLevel: 1.5` previously passed validation and was silently truncated to `1` by the native casts (`NSInteger` on iOS, `Double.toInt()` on Android); only the documented levels `0`, `1`, `2` and `3` are accepted. The error message is now `logLevel must be one of 0, 1, 2, or 3` (was `logLevel must be a number between 0 and 3`, which read as though `1.5` were valid).
* **Breaking:** `sendEvent` resolves `void` instead of `true`. The old `true` was misleading — it only ever meant "the call reached native", and was returned even when the native SDK went on to drop the event (automatic-only type, SDK disabled, buffer full, no connectivity). Nothing about delivery is observable from JavaScript, so the return value no longer implies it. Code branching on the result should stop doing so; `await`ing still works unchanged.
* **`sendEvent` now resolves the event type in JavaScript**, sending native an explicit `(type, name)` pair: a standard type with a `null` name, or the `CUSTOM` category with a name. This fixes a cross-platform divergence in which the *same call* behaved differently per platform: `sendEvent('MY_CUSTOM_EVENT')` — an unknown type with no separate name — was sent by iOS as a custom event named `MY_CUSTOM_EVENT`, while Android collapsed it to `CUSTOM` and then rejected it with `INVALID_EVENT_NAME` because no name had been supplied. Both platforms now receive identical arguments, and the native wrappers' guessing branches are no longer reachable from JavaScript.
* **A manual `INSTALL` is dropped before reaching native**, logging an error and resolving normally rather than throwing. Installs are recorded automatically and sending them by hand inflates install counts, which is why iOS already discarded them natively. The same guard covers the SDK's internal lifecycle events, which are not part of the public `EventType` API: those exist only in iOS's enum, so sending one by hand previously became a bogus *custom* event on Android alone. Resolving rather than rejecting keeps callers who harmlessly send a manual `INSTALL` today — a silent discard since 2.5.0 — from suddenly seeing failures.
* **Parameter values of `null` or `undefined` are stripped before the call reaches native.** Android already filtered them out (`filterValues { it != null }`) while iOS forwarded `NSNull`, so identical JavaScript produced a different payload per platform. Stripping is shallow, matching Android's behaviour; an empty result is sent as no parameters at all. `0`, `false` and `''` are real values and are preserved.
* **`parameters` is typed against a JSON-safe value type** (`Record`) instead of `Record`. Values outside that set — a `Date`, a class instance, a function — never survived the bridge intact; this makes that a compile error instead of a runtime surprise. `AppstackEventParameters` and `JsonValue` are exported for annotating your own payloads.
* **Android:** the native wrapper now treats an unrecognised event type as a custom event named after that type, matching iOS, instead of rejecting with `INVALID_EVENT_NAME`. Defence in depth only — the JavaScript wrapper no longer sends an unrecognised type, so this path is unreachable from JS; it exists so the divergence above cannot return if that invariant is ever broken.
**Removed**
* **Breaking:** the positional `configure(apiKey, isDebug?, endpointBaseUrl?, logLevel?, customerUserId?)` signature, deprecated in 2.6.0, is gone. Only `configure(apiKey, { logLevel, customerUserId })` remains. `isDebug` and `endpointBaseUrl` are removed outright — neither was ever forwarded to the native SDKs, so no behaviour is lost by dropping them. A 2.x-style call now throws an error naming the replacement instead of silently ignoring the extra arguments, which would otherwise discard the `logLevel` and `customerUserId` that follow them.
```js theme={null}
// 2.x
await AppstackSDK.configure(apiKey, false, undefined, 0, 'user_123');
// 3.0
await AppstackSDK.configure(apiKey, { logLevel: 0, customerUserId: 'user_123' });
```
* **Breaking:** `sendEvent(eventType, eventName, parameters)` is now `sendEvent(event, parameters)`. The middle `eventName` argument is gone. Pass a standard `EventType` for a standard event, or your own name for a custom one — there is no separate "custom" mode:
```js theme={null}
// before // after
sendEvent('PURCHASE', null, { revenue: 4.99 }) → sendEvent(EventType.PURCHASE, { revenue: 4.99 })
sendEvent('CUSTOM', 'user_attributes', params) → sendEvent('user_attributes', params)
sendEvent('CUSTOM', 'APP_OPENED') → sendEvent('APP_OPENED')
```
A three-argument call now rejects with a message naming the replacement, rather than silently binding the event name to `parameters` and shipping an event whose payload is its own name. (`sendEvent` is `async`, so every validation failure rejects the returned promise rather than throwing synchronously.) The two-argument `sendEvent('CUSTOM', 'my_event')` form — a custom event with no parameters — is rejected on the same grounds, since its name would otherwise land in `parameters`. `EventType` itself stays, and remains the recommended form; a plain string is accepted and resolved case-insensitively.
* **Breaking:** `EventType.CUSTOM` was removed. In the two-argument API you pass a custom event's name directly, so `CUSTOM` had no caller-facing meaning and was actively a footgun: `sendEvent(EventType.CUSTOM, params)` would have resolved as "standard type CUSTOM with no name", which iOS drops outright and Android sends with a null `event_name`. Removing it turns that into a compile error; the literal string `'CUSTOM'` is also rejected at runtime, with a message pointing at the custom-name form.
* **Breaking:** `isDebug` and `endpointBaseUrl` are removed from the exported `AppstackConfig` type.
* **iOS:** The native module no longer subclasses `RCTEventEmitter`. It never emitted any events (`supportedEvents` returned an empty array), and the inherited `addListener` / `removeListeners` methods were already unavailable on the New Architecture. Any code calling `new NativeEventEmitter(NativeModules.AppstackReactNative)` on the legacy architecture should be removed.
* The two deprecation `console.warn`s that 2.6.0 added for positional `isDebug` / `endpointBaseUrl` are gone with the signature they warned about.
* Deleted a stale, unwrapped duplicate of two `AppstackSDK.framework` slices that was committed directly under `ios/` (plus its orphaned `Info.plist`). It was not referenced by the podspec but was published in every npm tarball. The vendored framework at `ios/AppstackSDK.xcframework` is unaffected.
**Added**
* In development builds (`__DEV__`), `sendEvent` logs a warning when the event string is not a recognised standard type, naming the custom event it became. This is the only place a misspelled standard event is catchable: a typo like `'PURCAHSE'` otherwise becomes a custom event with no error anywhere, silently losing the standard-event semantics that enhanced app campaigns optimise against. It is a heuristic — it also fires on legitimate custom names — so it is advisory, dev-only, and never gates the send.
**Fixed**
* **iOS:** The SDK now builds when the host app forces our pod below iOS 15.0, instead of failing to compile. `AppstackASAAttribution` in the vendored framework requires iOS 15.0, and the podspec asks for a 15.0 deployment target — but React Native 0.74 writes its own minimum (13.4) onto every pod target after install, and that setting wins over the podspec. The build then stopped with `'AppstackASAAttribution' is only available in iOS 15.0 or newer`. The two Apple Search Ads calls are now behind a runtime `if #available(iOS 15.0, *)` check. Nothing changes for apps: the Objective-C layer already rejects both methods with `UNSUPPORTED_IOS_VERSION` below iOS 15.0, and Apple Search Ads attribution cannot work there anyway. iOS 15.0 remains the SDK's requirement.
* **iOS:** The podspec depended on `React-Codegen`, which is an unrelated third-party pod on the public CocoaPods trunk (React Native's pod is `ReactCodegen`). Dependencies are now installed via React Native's own `install_modules_dependencies` helper, which also supplies the codegen and static-framework header search paths that cannot be reproduced by hand. As a side effect, the React-Core header search path fix from 2.1.0 now applies on both architectures instead of only the legacy one.
* **Android:** Removed `cmakeListsPath: null` from `react-native.config.js`. It never disabled codegen autolinking as its comment claimed (`null` is falsy, so autolinking always fell through to the default generated path), and it obscured the real reason codegen never ran.
* Removed three hand-written files under `android/src/main/jni/` that imitated codegen output (`AppstackReactNativeSpec.{h,cpp}` and a `CMakeLists.txt` declaring a spec target). They contained no TurboModule code, were never built, and are now replaced by genuine generated artifacts.
[Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/3.0.0)
**Added**
* `configure(apiKey, { logLevel, customerUserId })` — an options-object form that removes the need to skip the two deprecated positional parameters. Previously, setting `logLevel` or `customerUserId` meant writing `configure(key, undefined, undefined, 0, 'user_123')`; it is now `configure(key, { logLevel: 0, customerUserId: 'user_123' })`. This matches the named-parameter shape the Flutter plugin already exposes.
* `AppstackSDK.setCustomerUserId(customerUserId)` — sets or clears the customer user ID after `configure()`, for when the ID is only known once the user logs in. Calling `configure` a second time does not work, because a repeat `configure` is a no-op and ignores its `customerUserId`. It applies to every event sent from that point on; on iOS it also reaches events already buffered but not yet delivered. Clear it on logout so the previous user's ID stops being attached to later events. `null`, `undefined` and `''` all clear the stored ID — unlike `configure`, which rejects an empty string because it never clears. Safe to call at any time; last write wins.
* **iOS:** `getAttributionParams()` now always resolves with an `appstack_match_status` key telling you whether the attribution request succeeded: `matched`, `matched_no_params`, `organic`, `skipped`, `failed` or `not_configured`. Only `failed` is worth re-reading later; the rest are settled answers. Android does not report this key yet, so check for it rather than assuming both platforms return it.
* **iOS:** Attribution match outcomes are now visible in the SDK's console output at `logLevel` `0` (debug).
**Changed**
* **iOS:** Updated `AppstackSDK.xcframework` to `4.5.0`, which adds the native setter behind `setCustomerUserId` along with the attribution match status.
* **Android:** Updated the native Appstack Android SDK dependency to `1.7.0`, which adds the native setter behind `setCustomerUserId`.
* **iOS:** An empty result from `getAttributionParams()` no longer means "not attributed" — read `appstack_match_status` instead.
* Passing `isDebug: true`, or any `endpointBaseUrl`, positionally now logs a `console.warn` pointing at the options object; their no-op values (`false` and `undefined`) stay silent. Both arguments remain accepted and continue to be ignored — neither has ever been forwarded to the native SDKs.
**Fixed**
* **iOS:** `deleteUserData()` now also clears the stored customer user ID.
* **iOS:** A blank customer user ID is treated as absent instead of being sent as an empty string.
* **iOS:** A `setCustomerUserId` call made immediately after `configure()` is no longer overwritten by the value passed to `configure()`.
**Compatibility**
* The positional signature `configure(apiKey, isDebug?, endpointBaseUrl?, logLevel?, customerUserId?)` is unchanged and still type-checks; the two forms are TypeScript overloads and are distinguished at runtime by whether the second argument is an object. All existing validation and error messages are preserved.
[Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/2.6.0)
**Changed**
* **iOS:** Updated `AppstackSDK.xcframework` to `4.4.1`. Attribution matching now includes additional network context to improve match diagnostics.
* **Android:** Updated the native Appstack Android SDK dependency to `1.6.0`. Attribution matching now includes additional device and network context to improve match accuracy, and install attribution resolves more reliably on first launch. No additional app permissions are required.
**Fixed**
* **iOS & Android:** `sendEvent` now ignores `INSTALL`, which the SDKs already track automatically. Sending it by hand previously double-counted installs; such calls are now discarded.
[Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/2.5.0)
**Changed**
* **iOS:** Updated `AppstackSDK.xcframework` to `4.4.0`. Adds Mac Catalyst support to the prebuilt XCFramework and improves install detection and attribution reliability.
* **Android:** Updated the native Appstack Android SDK dependency to `1.5.0`.
[Release notes on GitHub](https://github.com/appstack-tech/react-native-appstack-sdk/releases/tag/2.4.0)
# Swift SDK changelog
Source: https://docs.appstack.tech/changelog/swift
Every release of the Appstack Swift SDK, newest first.
Releases of the `ios-appstack-sdk` Swift package, newest first. Always integrate against the latest
stable release — [`appstack-cli review`](/tooling/cli) will tell you what you
are on.
Install, configure, and send your first events.
**Added**
* Custom event parameters are encrypted on the device before being sent, so personal data such as an
email or phone number leaves the app already protected. Parameter names that need to stay readable
— for example `currency`, `revenue` and campaign fields — are excluded, and that list is
controlled server-side, so no code change is required in your app. Requires iOS 17 or later; on
iOS 15 and 16 the values are encrypted server-side as before. Purchase `transaction_details` and
deeplink user data are unaffected, so revenue reporting is unchanged. A value that cannot be
encrypted is omitted from the event, and the rest of the event still sends.
**Fixed**
* A `null` in the attribution match response no longer discards the whole response. Previously one
null query parameter could cost an install its attribution data.
* Event parameters holding `null` no longer cause the event to be silently dropped. Null values are
now omitted from the payload (nulls inside arrays are kept), matching the Android SDK. This
covered a nil Swift `Optional` passed as a parameter, any dictionary decoded from JSON that
carries a null field, Dart `null` from Flutter, and nested nulls from React Native.
* `sendEvent(event:name:parameters:)` no longer crashes the app when a parameter holds a value JSON
cannot represent — a `Date`, `URL`, `Data`, `Set`, custom object, or a `NaN`/infinite number.
Such keys are now dropped individually and named in an error log; the event still sends with its
remaining parameters. Send strings, finite numbers, booleans, arrays, and nested string-keyed
dictionaries.
[Release notes on GitHub](https://github.com/appstack-tech/ios-appstack-sdk/releases/tag/4.6.0)
**Added**
* Events now carry an internal diagnostic recording whether Apple Ads attribution was enabled and
whether the AdServices token fetch is pending, succeeded, or failed. The SDK sends an automatic
diagnostic event when the token resolution completes, so no later app event is required.
[Release notes on GitHub](https://github.com/appstack-tech/ios-appstack-sdk/releases/tag/4.5.2)
**Added**
* Added an Apple privacy manifest declaring the SDK's required-reason API usage.
[Release notes on GitHub](https://github.com/appstack-tech/ios-appstack-sdk/releases/tag/4.5.1)
**Added**
* `setCustomerUserId(_:)` sets the customer user id after `configure()`, for when the id is only
known once the user logs in. It applies to every event sent from that point on, including events
already buffered but not yet delivered.
* `getAttributionParams()` now always carries an `appstack_match_status` key telling you whether the
attribution request succeeded: `matched`, `matched_no_params`, `organic`, `skipped`, `failed` or
`not_configured`. Only `failed` is worth re-reading later; the rest are settled answers.
* Attribution match outcomes are now visible on the `.debug` log channel.
**Changed**
* `getAttributionParams()` no longer returns `nil` — where the result was previously `nil` or empty,
it now carries the status key explaining why. Existing call sites keep compiling, but code using
an empty result to mean "not attributed" should switch to the status key.
**Fixed**
* `deleteUserData()` now also clears the stored customer user id.
* A blank customer user id is treated as absent instead of being sent as an empty string.
* A `setCustomerUserId(...)` call made immediately after `configure()` is no longer overwritten by
the value passed to `configure()`.
[Release notes on GitHub](https://github.com/appstack-tech/ios-appstack-sdk/releases/tag/4.5.0)
**Added**
* Attribution matching now includes additional network context to improve match diagnostics.
**Fixed**
* `sendEvent(event:)` now ignores `INSTALL`, which the SDK already tracks automatically. Sending it by hand previously double-counted installs; such calls are now logged and discarded.
[Release notes on GitHub](https://github.com/appstack-tech/ios-appstack-sdk/releases/tag/4.4.1)
**Added**
* Added Mac Catalyst support to the prebuilt XCFramework.
**Changed**
* Improved install detection and attribution reliability.
* Improved Swift Package Manager resolution: installations now download the prebuilt XCFramework
directly from the GitHub release asset instead of cloning the complete source repository.
* Deprecated `isDebug` and `endpointBaseUrl`; both arguments are now ignored and will be removed
in a future version. Integrations should migrate to `configure(apiKey:logLevel:customerUserId:)`.
**Fixed**
* Improved SDK behavior during initialization and temporary connectivity failures.
**Removed**
* Removed the test-only `AppstackAttributionSdk.setInstallDateForTesting(_:)` API.
[Release notes on GitHub](https://github.com/appstack-tech/ios-appstack-sdk/releases/tag/4.4.0)
# Unity SDK changelog
Source: https://docs.appstack.tech/changelog/unity
Every release of the Appstack Unity SDK, newest first.
Releases of the `com.appstack.unity-sdk` UPM package, newest first. Always integrate against the latest
stable release — [`appstack-cli review`](/tooling/cli) will tell you what you
are on.
Install, configure, and send your first events.
**Changed**
* Pinned native SDKs are now Appstack iOS SDK `4.6.0` and Appstack Android SDK
`1.8.0`. Platform floors are unchanged: iOS 15.0+ and Android API level 21+.
* Custom event parameters are encrypted on the device before being sent.
Parameter names that must stay readable, such as `currency` and `revenue`,
are excluded through a server-controlled list, so revenue reporting is
unchanged. On-device encryption requires iOS 17 or newer; iOS 15 and 16
encrypt server-side as before.
* The pinned iOS SDK ships an Apple privacy manifest declaring its
required-reason API usage.
**Fixed**
* On iOS, an event whose parameters hold `null` is no longer dropped, and a
parameter value JSON cannot represent no longer crashes the app. Those keys
are dropped individually and the event still sends.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-unity-sdk/releases/tag/1.3.0)
**Changed**
* OpenUPM releases now use a Unity-signed `.tgz` created with a pinned,
checksum-verified Unity UPM CLI. Unity 6.3 and newer can verify the package's
publisher and integrity instead of showing the missing-signature warning.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-unity-sdk/releases/tag/1.2.1)
**Added**
* Optional scene-independent auto-initialization configured through **Edit →
Project Settings → Appstack**, with separate development and production keys
for iOS and Android, per-platform enablement, automatic or pinned environment
selection, explicit production-key fallback for development builds, and
target-specific pre-build validation.
* Idempotent Unity-side configuration: the first successful automatic or
manual configuration wins, identical repeats are silent, conflicting repeats
warn without exposing credentials, and failed attempts remain retryable.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-unity-sdk/releases/tag/1.2.0)
**Added**
* `SetCustomerUserId(customerUserId)` and `ClearCustomerUserId()` for setting or
clearing the customer user ID after `Configure()`, bridging the native
iOS/Android setter of the same name. Use them when a login reveals the ID: a
repeat `Configure()` is a no-op and ignores its `customerUserId`. `null`, an
empty string, and whitespace all clear the stored ID, so
`ClearCustomerUserId()` is the explicit spelling of `SetCustomerUserId(null)`
rather than separate behavior. This differs from `Configure()`, where an empty
`customerUserId` means "not provided" because it never clears.
**Changed**
* Pinned native SDKs are now Appstack iOS SDK `4.5.0` (from `4.4.0`) and
Appstack Android SDK `1.7.0` (from `1.5.0`). Platform floors are unchanged:
iOS 15.0+ and Android API level 21+.
* `SendEvent(EventType.INSTALL)` is a no-op. `EventType.INSTALL` is emitted
automatically by the native SDKs on first launch, and both new pinned versions
discard a manually sent `INSTALL`; the previous pins accepted it.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-unity-sdk/releases/tag/1.1.0)
**Added**
* Initial Unity Package Manager distribution as `com.appstack.unity-sdk` for
Unity 6 (`6000.0`) or newer.
* Support for iOS 15.0+ through Appstack iOS SDK `4.4.0` and Android API level
21+ through Appstack Android SDK `1.5.0`.
* SDK configuration with an API key, log level, and optional customer user ID.
* Standard and custom event tracking with optional event parameters.
* Apple Ads attribution on iOS.
* Appstack ID, SDK status, and asynchronous attribution-parameter retrieval.
* Concurrent attribution requests with callbacks returned to the captured
synchronization context when available.
* Automatic iOS Swift Package Manager integration through the Unity Xcode
postprocessor, including dynamic-framework embedding in the application.
* Automatic Android native dependency resolution through EDM4U.
* Automatic Android R8/ProGuard configuration with no custom keep-rules step.
* Basic Integration sample for manual SDK configuration and event tracking.
[Release notes on GitHub](https://github.com/appstack-tech/appstack-unity-sdk/releases/tag/1.0.0)
# Enhanced app campaigns
Source: https://docs.appstack.tech/enhanced-app-campaigns
Appstack’s ‘Enhanced app campaign’ protocol, also known as EAC, is the new standard for mobile apps to scale their user acquisition efforts.
## What are enhanced app campaigns?
In a nutshell, it consists of web campaigns that redirect all users to the app stores, without needing a website.
Enhanced app campaigns combine the best of standard app campaigns and web-to-app campaigns into a unified protocol.
* Standard app campaigns are traditional campaigns that send users directly to app stores to download the app.
* Web-to-app campaigns refer to the web campaigns that redirect users to a website funnel and then redirect the users to the app store
It enables app advertisers to run paid ads across multiple ad networks using the Appstack SDK and unlock unparalleled performance benefits.
To start running enhanced app campaigns, [click here](https://cal.com/appstack/appstack-demo) to request access.
## How do enhanced app campaigns work?
Enhanced app campaigns work by providing ad networks with accurate, fast signals to optimize campaigns.
The advertised app uses the Appstack ad links in its ads. When the user clicks on the ad link, they are immediately redirected to the app store.
At the moment of the click, Appstack collects device-level information and stores it to potentially match the user later if an install occurs.
After seeing and clicking the ad, the user will go to the app store to install the app, then open it for the first time.
With the first app open, the Appstack SDK initializes, and the 'Appstack Attribution Service' (AAS) starts the matching process.
Once the user is matched to a click and a key in-app event is triggered, Appstack enriches the signal (postback) and sends it to the ad network in real time.
## Why are apps using enhanced app campaigns?
Apps are shifting their ad budget towards enhanced app campaigns because of:
1. **Superior attribution:** real-time device-level attribution data at the ad level.
2. **Improved performance:** benefit from enriched signals and top-tier infrastructure to boost your ad campaigns ROAS.
3. **Expanded reach:** discover new segments and audiences that convert with web-based campaigns.
4. **Zero overhead:** no need to build web funnels, data duplication issues, manage global tax compliance, or worry about financial and legal complexities.
Enhanced app campaigns for iOS don't require apps to have the ATT (App Tracking Transparency) framework installed, and therefore, they don't rely on SKAdNetwork to work.
| | Enhanced app campaigns | Standard app campaigns |
| -------------------- | ---------------------- | ---------------------- |
| Improved performance | ✓ | X |
| New campaign types | ✓ | X |
| Superior attribution | ✓ | X |
| Signal engineering | ✓ | X |
### New use cases
EAC breaks the black-box dilemma and reintroduces campaign control and signal engineering into a fully automated world. Leveraging EACs enables mobile apps to unlock new campaign types:
1. Google Ads search-to-app
2. TikTok Ads search-to-app
3. YouTube shorts
4. Google Ads display
## Frequently asked questions
No. Enhanced app campaigns (EACs) are frictionless, meaning users go straight to the app stores without needing a landing page. Therefore, it removes the pain of maintaining and managing web funnels, global tax compliance, and legal structures.
Yes. The Appstack SDK must be installed on the app. This is required to enable the matching and measuring of in-app events.
No, you don't need to install the Meta Ads SDK, and it won't interfere with EAC if you are using it. Only with the Appstack SDK can you run ads across multiple ad networks.
No. The enhanced app campaign protocol does not interfere with SKAdNetwork and will not affect its reporting capabilities. It operates 100% in parallel.
Appstack is responsible for managing and sending the in-app event postbacks (also known as signals) to the ad networks for campaign optimization.
Inside your Appstack account, you can create dashboards to analyze the performance of your ad campaigns.
Apps looking to expand their paid reach, improve profitability, target B2B audiences, older segments, specific niches, or those that are struggling with attribution and seeking clarity.
It depends. EACs highly improve your chances of getting a better ROAS. The goal of Appstack is to equip apps with reliable analytics and attribution so they can focus on creating the best ad creatives and product experience.
Yes, or at least someone with the knowledge to install and configure the Appstack SDK.
The 'Appstack Attribution Service' (AAS) uses an advanced probabilistic matching algorithm that combines data from ad links (also known as tracking links) and the Appstack SDK to match users who opened the app with those who clicked on ads.
Yes. For EACs to work, apps must use the Appstack ad link across all your ad networks to achieve a unified, reliable measurement. The more EAC you have and the less non-EAC, the more accurately Appstack can measure users.
100% safe. Apps can leverage the Appstack SDK and Links solution to attribute users to the right campaigns with an accuracy rate of 90%-97%. App developers decide what data to send to comply with GDPR and CCPA.
# Welcome to Appstack
Source: https://docs.appstack.tech/introduction
Measurement and attribution for mobile apps and games running paid ads.
## **What is Appstack?**
Appstack is the app marketing attribution company for mobile apps that want to scale their user acquisition efforts without dealing with complex measurement or infrastructure challenges.
A product that cares about:
1. Being easy to use
2. Fast to connect with
3. The most reliable attribution
Appstack is the go-to solution for apps looking to unlock [enhanced app campaigns](/enhanced-app-campaigns), access actionable analytics, leverage deep links, and solve attribution problems.
## How does it work?
Appstack simplifies the process for mobile apps looking to improve the performance and attribution of their paid ads by providing three main infrastructure services:
1. **Links and redirection:** ad links (also known as tracking links) to measure user engagement with ads and automatically redirect users to the app store with the lowest latency.
2. **Attribution service:** accurately matches the correct users to the appropriate ads using the Appstack SDK.
3. **Signal engineering:** optimizes the process of enriching signals and forwarding them in real time to ad networks to improve campaign performance.
These are the steps to start running enhanced app campaigns powered by Appstack:
After receiving an invitation to join Appstack, companies can create their organizations and add their mobile apps. [Click here](https://cal.com/appstack/appstack-demo) to request access.
Use the correct API key to configure the Appstack SDK (available for [Swift](/SDKs/swift), [Kotlin](/SDKs/kotlin), [React Native](/SDKs/react-native), [Flutter](/SDKs/flutter), and [Unity](/SDKs/unity)) as documented.
The [Appstack CLI](/tooling/cli) can do this for you — `npx appstack-cli integrate` detects your project, wires up the SDK, and verifies the result.
Complete the connection process for certified integrations such as [Meta Ads](/Integrations/meta-ads), [Google Ads](/Integrations/google-ads), [TikTok Ads,](/Integrations/tiktok-ads) [Apple Ads](/Integrations/apple-ads), etc.
Follow the instructions to successfully run the first enhanced app campaign.
## Why use Appstack?
Mobile apps looking to scale their paid ads, improve profitability, unlock accurate attribution, target specific audiences, or reach new inventories can benefit from Appstack's:
1. **Superior attribution:** real-time device-level attribution data at the ad level and enriched signals using end-to-end encryption.
2. **Improved performance:** benefit from a lightspeed redirection experience and top-tier infrastructure to boost your ad campaigns' ROAS.
3. **Campaign control:** deploy new campaign types with granular control over ad placements, including search-to-app on Google Ads and TikTok Ads, YouTube Shorts, and display.
4. **Expanded reach:** unlock new inventories by discovering new segments and audiences with web-based campaigns.
5. **Zero overhead:** no need to build web funnels, data duplication issues, manage global tax compliance, or worry about financial and legal complexities.
## Shortcuts
Learn everything you need to get started with enhanced app campaigns.
Connect Appstack with Meta, TikTok, Google, and more.
# Links
Source: https://docs.appstack.tech/links
The 'Links' product lets you generate a single URL that you can share anywhere with content creators, on your website, in an email, as a QR code, and many more places.
It automatically routes every user to the correct app store (iOS or Android). From that moment on, every click, install, in-app event, and revenue signal attributed to that link is measured and surfaced in your Appstack dashboard.
> If you've ever needed to know which content creator is bringing the most revenue to your app in the long term, 'Links' is built for that.
## How does it work?
When you create a 'Link' in Appstack, you get a URL that does three things automatically:
1. **Detects the user's device.** When someone taps the link, Appstack identifies whether they're on iOS or Android and redirects them to the correct app store listing.
2. **Captures the source.** The link carries the `media_source` and `campaign_name` parameters you defined when creating it. These travel with the user through the install flow.
3. **Attributes the install and everything after.** Once the user installs your app, Appstack attributes the install and all subsequent in-app and revenue events back to that link.
**How Links know where to redirect**
Every 'Link' is created inside a 'Project.' Each Project has its iOS and Android apps configured once, at the Project level.
When a user taps a 'Link', Appstack automatically detects their device and redirects them to the iOS or Android app set in that Project. You don't pick destinations per Link; the Project handles it.
## Why use Links
* **One link, both platforms.** You don't need to manage separate iOS and Android URLs. Share one link; Appstack handles the routing.
* **Full-funnel attribution.** Measure not just installs, but in-app events and revenue tied to each source.
* **Built for creator and influencer campaigns.** Hand a unique link to each creator and see exactly what each one delivered, without spreadsheets or guesswork.
* **Works across any channel.** Email, web, social, QR codes, anywhere you can paste a URL, 'Links' work.
* **Clean filtering in the dashboard.** Slice results by `media_source` and `campaign_name` to compare performance across partners and initiatives.
## Creating a Link
1. Open the Appstack dashboard and go to the '**Links'** page.
2. Click '**Create Link'** to start the creation flow.
3. Click on 'Select experience type' and, depending on your goal, different media sources will be enabled.
4. Click on 'Select media source' to attribute all clicks, installs, and in-app events.
5. Add a 'Campaign name' to your link. The campaign name will be the way to locate your link performance in the 'Dashboards' page.
6. Copy the link URL and share it to start measuring the results.
### Tips for naming
* Keep `media_source` consistent across the logic behind each link. Consistent naming makes dashboard filtering much cleaner.
* Use `campaign_name` to differentiate initiatives within a source. One creator running two different promos? Same`media_source`, different `campaign_name`.
## Viewing results
All 'Links' performance lives in the Appstack dashboard.
1. Open the **Dashboard** section.
2. Use the **filters** at the top to slice by:
* `media_source` — to see how a specific channel or creator is performing.
* `campaign_name` — to isolate a specific initiative.
3. Review the metrics that matter to you:
* **Installs**: how many users installed your app via the link?
* **In-app events**: actions users took after installing (signups, purchases, custom events, etc.).
* **Revenue**: revenue attributed to users who came in through that link.
## Common use cases
* **Creator and influencer campaigns**. Generate a unique link per creator. See which ones actually drive installs and revenue, not just impressions.
* **Email and newsletter campaigns**. Embed a link in your CTA and measure downstream revenue from each send.
* **Paid acquisition**. Tag links per ad network or campaign and compare attribution against your ad platforms.
* **Organic and PR**. Share a link in a podcast, press release, or social post and quantify the install lift.
## FAQ
No. The link handles device detection and routing automatically. The user just taps and lands in the right app store.
The link will still route them appropriately, and attribution rules will apply as configured.
Yes. There's no practical limit; create one per creator, campaign, channel, or send.
Every 4 hours, install and event data flows into the dashboard, with filterable views ready as soon as data starts coming in. We're working to improve this cadence soon.
Every 'Link' is created inside a 'Project.' Each Project has its iOS and Android apps configured once, at the Project level.
When a user taps a 'Link', Appstack automatically detects their device and redirects them to the iOS or Android app set in that Project. You don't pick destinations per Link; the Project handles it.
# MCP
Source: https://docs.appstack.tech/products/mcp
Connect Claude to your Appstack Analytics data: installs, ad spend, revenue, and attribution, over MCP.
**Beta.** MCP access is opt-in per organization right now. If a project's MCP key won't generate, or the OAuth connect flow says "not enabled for your organization," ask an Appstack admin to turn it on for your org.
Appstack's MCP server lets Claude (or any MCP-compatible client) query your app's attribution and analytics data directly, including installs, ad spend, revenue, and campaign performance, scoped to a single project.
## Prerequisites
* An Appstack account, with MCP access enabled for your organization (beta, so ask an admin).
* A project in Appstack (bundles up to one iOS + one Android app) whose data you want to query.
## Connect via OAuth (Claude Desktop, Claude.ai, ChatGPT desktop)
This is the self-serve, recommended path, with no key to copy or store.
**Claude Desktop / Claude.ai:**
1. Go to **Settings → Connectors → Add custom connector**.
2. Paste the server URL: `https://mcp.appstack.tech/mcp`
3. Log in with your Appstack account when prompted.
4. Pick the project you want to grant access to (auto-selected if you only have one eligible project).
**ChatGPT desktop:**
1. Go to **Settings → Plugins → Add → Add a MCP server**.
2. Name it, set the type to **Streamable HTTP**, and paste the server URL: `https://mcp.appstack.tech/mcp`
3. Save, then click **Authenticate** and log in with your Appstack account.
Both discover everything else automatically (client registration, PKCE, token issuance), so there is nothing to configure manually. Appstack staff logging in with a platform-admin account see an "all organizations" consent screen instead of a project picker.
## Connect via API key (Claude Code, scripts)
Each project has its own MCP key, generated from that project's settings page in the Appstack dashboard (**Project → Settings → MCP**). The key only exposes that project's data.
```bash theme={null}
claude mcp add --transport http appstack-analytics \
https://mcp.appstack.tech/mcp \
--header "Authorization: Bearer "
```
Or in Claude Desktop's config file:
```json theme={null}
{
"mcpServers": {
"appstack-analytics": {
"url": "https://mcp.appstack.tech/mcp",
"headers": {
"Authorization": "Bearer "
}
}
}
}
```
Regenerate the key any time from the same settings page. This immediately invalidates the old one.
## Get better results with a skill
Claude can use these tools with no extra setup, but it has to guess the right
call order and exact metric/dimension names on its own. Installing the
[`appstack-mcp` skill](https://github.com/appstack-tech/appstack-skills)
teaches it those conventions up front, so you get fewer wasted calls and no
made-up measure names.
```bash theme={null}
claude plugin marketplace add appstack-tech/appstack-skills
claude plugin install appstack@appstack-plugins
```
See the [appstack-skills README](https://github.com/appstack-tech/appstack-skills) for Codex and Cursor install instructions.
## Available tools
| Tool | What it does |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `whoami` | Identify which app(s) the connection has access to, each with its own organization. Call this first. |
| `find_organization` | Search organizations by (partial) name, with each match's apps. Only relevant for Appstack staff, since platform-admin access spans every organization with no other way to look one up by name. |
| `list_metrics` | List the metrics and dimensions available to query. |
| `list_dimension_values` | List valid values for a dimension (countries, media sources, app names, and so on) before filtering on it. |
| `query_metrics` | Run a scoped query with measures, dimensions, filters, and a date range, and get real numbers back. |
| `list_integrations` | Every integration (ad network, MMP, subscription platform, and so on) connected to your apps/projects, with its current status. |
| `get_onboarding_checklist` | Completion of the five onboarding steps for a project. |
| `list_dashboards` / `get_dashboard` | Saved dashboards and their widgets' configured measures/dimensions/filters. |
| `list_user_journeys` / `get_user_journey_filters` / `get_user_journey` | Per-user event timelines, the "Appstack ID history" for a specific user. |
| `create_link` / `list_links` / `get_link` / `update_link` / `delete_link` | **Write.** Create, read, update, and delete standard links. Ad-network links aren't supported here, so manage those from the dashboard. These are the only tools that change anything; everything else only reads. |
Ask things like:
* *"What was our ROAS by media source last month?"*
* *"Show me installs and revenue by country for the last 30 days."*
* *"What are the possible values for media\_source?"*
* *"Is our RevenueCat integration connected and active?"*
* *"Create a standard link for this app with campaign name 'summer-promo'."*
## Troubleshooting
**"MCP isn't enabled for your organization yet."**
MCP is in beta, opt-in per organization. Ask an Appstack admin to turn it on.
**The OAuth consent screen doesn't show the project I expected.**
Only projects in organizations with MCP access enabled appear in the picker, and only ones you're a member of.
**A tool call returns an error instead of data.**
Call `whoami` first to confirm which app(s) are actually in scope for your connection, and `list_metrics` / `list_dimension_values` before guessing measure or dimension names. A made-up name fails rather than silently returning nothing.
## Limits & notes
* Requests are rate-limited per credential (120 req/60s by default). A `429` response includes a `Retry-After` header.
* OAuth access tokens are short-lived (\~1 hour) and refresh automatically, so never copy-paste one.
* A project's MCP key only exposes that project's data (up to one iOS + one Android app), never the rest of your organization.
* Regenerating a project's key immediately breaks any client still using the old one.
* `create_link`/`update_link`/`delete_link` actually change data, so review what Claude is about to do before confirming, same as you would for any tool with write access.
# CLI
Source: https://docs.appstack.tech/tooling/cli
Automate the Appstack SDK lifecycle — install, audit, and upgrade — with your coding agent.
**Beta.** The CLI is published on npm as `appstack-cli`. `review`, `--dry-run`, `--json`, and `--skill` never modify files, so they are safe to run against any project. `integrate` and `upgrade` always ask for confirmation before writing.
The Appstack CLI does what the Mobile SDK pages describe, without you doing it by
hand. It detects your project, runs deterministic checks first, then hands a
framework-specific playbook to a coding agent — Claude Code or Codex — running
headlessly.
It is deliberately scoped to the SDK lifecycle. It never creates Appstack
organizations, apps, campaigns, links, or dashboard resources — do those in the
dashboard.
## Requirements
* **Node.js 20** or newer.
* **Claude Code or Codex**, installed and authenticated. Only the agent-backed
runs need one — `--json`, `--skill`, and `--dry-run` work without any agent.
* A **Swift/iOS**, **Kotlin/Android**, **React Native**, **Flutter**, or **Unity**
project.
## Install
Run it once, without installing anything:
```bash theme={null}
npx appstack-cli review
```
Or install it globally, which gives you the shorter `appstack` command:
```bash theme={null}
npm install -g appstack-cli
appstack review
```
## Interactive mode
Run `appstack` with no arguments in an interactive terminal to open the guided
walkthrough. It detects the apps in your repository, lets you pick a workflow and
an execution mode, masks API keys as you type them, and asks for confirmation
before anything is written to disk.
```bash theme={null}
appstack
```
## Commands
Installs and configures the SDK using your app's existing package manager and startup architecture.
Audits an existing integration. Deterministic checks plus a read-only semantic pass. Never writes.
Resolves the latest stable release, reads the migration surface, updates dependencies and lockfiles, then verifies the app.
### integrate
Installs the SDK and wires up the `configure` call in the right place for your
project's architecture.
```bash theme={null}
appstack integrate
```
Pass the API key for a single-platform app with `--api-key`, or set
`APPSTACK_API_KEY`. See [API keys](#api-keys) for cross-platform apps.
### review
The one to reach for first. It reports what is actually wired up versus what the
docs expect, and it cannot change your files.
```bash theme={null}
appstack review
```
### upgrade
Targets the latest stable release from the official registry by default. Pin a
specific version with `--to`:
```bash theme={null}
appstack upgrade
appstack upgrade --to 2.6.0 --dry-run
```
## Flags
| Flag | What it does |
| -------------------------- | -------------------------------------------------------------------------- |
| `--skill` | Print the composed, project-specific playbook instead of running an agent. |
| `--dry-run` | Inspect and show the plan without changing any files. |
| `--json` | Print the deterministic inspection as JSON. Useful in CI. |
| `--framework ` | Select one app in a repository where more than one is detected. |
| `--install-dir ` | Inspect a directory other than the current one. |
| `--driver ` | Force a coding-agent backend instead of the auto-detected one. |
| `--to ` | `upgrade` only. Target SDK version. Defaults to the latest stable. |
## API keys
The CLI never creates keys, and it instructs the agent not to print key values.
Grab the key for each app from its project settings in the Appstack dashboard.
For a single-platform app:
```bash theme={null}
appstack integrate --api-key
```
For a cross-platform app, pass both keys as flags or as environment variables:
```bash theme={null}
APPSTACK_IOS_API_KEY=... APPSTACK_ANDROID_API_KEY=... appstack integrate
```
```bash theme={null}
appstack integrate --ios-api-key --android-api-key
```
## Continuous integration
`--json` gives you the deterministic inspection with no agent involved, so it
runs anywhere Node does:
```bash theme={null}
npx appstack-cli review --json
```
## No coding agent installed?
Use `--skill` to print the full playbook for your specific project and paste it
into whichever assistant you already use:
```bash theme={null}
appstack integrate --skill
```
This is the same content as the copyable prompt at the top of each SDK page, but
resolved against your actual project rather than generic.
## How it works
The CLI is a thin, deterministic harness — project detection, version
resolution, safety boundaries, and verification contracts. The Appstack
integration expertise lives in a bundled skill, sourced from
[`appstack-tech/appstack-skills`](https://github.com/appstack-tech/appstack-skills),
with one reference per platform. Claude Code is preferred when both agents are
available.
## Troubleshooting
**"No coding agent found."**
Install Claude Code or Codex, or run the command with `--skill` and paste the
playbook into your agent.
**"`codex` is not available."**
You passed `--driver codex` but only Claude Code was detected, or neither was.
Drop the flag to use whatever is installed.
**"No installed Appstack SDK version was detected. Run `appstack integrate` instead."**
You ran `upgrade` on a project that has no Appstack SDK yet.
**More than one app detected.**
Pass `--framework` to pick one, or `--install-dir` to point at a single app's
directory.
## Source
The CLI is open source at
[`appstack-tech/appstack-cli`](https://github.com/appstack-tech/appstack-cli).