Skip to main content

Troubleshooting SDK Integration

The dashboard's Settings → SDK Integration page provides a self-service health check for your Monetai SDK integration. This page describes how to interpret each card's status and how to diagnose common integration issues.


Reading the validation page

The page shows three status cards:

CardWhat it checks
SDK ConnectionWhether initialize() has ever been called from your app
Event ReceptionWhether both getOffer() (offer requested) and logViewProductItem() (paywall impression recorded) calls arrived in the last 24 hours
SDK OperationWhether the products recommended via getOffer() are actually being shown on the paywall (last 24 hours)

Card-by-card diagnosis

🔴 SDK Connection: Not connected

The SDK has never reported an initialize() call from your app.

Check:

  • The Monetai SDK library is included in your app build
  • initialize() runs at app launch
  • sdkKey matches the one shown in the dashboard

🟡 Event Reception: Partial / 🔴 Not received

Only one of getOffer() and logViewProductItem() calls is arriving, or neither is.

Missing eventLikely cause
getOffer() arrives, logViewProductItem() missingPaywall code is missing the impression call
logViewProductItem() arrives, getOffer() missingThe SDK isn't being asked for offers
Both missingThe SDK isn't integrated yet, or hasn't been used in the last 24 hours

🟡 SDK Operation: Issues N

Some users received an offer for a SKU but did not view that SKU on the paywall. This is the most common — and most informative — signal. Use the 'Five integration principles' below to check your integration patterns, then drill into per-screen diagnosis with 'Diagnosing "Issues N" by row pattern'.


Five integration principles

Most integration bugs trace back to violating one of these.

1. Use the SKU returned by getOffer() directly in your paywall UI

The most common bug: an app calls getOffer(), receives a SKU recommendation, but the UI displays a different SKU.

Anti-pattern:

const offer = await MonetaiSDK.getOffer('main_paywall');
saveToStore(offer); // saved but never used
showPaywall(BASE_DISCOUNT_SKU); // UI always uses a fixed SKU (e.g. your existing 60% discount SKU)

Correct:

// BASE_DISCOUNT_SKU = the SKU at the discount rate you've been operating with
// before introducing Monetai (e.g. your existing 60%-off SKU).
const offer = await MonetaiSDK.getOffer('main_paywall');
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
showPaywall(sku);

2. Call getOffer() close to the moment the paywall is shown

Anti-pattern: call getOffer() once at app launch, cache the result, then read from cache hours later when the paywall opens.

This breaks validation in three ways:

  • Match rate drops to 0%, since validation matches each user's view event against the same user's getOffer() call within a 24-hour window, in that order
  • The cached recommendation may be out of date — the AI model continually refines its decisions based on user behavior, so a recommendation cached hours ago may no longer be the right one
  • getOffer() calls accumulate even for users who never reach the paywall, leaving the SDK with calls that have no matching impression

Correct: when your promotion trigger condition fires, call getOffer(), use the result to render the paywall, then call logViewProductItem() once the SKU appears on screen — all within the same code path.

// When the promotion trigger fires (e.g. user enters the time-sale flow)
async function onTimeSaleTrigger() {
const offer = await MonetaiSDK.getOffer('time_sale_paywall'); // call right after trigger
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
showPaywall(sku); // render UI immediately
logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... }); // log impression right after
}

3. Each paywall screen needs its own placement

A placement is an identifier for a specific paywall screen. Two different paywall screens (e.g., onboarding paywall vs exit-intent paywall) must have different placement values, even if they show the same SKUs.

If you reuse the same placement across multiple screens, the validation page can no longer distinguish between them, and per-screen analytics become unreliable.

4. Always call logViewProductItem() when a SKU is rendered

Anti-pattern 1: only call logViewProductItem() when getOffer() returns a recommendation.

const offer = await MonetaiSDK.getOffer('time_sale_paywall');
if (offer != null) {
showPaywall(offer.products[0].sku);
logViewProductItem({ placement: 'time_sale_paywall', productId: offer.products[0].sku, ... });
// ↑ users who got null never enter the if branch, so the call is skipped
}

Anti-pattern 2: skip the paywall entirely when getOffer() returns null — users who get null never reach the paywall, so no impression event fires either.

Both patterns silently drop impressions for users who got null from getOffer(), making A/B comparison impossible. To accurately compare the AI-optimized group against your existing pricing, both sides must be tracked.

Correct: log impressions every time the paywall renders a SKU, regardless of whether getOffer() returned an offer object or null.

const offer = await MonetaiSDK.getOffer('time_sale_paywall');
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
showPaywall(sku);
logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... }); // call for users who got null too

5. Wait for SDK initialization to complete before calling getOffer()

If getOffer() is called before initialize() has finished, it returns null. Make sure your app awaits the initialization handle (Promise / callback / completion delegate) before calling getOffer().


Diagnosing "Issues N" by row pattern

When the SDK Operation card shows issues, expand the placement accordions and look at which rows are 0%.

Pattern A: All rows 0% in a placement, with getOffer count > 0

