# SmartAppPush — Mobile Integration Guide (OneSignal)

> **For AI coding tools.** Drop this file into your mobile app project and reference it when implementing SmartAppPush event tracking and push open reporting for apps using OneSignal as the push delivery provider.
>
> 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) and reporting **push opens** (notification taps) via HTTP endpoints.

**This guide is for apps using OneSignal as the push delivery provider.**

Key things to know about the OneSignal integration:
- The `device_token` you send is the **OneSignal subscription ID** — it is stable and does not rotate when the underlying platform token refreshes
- OneSignal SDK handles notification display — your app does **not** need to build notifications manually
- The `POST /devices/refresh-token` endpoint is **not needed** — OneSignal manages token rotation internally

---

## 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": "onesignal_subscription_id_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 OneSignal:** This is the OneSignal subscription ID obtained from the OneSignal SDK (e.g., `OneSignal.User.pushSubscription.id` on Android/iOS). Unlike FCM registration tokens, the OneSignal subscription ID is **stable** — it does not change when the underlying platform token refreshes. No token refresh handling is needed.

### 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": "onesignal_subscription_id",
    "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** | OneSignal subscription ID — the stable ID from the OneSignal SDK. Does not change when the platform token refreshes |
| `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 additional 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-additional-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_additional_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 additional data |

### Implementation Flow

1. Your app receives a push notification via the OneSignal SDK
2. Extract `tracking_token` from the notification's additional data
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 the additional data (e.g., `screen`, `order_id`)

### Pseudo-code

```javascript
onNotificationOpened(notification) {
  const trackingToken = notification.additionalData?.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.additionalData?.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 OneSignal, the **OneSignal SDK handles notification display**. Your app does not need to build or show notifications manually.

SmartAppPush passes custom data (including `tracking_token`) via OneSignal's additional data mechanism. This data is accessible through the OneSignal SDK's notification click handler.

### How Data Arrives

SmartAppPush sets `tracking_token` and any `extra_data` fields as additional data on the OneSignal notification. The OneSignal SDK delivers this data to your app via its notification callbacks.

Key points:
- The OneSignal SDK handles notification display — you do **not** build notifications manually
- `tracking_token` is present in the additional data for SmartAppPush-delivered pushes. Use it for open tracking
- Other `extra_data` fields (set by campaign creators or Developer API callers) are also delivered via additional data
- There is no fixed schema for `extra_data` beyond `tracking_token`

### Android — OneSignal Click Handler (Kotlin)

```kotlin
// In Application.onCreate()
OneSignal.Notifications.addClickListener { event ->
    val additionalData = event.notification.additionalData
    val trackingToken = additionalData?.optString("tracking_token")

    // Report open tracking
    if (!trackingToken.isNullOrEmpty()) {
        SmartAppPushClient.reportPushOpened(trackingToken)
    }

    // Handle deep linking
    val screen = additionalData?.optString("screen")
    if (!screen.isNullOrEmpty()) {
        DeepLinkRouter.navigate(screen, additionalData)
    }
}
```

### iOS — OneSignal Click Handler (Swift)

```swift
// In AppDelegate or Application init
OneSignal.Notifications.addClickListener { event in
    let additionalData = event.notification.additionalData

    // Report open tracking
    if let trackingToken = additionalData?["tracking_token"] as? String {
        SmartAppPushClient.reportPushOpened(trackingToken: trackingToken)
    }

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

### 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.

---

## Device Token Refresh — Not Required

> **OneSignal manages token rotation internally.** The OneSignal subscription ID you send as `device_token` is stable and does not change when the underlying platform token (FCM or APNs) refreshes.

The `POST /devices/refresh-token` endpoint exists for FCM integrations where tokens can rotate. **OneSignal apps do not need this endpoint.**

You can optionally listen to subscription changes for diagnostic purposes:

```kotlin
// Android — for logging/diagnostics only, not required for SmartAppPush
OneSignal.User.pushSubscription.addObserver { state ->
    Log.d("SmartAppPush", "Subscription changed: ${state.current.id}")
}
```

```swift
// iOS — for logging/diagnostics only, not required for SmartAppPush
OneSignal.User.pushSubscription.addObserver { state in
    print("Subscription changed: \(state.current.id ?? "nil")")
}
```

---

## Android Implementation Notes

### 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.

---

## iOS Implementation Notes

### 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.
- **Initialize the OneSignal SDK early** in `Application.onCreate()` (Android) or `AppDelegate` (iOS).
- **Use `OneSignal.User.pushSubscription.id`** as the `device_token` in SmartAppPush events.
- **Set up `addClickListener`** to handle notification taps and extract `tracking_token` for open tracking.

### 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 call `POST /devices/refresh-token`** — OneSignal manages token rotation internally. This endpoint is for FCM integrations only.
- **Don't build notifications manually** — let the OneSignal SDK handle notification display.

---

## 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.
