iOS(Swift) - MomentPerks API Integration Guide
Overview
The 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
To explore a working example, see the MomentScience iOS Example App on GitHub.ο»Ώ
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 to obtain your key.
- Minimum iOS Version: Your project must target iOS15.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.
For complete details on MomentPerks API, refer to the MomentPerks API documentation.
Step 1: Fetch Offers
In this step, build a function that sends a POST request to the MomentPerks API (native/v4/offers.json) and returns personalized offers based on the given user context.
- Build and Validate Input Parameters: Start by validating optional parameters like loyaltyBoost and creative, then prepare the payload body.
// 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"
}- Construct the Request URL: Use URLComponents to construct the URL and append query parameters like api_key, loyaltyboost, creative, and campaignId.
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
}
- Configure and Send the POSTRequest: Prepare the request with headers and payload body, then send the request using URLSession.
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)- Parse the Response and Handle Errors: Decode the API response, validate the result, and handle any decoding or network errors.
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)
}
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)
}
}
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.
See OfferService.swift and 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.
For a detailed explanation of how each field in the offer object is used refer to the Offer Anatomy documentation. This guide will help you understand how to map API fields to UI components and apply dynamic styling correctly.
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.
The OfferContainerView, OffersViewModel and OfferView 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.
ο»Ώ

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:
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)
}
}
For more details, see OfferContainerView.swift and 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 class, following the MVVM pattern.
// Display the current offer
OfferView(
offer: currentOffer,
buttonStyles: styles?.offerText,
onPositiveCTA: { /* handle accept */ },
onNegativeCTA: { /* handle decline */ },
viewModel: viewModel
)For advanced usage and dynamic styling, refer to OfferView.swift, OfferViewModel.swift.
If you prefer to use your own layout or styling you can:
- Parse the offer response manually.
- Use the 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.
- Create a Function to Fire Tracking Beacons: Define a utility function to send tracking pixels via HTTP GET requests.
/// 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)")
}
}
}- Track When the Offer Container is Closed: Send thebeacons.close beacon when a user dismisses the offer container.
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)")
}
}- Track When an Offer is Displayed: Send impression pixels when the offer is shown (pixeland adv_pixel_url).
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)")
}
}
}
- Track When the Negative CTA is Clicked: Fire the beacons.no_thanks_click beacon when a user taps on negative CTA.
if let beacons = offer.beacons,
let noThanksClickUrl = beacons.noThanksClick,
!noThanksClickUrl.isEmpty,
let url = URL(string: noThanksClickUrl) {
viewModel.fireBeaconRequest(url: url)
}
See OffersService.swift and OffersViewModel.swift in the demo app for a working implementation.
Next Steps
We recommend that you go through the MomentPerks API Implementation 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 [email protected]