# SmartAppPush — Mobile Integration Guide (Firebase Cloud Messaging)

> **For AI coding tools.** Drop this file into your mobile app project and reference it when implementing SmartAppPush event tracking, push open reporting, and device token refresh for apps using Firebase Cloud Messaging (FCM).
>
> Official docs: https://smartapppush.ai/docs

## What is SmartAppPush?

SmartAppPush is a push notification platform for mobile and web apps. It handles user tracking, device management, campaign scheduling, delivery, open tracking, and analytics. Your mobile app integrates by sending **events** (user actions), reporting **push opens** (notification taps), and handling **device token refresh** (FCM token rotation) via HTTP endpoints.

**This guide is for apps using Firebase Cloud Messaging (FCM) as the push delivery provider.**

Key things to know about the FCM integration:
- The `device_token` you send is the **FCM registration token** — it can rotate and needs refresh handling
- SmartAppPush delivers pushes as **data-only FCM messages** (not `notification` messages) — your app must build and display notifications itself
- The `POST /devices/refresh-token` endpoint is used to update rotated FCM tokens in-place

---

## Configuration

You need these values from the SmartAppPush dashboard:

| Key | Where to find | Usage |
|-----|--------------|-------|
| `tenant_id` | Dashboard → Account Settings | Your account/organization ID |
| `api_key` | Dashboard → App → Settings | Identifies your app. Safe for client-side use |

**Base URL:** `https://api.smartapppush.ai`

> **Do not put `server_key` in your mobile app.** The `server_key` is for backend-to-backend calls only. The `api_key` can only write events and report push opens — it cannot send pushes or access data.

---

## Step 1: Event Tracking

Events are the foundation. Every event you send does three things at once:
1. Records the user behavior (for analytics and campaign targeting)
2. Registers or updates the user profile (language, country, timezone, platform)
3. Registers or updates the device (token + push permission status)

No separate user/device registration calls needed — just send events.

### Endpoint

```
POST https://api.smartapppush.ai/events
Content-Type: application/json
```

Authentication: `api_key` in the request body (no Authorization header).

### Request Example

```json
{
  "tenant_id": "your_tenant_id",
  "api_key": "your_app_api_key",
  "app_user_id": "user_123",
  "event_name": "purchase_completed",
  "event_time": "2026-03-29T14:30:00+03:00",
  "device_token": "fcm_registration_token_here",
  "platform": "android",
  "push_permission_granted": true,
  "timezone": "Europe/Istanbul",
  "language": "tr",
  "country": "TR",
  "session_id": "sess_abc123",
  "params": {
    "product_id": "SKU-9001",
    "amount": 49.99,
    "currency": "USD"
  },
  "context": {
    "app_version": "2.1.0",
    "os_version": "Android 14",
    "screen": "checkout"
  }
}
```