The SDK is being asked for offers but no resulting view events match. Most likely:

  1. Wrong placement attached: getOffer() is called from a screen that isn't the one actually rendering the discounted SKUs (e.g., the call lives on the regular subscription page, while the discounted SKUs only appear on a separate time-sale screen).

    Anti-pattern: getOffer('time_sale_paywall') is called, but the user actually sees the regular subscription page (which has its own subscription_paywall placement). No view event for time_sale_paywall is ever fired for that user, so the match fails.

    // Regular subscription page — this screen's placement is 'subscription_paywall'
    function showRegularSubscriptionPage() {
    await MonetaiSDK.getOffer('time_sale_paywall'); // the placement we want to validate
    showPaywall(REGULAR_PRICE_SKU);
    logViewProductItem({ placement: 'subscription_paywall', productId: REGULAR_PRICE_SKU, ... });
    // ↑ this screen logs its own placement → no view event will match the 'time_sale_paywall' recommendation
    }

    Correct: only call getOffer() from the screen where the discounted SKUs are actually shown.

    // Time-sale bottom sheet — the screen that actually renders the discounted SKU
    async function onTimeSaleTrigger() {
    const offer = await MonetaiSDK.getOffer('time_sale_paywall');
    const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
    showPaywall(sku);
    logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... });
    }
  2. logViewProductItem() missing entirely: the SKU appears on screen but no view event fires. Search for logViewProductItem() in the relevant paywall code path to verify it's actually being called (see Principle 4 code example).

Pattern B: Some rows 0%, others normal

The SDK return value is being partially ignored. Most likely:

  1. Hardcoded SKU on UI: getOffer() recommended SKU X, but the paywall shows a fixed SKU Y. View events fire for Y only; X shows 0%. (See Principle 1.)

    Anti-pattern: getOffer()'s result is received but ignored; the paywall always shows a fixed SKU.

    const offer = await MonetaiSDK.getOffer('time_sale_paywall');
    // offer is received but ignored — always show the fixed SKU
    showPaywall(BASE_DISCOUNT_SKU);
    logViewProductItem({ placement: 'time_sale_paywall', productId: BASE_DISCOUNT_SKU, ... });
    // → other recommended SKUs (40%, 50%) get no view events, showing 0% (only 60% shows 100%)

    Correct: render the SKU returned by getOffer() and log the view event with the same SKU.

    const offer = await MonetaiSDK.getOffer('time_sale_paywall');
    const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;
    showPaywall(sku);
    logViewProductItem({ placement: 'time_sale_paywall', productId: sku, ... });
    // → the recommended SKU matches the view event
  2. Conditional view logging: view logging is gated on getOffer()'s result (object vs null). (See Principle 4.)

  3. SKU naming mismatch: the SKU registered in the dashboard differs from the actual SKU sold by the store (e.g., .60 suffix missing). Cross-check the dashboard SKU against your App Store / Play Console product IDs.


The recommended call sequence: promotion trigger fires → getOffer() → render UI → logViewProductItem(), all in the same flow within seconds.

import MonetaiSDK from '@hayanmind/monetai-react-native';
import { useEffect } from 'react';

// 1. Initialize the SDK once at app start (e.g. in App.tsx useEffect).
useEffect(() => {
MonetaiSDK.initialize({
sdkKey: 'YOUR_SDK_KEY',
userId: 'USER_ID',
});
}, []);

// 2. Called when the promotion trigger fires (e.g. user enters the time-sale flow, time-based trigger).
async function openTimeSalePaywall() {
// Right after the trigger, request the offer.
const offer = await MonetaiSDK.getOffer('time_sale_paywall');

// Pick the SKU. If null, fall back to your existing baseline-discount SKU.
const sku = offer?.products[0]?.sku ?? BASE_DISCOUNT_SKU;

// Resolve store details and render the paywall.
const storeProduct = await fetchStoreProduct(sku);
renderPaywall(storeProduct);

// Log the view event right after the SKU appears on screen.
await MonetaiSDK.logViewProductItem({
placement: 'time_sale_paywall',
productId: sku,
price: storeProduct.discountedPrice,
regularPrice: storeProduct.regularPrice,
currencyCode: storeProduct.currencyCode,
});
}

📌 Note on null returns: when getOffer() returns null, your app should still render the paywall using a default SKU and still call logViewProductItem(). The default SKU can be registered on the Monetai dashboard so that the backend automatically returns it for users assigned to the baseline / unknown groups, removing the need for client-side null handling.


A note on the 24-hour window

The validation page's match rate uses a 24-hour rolling window. Two implications:

  • Low-traffic SKUs may not yet have enough samples to show a meaningful rate within 24 hours. If you registered a SKU very recently, check again the next day.
  • Recently launched campaigns (started within the last 24 hours) may show partial data. Wait at least 24 hours after the campaign starts before drawing conclusions.

Still stuck?

If the validation page still shows issues after applying the principles above, reach out via Slack support channel or email and include:

  • A screenshot of the SDK Operation card and the expanded placement(s) showing the issue rows
  • The relevant code path (function name / file) where getOffer() and logViewProductItem() are called
  • App version and SDK version