---
title: iOS(Swift) - MomentPerks API Integration Guide
slug: iosswift-momentperks-api-integration-guide
icon: {"faIcon":"fa-brands fa-app-store-ios"}
docTags: 
createdAt: 2025-07-25T19:53:36.957Z
---

# Overview

The [MomentPerks API](https://docs.momentscience.com/momentperks-api) allows you to display personalized, performance-driven offers to users directly within your iOS application. This guide outlines the steps required to integrate the API into your app—from fetching offers to rendering UI and tracking user interactions.

**By following this integration guide, you’ll be able to:**

- Fetch real-time, targeted offers based on user context
- Present these offers using your own custom UI or prebuilt reference components
- Track user responses and impressions for reporting and optimization

:::hint{type="success"}
To explore a working example, see the[ MomentScience iOS Example App on GitHub.](https://github.com/AdsPostX/examples/tree/main/ios-native/MomentsAPIDemoApp-iOS)
:::

## Prerequisites

Before you begin the integration process, ensure the following requirements are met:

- **API Key:** Before you start the integration, you must acquire a unique API key. Follow the [instructions provided here](https://docs.momentscience.com/getting-your-api-key) to obtain your key.
- **Minimum iOS Version:** Your project must target iOS`15.0` or higher.

# Integration Steps

The Moments API enables your iOS app to fetch and display personalized offers using a simple POST request and contextual payload. In this section, you’ll implement a utility function to retrieve and normalize offers for use in your UI.

:::hint{type="success"}
For complete details on MomentPerks API, refer to the [MomentPerks API documentation](https://docs.momentscience.com/momentperks-api).
:::

## Step 1:  Fetch Offers

In this step, build a function that sends a POST request to the [MomentPerks API](https://docs.momentscience.com/momentperks-api)&#x20;
(`native/v4/offers.json`) and returns personalized offers based on the given user context.

1. **Build and Validate Input Parameters:** Start by validating optional parameters like loyaltyBoost and creative, then prepare the payload body.

:::CodeblockTabs{indent="2"}
```swift
// Validate parameters
if let loyaltyBoost = loyaltyBoost, !["0", "1", "2"].contains(loyaltyBoost) {
    throw OffersError.invalidParameter(message: "Invalid loyaltyBoost: \(loyaltyBoost)")
}

if let creative = creative, !["0", "1"].contains(creative) {
    throw OffersError.invalidParameter(message: "Invalid creative: \(creative)")
}

var finalPayload = payload ?? [:]
if isDevelopment {
    finalPayload["dev"] = "1"
}
```

payload

```swift
// Initialize payload
let payload: [String: String] = [
    "adpx_fp": "<unique_value>",
    "pub_user_id": "<unique_value>",
    "placement": "checkout",
    "ua": "<user_agent_value>"
]

```
:::

2. **Construct the Request URL:&#x20;**&#x55;se URLComponents to construct the URL and append query parameters like api\_key, loyaltyboost, creative, and campaignId.

:::CodeblockTabs{indent="2"}
```swift
var urlComponents = URLComponents(string: "https://api.adspostx.com/native/v4/offers.json")
var queryItems = [URLQueryItem(name: "api_key", value: apiKey)]

if let loyaltyBoost = loyaltyBoost {
    queryItems.append(URLQueryItem(name: "loyaltyboost", value: loyaltyBoost))
}
if let creative = creative {
    queryItems.append(URLQueryItem(name: "creative", value: creative))
}
if let campaignId = campaignId {
    queryItems.append(URLQueryItem(name: "campaignId", value: campaignId))
}

urlComponents?.queryItems = queryItems

guard let url = urlComponents?.url else {
    throw OffersError.invalidURL
}

```
:::

3. **Configure and Send the&#x20;**`POST`**Request:&#x20;**&#x50;repare the request with headers and payload body, then send the request using URLSession.

:::CodeblockTabs{indent="2"}
```swift
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")

// Set User-Agent from payload or fallback
let userAgent = finalPayload["ua"] ?? getUserAgent()
request.setValue(userAgent, forHTTPHeaderField: "User-Agent")

// Encode payload
request.httpBody = try JSONSerialization.data(withJSONObject: finalPayload)
```
:::

4. **Parse the Response and Handle Errors:** Decode the API response, validate the result, and handle any decoding or network errors.

:::CodeblockTabs{indent="2"}
```swift
do {
    let (data, _) = try await URLSession.shared.data(for: request)
    let response = try JSONDecoder().decode(OffersResponse.self, from: data)

    // Ensure response contains offers
    guard let offers = response.data?.offers, !offers.isEmpty else {
        throw OffersError.noOffers
    }

    return response

} catch let error as DecodingError {
    print("Decoding error: \(error)")
    throw OffersError.decodingError

} catch {
    throw OffersError.networkError(error)
}

```
:::

:::CodeblockTabs
Full Implementation: Fetching Offers in iOS

```swift
func fetchOffers(
    apiKey: String,
    loyaltyBoost: String? = nil,
    creative: String? = nil,
    isDevelopment: Bool = false,
    payload: [String: String]? = nil,
    campaignId: String? = nil
) async throws -> OffersResponse {
    
    // Validate loyaltyBoost parameter if provided
    if let loyaltyBoost = loyaltyBoost {
        guard ["0", "1", "2"].contains(loyaltyBoost) else {
            throw OffersError.invalidParameter(message: "Invalid loyaltyBoost parameter: \(loyaltyBoost)")
        }
    }
    
    // Validate creative parameter if provided
    if let creative = creative {
        guard ["0", "1"].contains(creative) else {
            throw OffersError.invalidParameter(message: "Invalid creative parameter: \(creative)")
        }
    }
    
    // Construct the payload, ensuring 'dev' = '1' is included if isDevelopment is true
    var finalPayload = payload ?? [:]
    if isDevelopment {
        finalPayload["dev"] = "1"
    }
    // baseURL: https://api.adspostx.com/native/v4
    // Construct URL with query parameters
    var urlComponents = URLComponents(string: "\(baseURL)/offers.json")
    var queryItems = [
        URLQueryItem(name: "api_key", value: apiKey)
    ]
    
    // Add optional parameters to query items if they exist
    if let loyaltyBoost = loyaltyBoost {
        queryItems.append(URLQueryItem(name: "loyaltyboost", value: loyaltyBoost))
    }
    
    if let creative = creative {
        queryItems.append(URLQueryItem(name: "creative", value: creative))
    }
    
    if let campaignId = campaignId {
        queryItems.append(URLQueryItem(name: "campaignId", value: campaignId))
    }
    
    urlComponents?.queryItems = queryItems
    
    guard let url = urlComponents?.url else {
        throw OffersError.invalidURL
    }
    
    // Prepare request
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("application/json", forHTTPHeaderField: "Accept")
    
    // Set User-Agent header from payload["ua"] or getUserAgent()
    let userAgent = finalPayload["ua"] ?? getUserAgent()
    request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
    
    // Serialize the final payload
    request.httpBody = try? JSONSerialization.data(withJSONObject: finalPayload)
    
    do {
        let (data, _) = try await URLSession.shared.data(for: request)
        let response = try JSONDecoder().decode(OffersResponse.self, from: data)
        
        // Check if we have valid offers data
        guard let offers = response.data?.offers, !offers.isEmpty else {
            throw OffersError.noOffers
        }
        
        return response
    } catch let error as DecodingError {
        print("Decoding error: \(error)")
        throw OffersError.decodingError
    } catch {
        throw OffersError.networkError(error)
    }
}

```

Payload Example

```swift
let payload: [String: String] = [
    "adpx_fp": "<unique_value>",
    "pub_user_id": "<unique_value>",
    "placement": "checkout",
    "ua": "<user_agent_value>"
]

```
:::

**Parameters**

The following table describes the parameters you can use when calling the `fetchOffers` function:

| **Parameter**   | **Type**           | **Description**                                                        | **Required** | **Default** |
| --------------- | ------------------ | ---------------------------------------------------------------------- | ------------ | ----------- |
| `apiKey`        | String             | The API key associated with your MomentScience account.                | Yes          | –           |
| `loyaltyBoost`  | String             | Sets the loyalty boost level for the offers. Accepts "0", "1", or "2". | No           | nil         |
| `creative`      | String             | Determines the creative mode for the offers. Accepts "0" or "1".       | No           | nil         |
| `isDevelopment` | Bool               | Enables development mode. Set to `true` for testing environments.      | No           | False       |
| `payload`       | \[String: String]? | Additional key-value pairs to include in the request body.             | No           | nil         |
| `campaignId`    | String?            | campaignId                                                             | No           | nil         |

**Common payload fields**

| **Field**     | **Type** | **Description**                                                                                                                                    |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adpx_fp`     | String   | Device fingerprint or session ID                                                                                                                   |
| `pub_user_id` | String   | A unique, non-PII identifier for the end user. This value links offers to individual users and must remain consistent across sessions and devices. |
| `placement`   | String   | An attribute that represents the specific page, section, or location where the Offer Unit was triggered                                            |
| `dev`         | String   | Use "1" to return test offers.                                                                                                                     |
| `ua`          | String   | User-Agent String                                                                                                                                  |

**Response**

A successful response returns an object containing the available offers and related metadata. For detailed response structure and field descriptions, refer to the [MomentPerks API documentation](https://docs.momentscience.com/momentperks-api).

:::hint{type="info"}
See [OfferService.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Services/OffersService.swift) and [Offer.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Models/Offer.swift) in the demo app for a working implementation.
:::

***

## Step 2: Build the Offer UI

Once offers are fetched, you need to build a UI to present them to users and allow interaction, such as accepting or dismissing. The structure of this UI depends on your app architecture.

:::hint{type="success"}
For a detailed explanation of how each field in the offer object is used refer to the [Offer Anatomy documentation](https://docs.momentscience.com/offer-anatomy). This guide will help you understand how to map API fields to UI components and apply dynamic styling correctly.&#x20;
:::

The examples below show how to implement this using SwiftUI. They are intended to illustrate one possible approach and can be adapted freely to match your platform, design system, and navigation logic.

:::::VerticalSplit{layout="middle"}
::::VerticalSplitItem
:::hint{type="warning"}
The [`OfferContainerView`](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Views/OfferContainerView.swift), [`OffersViewModel`](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/ViewModels/OffersViewModel.swift) and [`OfferView`](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Views/OfferView.swift) shown below are example implementations. You are free to structure your UI and state management according to your app's architecture. These examples are provided to help you get started quickly.
:::


::::

:::VerticalSplitItem
![](https://api.archbee.com/api/optimize/ELjiwjWcrv0a1IejQFFAF/Z-4NfVPyCMOHiQD_GmKUB_image.png "Example App UI")
:::
:::::

### Offer Container UI

The Offer Container UI displays multiple offers in sequence and manages user navigation, loading, and error states. Below is a sample implementation using SwiftUI:

```swift
import SwiftUI

/// A view that presents offers in a full-screen modal interface
struct OfferContainerView: View {
    /// View model that manages the offers and their state
    @ObservedObject var viewModel: OffersViewModel
    /// Environment value for dismissing the view
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        ZStack {
            // Semi-transparent background from API styling
            viewModel.getPopupBackgroundColor()
                .ignoresSafeArea()
            
            if viewModel.isLoading {
                // Loading state
                ProgressView("Loading offers...")
                    .foregroundColor(.white)
            } else if let error = viewModel.error {
                // Error state with retry option
                VStack(spacing: 16) {
                    Text(error)
                        .foregroundColor(.red)
                        .padding()
                    
                    HStack(spacing: 20) {
                        Button("Close") {
                            dismiss()
                        }
                        .foregroundColor(.gray)
                        
                        Button("Try Again") {
                            viewModel.loadOffers()
                        }
                        .foregroundColor(.blue)
                    }
                }
                .background(Color.white)
                .clipShape(RoundedRectangle(cornerRadius: 12))
                .padding()
            } else if let offer = viewModel.currentOffer {
                // Main offer display container
                VStack(spacing: 0) {
                    // Close button with beacon tracking
                    HStack {
                        Spacer()
                        Button {
                            Task {
                                await viewModel.fireCloseBeacon()
                                dismiss()
                            }
                        } label: {
                            Image(systemName: "xmark")
                                .imageScale(.large)
                                .foregroundColor(.gray)
                                .padding()
                        }
                    }
                    
                    // Main offer content
                    OfferView(
                        offer: offer,
                        buttonStyles: viewModel.styles?.offerText,
                        onPositiveCTA: {
                            // Handle positive response
                            if let clickUrl = offer.clickUrl, 
                               let url = URL(string: clickUrl),
                               UIApplication.shared.canOpenURL(url) {
                                UIApplication.shared.open(url)
                            }
                            
                            // Navigate or dismiss
                            if viewModel.hasNextOffer {
                                viewModel.showNextOffer()
                            } else {
                                Task {
                                    await viewModel.fireCloseBeacon()
                                    dismiss()
                                }
                            }
                        },
                        onNegativeCTA: {
                            // Handle negative response
                            if let beacons = offer.beacons,
                               let noThanksClickUrl = beacons.noThanksClick,
                               !noThanksClickUrl.isEmpty,
                               let url = URL(string: noThanksClickUrl) {
                                viewModel.fireBeaconRequest(url: url)
                            }
                            
                            // Navigate or dismiss
                            if viewModel.hasNextOffer {
                                viewModel.showNextOffer()
                            } else {
                                // On last offer, fire close beacon and dismiss
                                Task {
                                    // Fire close beacon and wait for it to complete
                                    await viewModel.fireCloseBeacon()
                                    // If no more offers, close the container
                                    dismiss()
                                }
                            }
                        }, viewModel: viewModel
                    )
                    
                    // Navigation controls
                    NavigationButtonsView(
                        hasPrevious: viewModel.hasPreviousOffer,
                        hasNext: viewModel.hasNextOffer,
                        onPrevious: viewModel.showPreviousOffer,
                        onNext: viewModel.showNextOffer
                    )
                }
                .background(Color.white)
                .clipShape(RoundedRectangle(cornerRadius: 12))
                .padding(.horizontal)
                .padding(.vertical, 40)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
            }
        }
        .statusBar(hidden: true)
    }
}

```

:::hint{type="info"}
For more details, see [OfferContainerView.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Views/OfferContainerView.swift) and [OffersViewModel.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/ViewModels/OffersViewModel.swift).
:::

### Individual Offer UI

Each offer is displayed using the `OfferView` component, which presents offer details such as title, image, description, and call-to-action buttons with dynamic styling. The business logic and state for the offer presentation are managed by the [`OffersViewModel`](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/ViewModels/OffersViewModel.swift) class, following the MVVM pattern.

:::CodeblockTabs
OfferView usage

```swift
// Display the current offer
OfferView(
    offer: currentOffer,
    buttonStyles: styles?.offerText,
    onPositiveCTA: { /* handle accept */ },
    onNegativeCTA: { /* handle decline */ },
    viewModel: viewModel
)
```
:::

:::hint{type="info"}
For advanced usage and dynamic styling, refer to [OfferView.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Views/OfferView.swift), [OfferViewModel.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/ViewModels/OffersViewModel.swift).
:::

:::hint{type="success"}
If you prefer to use your own layout or styling you can:

- Parse the offer response manually.
- Use the [Offer Anatomy](https://docs.momentscience.com/offer-anatomy) documentation to map fields like title, `image`, `cta_yes` etc.
- Apply any visual styling or logic defined in your own architecture.

This approach gives you full control over the user experience, while still integrating with the core Moments API logic.
:::

***

## Step 3: Track User Interactions

To monitor engagement and ensure accurate analytics, send tracking requests when users interact with offers. This includes impressions, dismissals, and CTA clicks.

1. **Create a Function to Fire Tracking Beacons:&#x20;**&#x44;efine a utility function to send tracking pixels via HTTP GET requests.

:::CodeblockTabs{indent="2"}
fireBeaconRequest

```swift
/// Sends a beacon request to the specified URL for tracking user interactions
func fireBeaconRequest(url: URL) {
    Task {
        do {
            try await offersService.fireBeaconRequest(url: url)
        } catch {
            print("Beacon request failed: \(error.localizedDescription)")
        }
    }
}
```

offerservice - fireBeaconRequest

```swift
   func fireBeaconRequest(url: URL) async throws {
        print("👉 Firing beacon request to \(url.absoluteString)")
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.setValue(getUserAgent(), forHTTPHeaderField: "User-Agent")
        
        do {
            let (_, response) = try await URLSession.shared.data(for: request)
            
            guard let httpResponse = response as? HTTPURLResponse else {
                print("❌ Beacon request failed: Invalid response type")
                throw OffersError.networkError(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid response type"]))
            }
            
            guard (200...299).contains(httpResponse.statusCode) else {
                print("❌ Beacon request failed with status code: \(httpResponse.statusCode)")
                throw OffersError.networkError(NSError(domain: "", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "Beacon request failed with status code: \(httpResponse.statusCode)"]))
            }
            
            print("✅ Beacon request succeeded with status code: \(httpResponse.statusCode)")
        } catch {
            print("❌ Beacon request failed with error: \(error.localizedDescription)")
            throw OffersError.networkError(error)
        }
    }
```
:::

2. **Track When the Offer Container is Closed:&#x20;**&#x53;end the`beacons.close` beacon when a user dismisses the offer container.

:::CodeblockTabs{indent="2"}
```swift
func fireCloseBeacon() async {
    guard let offer = currentOffer,
          let beacons = offer.beacons,
          let closeUrl = beacons.close,
          !closeUrl.isEmpty,
          let url = URL(string: closeUrl) else {
        return
    }

    do {
        try await offersService.fireBeaconRequest(url: url)
    } catch {
        print("Close beacon request failed: \(error.localizedDescription)")
    }
}
```
:::

3. **Track When an Offer is Displayed:&#x20;**&#x53;end impression pixels when the offer is shown (`pixel`and `adv_pixel_url`).

:::CodeblockTabs{indent="2"}
Firing pixel

```swift
private func firePixelRequestForCurrentOffer() {
    guard let offer = currentOffer,
          let pixelUrl = offer.pixel,
          !pixelUrl.isEmpty,
          let url = URL(string: pixelUrl) else {
        return
    }

    Task {
        do {
            try await offersService.fireBeaconRequest(url: url)
        } catch {
            print("Pixel request failed: \(error.localizedDescription)")
        }
    }
}

```

Firing adv\_pixel\_url

```swift
private func fireAdvPixelRequestForCurrentOffer() {
    guard let offer = currentOffer,
          let advPixelUrl = offer.advPixelUrl,
          !advPixelUrl.isEmpty,
          let url = URL(string: advPixelUrl) else {
        return
    }

    Task {
        do {
            try await offersService.fireBeaconRequest(url: url)
        } catch {
            print("Pixel request failed: \(error.localizedDescription)")
        }
    }
}

```
:::

4. **Track When the Negative CTA is Clicked:&#x20;**&#x46;ire the `beacons.no_thanks_click` beacon when a user taps on negative CTA.

:::CodeblockTabs{indent="2"}
```swift
if let beacons = offer.beacons,
   let noThanksClickUrl = beacons.noThanksClick,
   !noThanksClickUrl.isEmpty,
   let url = URL(string: noThanksClickUrl) {
    viewModel.fireBeaconRequest(url: url)
}

```
:::

:::hint{type="info"}
See [OffersService.swift](https://github.com/AdsPostX/examples/blob/main/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/Services/OffersService.swift) and [OffersViewModel.swift ](https://github.com/AdsPostX/examples/blob/ce406744ad263a386ad5228e9bd4e7c66d35540c/ios-native/MomentsAPIDemoApp-iOS/MSAPIDemoApp/MSAPIDemoApp/ViewModels/OffersViewModel.swift)in the demo app for a working implementation.
:::

# Next Steps

We recommend that you go through the [MomentPerks API Implementation Checklist](https://docs.momentscience.com/momentperks-integration-checklist) to verify your integration. Completing this checklist ensures that all best practices and requirements are met for a successful Moments API Integration.

***

📢 If you're running into any issues while going through the integration process, feel free to contact us at [help@momentscience.com](#)&#x20;