**About `device_token` for FCM:** This is the FCM registration token obtained from `FirebaseMessaging.getInstance().token` (Android) or `Messaging.messaging().token` (iOS via Firebase SDK). This token **can rotate** — see [Step 4: Device Token Refresh](#step-4-device-token-refresh) for how to handle rotation.

### cURL Example

```bash
curl -X POST https://api.smartapppush.ai/events \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "<tenant_id>",
    "api_key": "<api_key>",
    "app_user_id": "user_123",
    "event_name": "app_open",
    "event_time": "2026-03-05T10:00:00Z",
    "device_token": "fcm_token_abc",
    "language": "en",
    "country": "US",
    "session_id": "sess_001",
    "platform": "android",
    "push_permission_granted": true,
    "timezone": "Europe/Amsterdam",
    "context": {"screen": "home"},
    "params": {"source": "organic"}
  }'
```

### Response

```json
// 200 OK
{
  "fingerprint": "a1b2c3d4e5f6..."
}
```

The fingerprint is a SHA-256 hash used for deduplication. Log it for debugging.

### Field Reference

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `tenant_id` | string | **Yes** | Your account ID (from the dashboard) |
| `api_key` | string | **Yes** | Your app's API key |
| `app_user_id` | string | No | Your app's internal user ID. If empty, an anonymous ID is generated as `anon:{device_token}` |
| `event_name` | string | **Yes** | Event name (e.g., `app_opened`, `purchase_completed`, `level_up`) |
| `event_time` | string | **Yes** | RFC 3339 timestamp with timezone offset (e.g., `2026-03-29T14:30:00+03:00`). UTC like `2026-03-29T11:30:00Z` is also valid |
| `device_token` | string | **Yes** | FCM registration token — the token from `onNewToken()` (Android) or `messaging:didReceiveRegistrationToken:` (iOS). This token can rotate and needs refresh handling |
| `platform` | string | **Yes** | One of: `android`, `ios`, `web` |
| `push_permission_granted` | boolean | **Yes** | Whether the user currently has push notifications enabled. **Defaults to `false` if omitted — device will not receive any pushes** |
| `timezone` | string | **Yes** | IANA timezone (e.g., `Europe/Istanbul`, `America/New_York`) |
| `language` | string | **Yes** | ISO 639-1 language code (e.g., `en`, `tr`, `de`) |
| `country` | string | **Yes** | ISO 3166-1 alpha-2 country code (e.g., `US`, `TR`, `DE`) |
| `session_id` | string | **Yes** | Session identifier for grouping events within a single app session |
| `params` | object | No | Event-specific data (product ID, amount, category). Used for campaign targeting and analytics |
| `context` | object | No | Environment metadata (app version, OS version, screen). Stored for reference, **not used** in targeting rules |

### Error Responses

| Status | Meaning |
|--------|---------|
| `400` | Validation error — missing required fields, invalid format, or unknown API key. Response contains `errors` array with details |
| `503` | Server overloaded — retry after a short delay |

---

## Step 2: Push Open Tracking

When SmartAppPush delivers a push notification, it includes a `tracking_token` (UUID) in the push payload's `extra_data`. Your app reports this token back when the user taps the notification.

### Endpoint

```
POST https://api.smartapppush.ai/pushes/opened
Content-Type: application/json
```

Authentication: `api_key` in the request body.

### Request Example

```json
{
  "tenant_id": "your_tenant_id",
  "api_key": "your_app_api_key",
  "tracking_token": "uuid-from-push-extra-data"
}
```

### cURL Example

```bash
curl -X POST https://api.smartapppush.ai/pushes/opened \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "<tenant_id>",
    "api_key": "<api_key>",
    "tracking_token": "<uuid_from_extra_data.tracking_token>"
  }'
```

### Response

```json
// 200 OK
{
  "success": true
}
```

### Field Reference

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `tenant_id` | string | **Yes** | Your account ID |
| `api_key` | string | **Yes** | Your app's API key |
| `tracking_token` | string | **Yes** | UUID from the push notification's `extra_data.tracking_token` |

### Implementation Flow

1. Your app receives a push notification payload
2. Extract `tracking_token` from `extra_data` in the push payload
3. When the user **taps** the notification and the app opens, POST the token to this endpoint
4. Handle deep linking using other fields in `extra_data` (e.g., `screen`, `order_id`)

### Pseudo-code

```javascript
onNotificationOpened(notification) {
  const trackingToken = notification.extra_data?.tracking_token;

  if (trackingToken) {
    fetch("https://api.smartapppush.ai/pushes/opened", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        tenant_id: SMARTAPPPUSH_TENANT,
        api_key: SMARTAPPPUSH_API_KEY,
        tracking_token: trackingToken
      })
    });
  }

  // Handle deep linking
  navigateToScreen(notification.extra_data?.screen);
}
```

### Error Responses

| Status | Meaning |
|--------|---------|
| `400` | Missing required fields (tenant_id, api_key, tracking_token) |
| `401` | Invalid tenant_id or api_key |

---

## Step 3: Incoming Push Notification Payload

When SmartAppPush delivers a push via FCM, the notification arrives as a **data-only message** (not a `notification` message). This gives your app full control over how the notification is displayed and handled — including when the app is in the background or killed.

### FCM Payload Structure

```json
{
  "data": {
    "title": "Your order has shipped!",
    "body": "Order #4521 is on its way.",
    "tracking_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "extra_data": "{\"order_id\":\"4521\",\"screen\":\"order_tracking\",\"deep_link\":\"/orders/4521\"}"
  }
}
```

Key points:
- `title` and `body` are in `data`, not `notification` — your app must build and display the notification itself
- `tracking_token` is present for SmartAppPush-delivered pushes. Save it for open tracking
- `extra_data` is a **JSON string** (not a nested object) — parse it to get the key-value pairs
- `extra_data` contents are defined by whoever created the push (campaign creator or Developer API caller) — there is no fixed schema beyond `tracking_token`

### Android — Parsing in FirebaseMessagingService (Kotlin)

```kotlin
class SmartAppPushMessagingService : FirebaseMessagingService() {

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val data = remoteMessage.data
        val title = data["title"] ?: return
        val body = data["body"] ?: return
        val trackingToken = data["tracking_token"]
        val extraDataRaw = data["extra_data"]
        val extraData: Map<String, String> = try {
            extraDataRaw?.let {
                JSONObject(it).let { json ->
                    json.keys().asSequence().associateWith { key -> json.getString(key) }
                }
            } ?: emptyMap()
        } catch (e: Exception) { emptyMap() }

        // Build and show the notification
        // Pass trackingToken + extraData via Intent extras for open tracking and deep linking
        showNotification(title, body, trackingToken, extraData)
    }
}
```

### iOS — Handling in AppDelegate / UNUserNotificationCenterDelegate (Swift)

```swift
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    let userInfo = response.notification.request.content.userInfo

    let trackingToken = userInfo["tracking_token"] as? String
    let extraDataString = userInfo["extra_data"] as? String
    let extraData: [String: Any] = {
        guard let data = extraDataString?.data(using: .utf8),
              let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else { return [:] }
        return json
    }()

    // Report open tracking
    if let token = trackingToken {
        SmartAppPushClient.reportPushOpened(trackingToken: token)
    }

    // Handle deep linking
    if let screen = extraData["screen"] as? String {
        DeepLinkRouter.navigate(to: screen, params: extraData)
    }

    completionHandler()
}
```

### extra_data Keys

`extra_data` has **no fixed schema**. The contents depend on what was set when the push was created (via campaign or Developer API). Common patterns:

| Key | Example | Purpose |
|-----|---------|---------|
| `screen` | `"order_tracking"` | Which screen to navigate to |
| `order_id` | `"4521"` | Entity ID for the deep link target |
| `deep_link` | `"/orders/4521"` | Full deep link path |
| `chat_id` | `"conv_789"` | Chat conversation to open |
| `url` | `"https://..."` | External URL to open |

The only key SmartAppPush injects is `tracking_token`. Everything else is developer-defined. Your deep link router should handle unknown keys gracefully.

---

## Step 4: Device Token Refresh

> **This section is specific to Firebase Cloud Messaging (FCM).** FCM registration tokens can rotate — when this happens, the old token becomes invalid and SmartAppPush needs the new one to continue delivering pushes.

### When FCM Tokens Rotate

FCM tokens can change in these situations:
- App is installed or reinstalled
- User clears app data
- App is restored on a new device
- Firebase periodically rotates the token

When this happens, Firebase triggers `onNewToken()` (Android) or `messaging:didReceiveRegistrationToken:` (iOS).

### Endpoint

```
POST https://api.smartapppush.ai/devices/refresh-token
Content-Type: application/json
```

Authentication: `api_key` in the request body.

Updates the device token in-place without creating a new device record.

### Request Example

```json
{
  "tenant_id": "your_tenant_id",
  "api_key": "your_app_api_key",
  "old_token": "expired_fcm_registration_token",
  "new_token": "fresh_fcm_registration_token",
  "app_user_id": "user_123",
  "platform": "android"
}
```

### cURL Example

```bash
curl -X POST https://api.smartapppush.ai/devices/refresh-token \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_id": "<tenant_id>",
    "api_key": "<api_key>",
    "old_token": "expired_fcm_registration_token",
    "new_token": "fresh_fcm_registration_token",
    "app_user_id": "user_123",
    "platform": "android"
  }'
```

### Response

```json
// 200 OK
{
  "success": true
}
```

### Field Reference

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `tenant_id` | string | **Yes** | Your account ID |
| `api_key` | string | **Yes** | Your app's API key |
| `old_token` | string | **Yes** | The previous FCM registration token being replaced |
| `new_token` | string | **Yes** | The new FCM registration token from `onNewToken()` |
| `app_user_id` | string | No | Your internal user ID (used for conflict resolution if new token already exists) |
| `platform` | string | No | One of: `android`, `ios`, `web` |

### Error Responses

| Status | Body | Meaning |
|--------|------|---------|
| `400` | `{"errors": ["Missing old_token", "Missing new_token"]}` | Missing required fields |
| `400` | `{"errors": ["old_token and new_token must be different"]}` | Same token sent for both fields |
| `401` | `{"error": "invalid tenant_id or api_key"}` | Invalid credentials |
| `404` | `{"error": "device not found"}` | Old token not found — see fallback below |
| `409` | `{"error": "new token already registered to a different user"}` | Token conflict |

### Fallback: Old Token Not Found

If you get a `404` (e.g., app was reinstalled and the old token was never registered), send a regular event via `POST /events` with the new token instead — it will create the device automatically.

### Android — onNewToken (Kotlin)

```kotlin
override fun onNewToken(token: String) {
    // FCM only — send an event with the new device_token to update SmartAppPush
    SmartAppPushClient.sendEvent(
        eventName = "app_opened",
        deviceToken = token,  // new FCM registration token
        params = mapOf("source" to "token_refresh")
    )
}
```

If you persist the previous token locally (e.g., SharedPreferences), you can also call `POST /devices/refresh-token` with both old and new tokens for an immediate in-place update.

### iOS — didReceiveRegistrationToken (Swift)

```swift
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
    guard let token = fcmToken else { return }
    SmartAppPushClient.sendEvent(
        eventName: "app_opened",
        deviceToken: token,  // new FCM registration token
        params: ["source": "token_refresh"]
    )
}
```

---

## Android Implementation Notes

### Device Token Refresh

When `onNewToken()` fires (app install, token rotation, or user clears app data), the old token becomes invalid. SmartAppPush needs the new token to deliver pushes.

**Rule: Send an `app_opened` event immediately with the new token.** This updates the device record in SmartAppPush.

```kotlin
override fun onNewToken(token: String) {
    SmartAppPushClient.sendEvent(
        eventName = "app_opened",
        deviceToken = token,
        params = mapOf("source" to "token_refresh")
    )
}
```

If the user is not yet identified, omit `app_user_id` — the anonymous profile will be updated.

### session_id Lifecycle

**Concrete rule: Use `ProcessLifecycleOwner` to detect foreground/background transitions.**

```kotlin
class SessionManager : DefaultLifecycleObserver {
    var sessionId: String = UUID.randomUUID().toString()
        private set

    override fun onStart(owner: LifecycleOwner) {
        // App came to foreground — new session
        sessionId = UUID.randomUUID().toString()
    }
}

// In Application.onCreate():
ProcessLifecycleOwner.get().lifecycle.addObserver(sessionManager)
```

- Generate a new UUID when `ProcessLifecycleOwner` reports `ON_START` (app enters foreground)
- Reuse the same `session_id` for all events until the next `ON_START`
- This means: backgrounding for a few seconds and returning keeps the same session. A full background → foreground transition creates a new session
- This is the standard Android definition of a "session" and matches analytics tools like Firebase Analytics

### Event Queue and Offline Handling

Events and open tracking calls should survive network failures and app kills. Use a local queue:

```
1. Event happens → write to local DB (Room) or file
2. Attempt HTTP send on background thread (WorkManager or coroutine)
3. On success (200) → delete from queue
4. On network error → leave in queue, retry later
5. On 400 → delete from queue (bad request, retrying won't help)
6. On 503 → leave in queue, retry with backoff
```

**WorkManager is recommended** for reliable delivery because:
- Survives app kills and device reboots
- Respects battery/doze constraints
- Handles retry with backoff natively via `Result.retry()`

Open tracking is especially important to queue — the user may tap a notification while offline (e.g., airplane mode). Queue the tracking call and flush when connectivity returns.

### Notification Display (Background/Killed State)

Since SmartAppPush sends **data-only messages**, your `FirebaseMessagingService.onMessageReceived()` is called even when the app is in the background or killed. You must:

1. Build the notification yourself using `NotificationCompat.Builder`
2. Attach `tracking_token` and `extra_data` as Intent extras on the notification's `PendingIntent`
3. When the user taps → your Activity receives the Intent → extract extras → report open tracking + deep link

Do **not** rely on FCM's automatic notification display — it only works with `notification` payloads, which SmartAppPush does not use.

---

## iOS Implementation Notes

### Device Token Refresh

The FCM SDK handles APNs token registration and gives you an FCM token. Use that FCM token as `device_token` in events.

When the token refreshes, the FCM SDK calls `messaging:didReceiveRegistrationToken:`. Handle it the same way as Android — send an `app_opened` event with the new token.

```swift
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
    guard let token = fcmToken else { return }
    SmartAppPushClient.sendEvent(
        eventName: "app_opened",
        deviceToken: token,
        params: ["source": "token_refresh"]
    )
}
```

### session_id Lifecycle

**Concrete rule: Generate a new session_id when the app enters foreground from a non-active state.**

```swift
// In AppDelegate or SceneDelegate
func sceneDidBecomeActive(_ scene: UIScene) {
    if !SessionManager.shared.isActive {
        SessionManager.shared.startNewSession()  // new UUID
    }
}

func sceneDidEnterBackground(_ scene: UIScene) {
    SessionManager.shared.isActive = false
}
```

Or use `UIApplication.didBecomeActiveNotification` / `didEnterBackgroundNotification` if not using scenes.

### Offline Queue

Use a lightweight local persistence (UserDefaults for small queues, Core Data or a file for larger ones) to queue events and open tracking calls when offline. Flush when `NWPathMonitor` reports connectivity.

---

## Rate Limits

| Endpoint | Limit |
|----------|-------|
| `POST /events` | 10 requests/second per IP |
| `POST /pushes/opened` | 10 requests/second per IP |

These limits are generous for individual devices. You will only hit them if you accidentally send in a tight loop. The local queue approach naturally prevents this.

### Retry Strategy

| Scenario | Action |
|----------|--------|
| Network error | Queue locally, retry with exponential backoff (1s, 2s, 4s, max 60s). Max 5 retries, then keep in queue for next app launch |
| `200` | Success. Remove from queue |
| `400` | Bad request. Remove from queue, log error for debugging — retrying won't help |
| `401` | Invalid credentials. Remove from queue, log error — check your `tenant_id`/`api_key` config |
| `503` | Server overloaded. Retry with exponential backoff (1s, 2s, 4s). Max 3 retries |

---

## Do / Don't Rules

### Do

- **Send `push_permission_granted`** with every event. Check the OS-level notification permission each time the app launches or resumes. If omitted, it defaults to `false` and the device will not receive pushes.
- **Generate a new `session_id`** (UUID) on each app cold start. Reuse it for all events until the app is backgrounded or terminated.
- **Send events on a background thread.** Do not block the UI waiting for the HTTP response.
- **Send `app_user_id` as soon as you know it** (after login or signup). Before that, omit it — SmartAppPush auto-generates `anon:{device_token}`.
- **Use `params` for data you want to target by** (product_id, amount, category). Use `context` for debugging metadata (app_version, OS, screen).
- **Include timezone offset in `event_time`** (e.g., `+03:00` or `Z` for UTC).
- **Store `tenant_id` and `api_key` in your app's configuration** (environment config, build config, or remote config). Do not hardcode inline.
- **Retry on network failure.** Events are deduplicated by fingerprint, so sending the same event twice is safe — the duplicate is silently ignored.
- **Handle `onNewToken()` immediately.** When FCM rotates the token, send an `app_opened` event with the new token right away. If you persist the old token, also call `POST /devices/refresh-token`.
- **Build notifications manually** in `onMessageReceived()`. SmartAppPush sends data-only FCM messages — your app is responsible for creating and displaying the notification.
- **Create notification channels** in `Application.onCreate()` for Android 8+.
- **Persist the current FCM token** in SharedPreferences so you can provide `old_token` when `onNewToken()` fires.

### Don't

- **Do not put `server_key` in your mobile app.** It is a secret for server-to-server calls only.
- **Do not skip `push_permission_granted`.** If the user revokes push permission but you don't report `false`, SmartAppPush will attempt to deliver pushes that will fail.
- **Don't batch events.** Send each event individually as it happens. SmartAppPush handles high-throughput ingestion on its side.
- **Don't send open tracking for every push received** — only when the user **taps** the notification.
- **Don't rely on FCM automatic notification display** — it only works with `notification` payloads, which SmartAppPush does not use.
- **Don't ignore `onNewToken()` callbacks** — missed token refreshes mean the device becomes unreachable for push delivery.

---

## Anonymous Users & Identity Merge

Before the user logs in, you may not have a `app_user_id`. That's fine:

1. **Omit `app_user_id`** (or send empty string) in events before login
2. SmartAppPush creates an anonymous profile with ID `anon:{device_token}`
3. All events and push history are attributed to this anonymous profile
4. **When the user logs in**, start sending events with their real `app_user_id`
5. SmartAppPush automatically:
   - Detects the device was linked to an anonymous user
   - Re-attributes all past events to the identified user
   - Re-links push history
   - Deletes the orphaned anonymous profile

No additional API calls needed. Just start including `app_user_id` once you know it.

---

## Event Deduplication

Every event gets a SHA-256 fingerprint generated from: `tenant_id`, `app_id`, `app_user_id`, `event_name`, `event_time`, `device_token`, `context`, and `params`.

If you send the same event twice (e.g., network retry), the duplicate is silently ignored. The fingerprint is returned in the response for debugging.

Two events with the same name and time but different `params` are considered distinct (different fingerprints) — this is correct behavior.

---

## System Events Reference

SmartAppPush ships with predefined event names. Use these as-is, or send any custom event name — custom events are automatically discovered from your event stream.

### Onboarding

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `onboarding_displayed` | Onboarding screen shown | `screen_index`, `variant` |
| `onboarding_step_completed` | User completed a step | `step`, `step_name` |
| `onboarding_completed` | User finished onboarding | `duration_seconds`, `steps_count` |
| `onboarding_skipped` | User skipped onboarding | `skipped_at_step` |

### App Lifecycle

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `app_opened` | App opened | `source` (notification, deeplink, organic) |
| `screen_viewed` | User viewed a screen | `screen_name`, `previous_screen` |

### Authentication

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `login_page_displayed` | Login screen shown | |
| `logged_in` | User authenticated | `method` (email, google, apple) |
| `sign_up_started` | User initiated registration | `method` |
| `sign_up_completed` | User finished registration | `method` |
| `logged_out` | User signed out | |

### E-Commerce

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `product_viewed` | Viewed product detail | `product_id`, `category`, `price` |
| `product_list_viewed` | User browsed a product listing | `category`, `list_name`, `item_count` |
| `add_to_cart` | Item added to cart | `product_id`, `quantity`, `price`, `currency` |
| `remove_from_cart` | Item removed from cart | `product_id` |
| `cart_viewed` | User opened cart | `item_count`, `cart_total` |
| `begin_checkout` | Started checkout | `cart_total`, `item_count`, `currency` |
| `checkout_abandoned` | Left checkout incomplete | `cart_total`, `step` |
| `payment_failed` | Payment unsuccessful | `error_code`, `payment_method` |
| `purchase_completed` | Transaction completed | `order_id`, `amount`, `currency`, `item_count` |
| `coupon_applied` | Discount code applied | `coupon_code`, `discount_amount` |
| `coupon_failed` | Coupon code was invalid or expired | `coupon_code`, `reason` |
| `refund_requested` | User initiated refund | `order_id`, `amount`, `reason` |
| `order_status_viewed` | User checked order tracking | `order_id`, `status` |
| `wishlist_added` | Item saved to wishlist | `product_id`, `category` |
| `wishlist_removed` | Item removed from wishlist | `product_id` |

### Search & Discovery

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `search_performed` | User searched | `query`, `results_count` |
| `search_result_clicked` | Tapped a search result | `query`, `result_position`, `result_id` |
| `search_no_results` | Empty search results | `query` |
| `category_browsed` | Browsed a category | `category_name`, `item_count` |
| `filter_applied` | Applied filters | `filter_type`, `filter_value` |

### Subscription & Monetization

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `paywall_displayed` | Paywall shown | `source`, `variant` |
| `paywall_dismissed` | User closed paywall without action | `source`, `time_spent_seconds` |
| `trial_started` | Free trial began | `plan`, `duration_days` |
| `trial_ended` | Trial expired | `plan`, `converted` (boolean) |
| `subscription_started` | Subscription purchased | `plan`, `price`, `currency`, `period` |
| `subscription_renewed` | Subscription renewed | `plan`, `price`, `period` |
| `subscription_cancelled` | Subscription cancelled | `plan`, `reason` |
| `subscription_expired` | Subscription access ended | `plan` |
| `in_app_purchase_completed` | One-time IAP completed | `product_id`, `amount`, `currency` |
| `billing_issue_detected` | Payment method failed | `plan`, `error_code` |

### Push Notifications

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `push_permission_granted` | User allowed push | |
| `push_permission_denied` | User denied push | |
| `push_received` | Push delivered to device | `campaign_id`, `message_id` |
| `push_opened` | User tapped push | `campaign_id`, `message_id` |
| `push_dismissed` | User dismissed push | `campaign_id` |
| `notification_settings_changed` | User modified notification preferences | `channel`, `enabled` |

### User Profile & Preferences

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `profile_completed` | User filled all required profile fields | |
| `profile_updated` | User changed profile information | `fields_updated` |
| `language_changed` | User changed app language | `from_language`, `to_language` |
| `preferences_updated` | User changed app preferences | `preference_key`, `new_value` |

### Content & Engagement

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `content_viewed` | User consumed content | `content_id`, `content_type`, `duration_seconds` |
| `content_completed` | User finished content | `content_id`, `content_type`, `completion_percent` |
| `feature_used` | Engaged with a feature | `feature_name`, `screen_name` |
| `feature_discovered` | First time using feature | `feature_name` |
| `rating_submitted` | User rated something | `target_id`, `target_type`, `rating` |
| `review_submitted` | User wrote a review | `target_id`, `target_type`, `rating` |
| `feedback_submitted` | In-app feedback sent | `feedback_type`, `screen_name` |

### Social & Virality

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `content_shared` | Shared content externally | `content_id`, `share_method` |
| `invite_sent` | Invited someone to app | `invite_method` |
| `referral_completed` | Invited user signed up | `referrer_user_id` |

### Friction & Errors

| Event Name | Description | Useful Params |
|------------|-------------|---------------|
| `error_encountered` | User hit an error | `error_code`, `error_message`, `screen_name` |
| `form_abandoned` | Left form incomplete | `form_name`, `abandoned_at_step` |
| `deep_link_failed` | Deep link failed | `deep_link_url`, `error_reason` |
| `permission_denied` | Denied system permission | `permission_type` |

> Custom events welcome — send any event name and SmartAppPush automatically discovers and registers it for your app.
