# Onside Documentation

Learn in detail about the console, tools, and technologies in Onside

<table data-card-size="large" data-view="cards"><thead><tr><th align="center"></th><th align="center"></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center"><strong>Onside Store</strong></td><td align="center">Learn how to install the marketplace, discover exclusive apps, manage your account, and find answers to common questions.</td><td><a href="/files/AQ6BSAwIPo4MZhdA4Ohi">/files/AQ6BSAwIPo4MZhdA4Ohi</a></td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg">/spaces/K6W2Xq8YQWUKNGf4QCyg</a></td></tr><tr><td align="center"><strong>Onside Dev Console</strong></td><td align="center">Find everything you need to publish your app, set up monetization, analyze performance, and manage your developer account.</td><td><a href="/files/ibkfy4QYwYdzWyAy4z26">/files/ibkfy4QYwYdzWyAy4z26</a></td><td><a href="/spaces/V24VBLkTP47H3Kf48DFs">/spaces/V24VBLkTP47H3Kf48DFs</a></td></tr><tr><td align="center"><strong>Onside SDK</strong></td><td align="center">Connect developer tools to use all required services</td><td><a href="/files/ZdvFVTOy9spLVFw2I697">/files/ZdvFVTOy9spLVFw2I697</a></td><td><a href="/spaces/ar2Jh3wLmverIdTKRpUi">/spaces/ar2Jh3wLmverIdTKRpUi</a></td></tr><tr><td align="center"><strong>Onside API</strong></td><td align="center">Automate update and managing processes with our API</td><td><a href="/files/mgLyq9RfjKgLAaUdLXaX">/files/mgLyq9RfjKgLAaUdLXaX</a></td><td><a href="/spaces/vu6GnA9QIMP7iX7RdyRJ">/spaces/vu6GnA9QIMP7iX7RdyRJ</a></td></tr></tbody></table>


# Welcome to Onside Store: Alternative iOS App Store for the EU and Japan

A quick introduction to the Onside Store, its key benefits, what you can find inside, and our commitment to your safety.

The **Onside Store** is an alternative iOS app marketplace for users in the **European Union (EU)** and **Japan**.

You can **discover, download, install, and update iOS apps** that may not be available on the Apple App Store.

### Quick answers (most common questions)

* **Where is the Onside Store available?** EU (excluding the UK) and Japan. You must be **physically located** there. A **VPN won’t bypass** this.
* **Which devices are supported?**
  * **EU:** iPhone (iOS **17.6+**) and iPad (iPadOS **18+**)
  * **Japan:** iPhone only (iOS **26.2+**). **Not available on iPad**
* **How do I install it?** Follow the [Installation Guide →](/store/getting-started/how-to-install-onside-store). If it fails, use [Troubleshooting Installation Issues →](/store/getting-started/how-to-install-onside-store/troubleshooting-installation-issues).
* **Do I need a specific Apple Account Country/Region?** Yes. Your Apple Account **Country/Region** must be set to an **EU country/region** or **Japan**.
* **Where do I download Onside Store?** Only from our official website: <https://onside.io/>
* **Can I trust Onside?** Yes. We review apps and use secure payments. See [Can I Trust Onside?](/store/authorization-and-safety/can-i-trust-onside).
* **I’m a developer. Where do I start?**
  * Developer landing: <https://developer.onside.io/>
  * Onside Developer Console: <https://console.onside.io/>
* **Why does this exist?** EU distribution is enabled by the **Digital Markets Act (DMA)**. Japan distribution is enabled by the **Mobile Software Competition Act (MSCA)**.

{% hint style="warning" %}
Installation requires an Apple Account with **Country/Region** set to an **EU country/region** or **Japan**.
{% endhint %}

{% hint style="warning" %}
On **iPad**, you must use an Apple Account set to an **EU country/region**. Onside Store is not available on iPad in Japan.
{% endhint %}

### Jump Right In

Common next steps when you’re getting started:

<table data-view="cards"><thead><tr><th></th><th></th><th></th></tr></thead><tbody><tr><td><strong>Install Onside Store</strong></td><td>Install the Onside Store marketplace app and complete setup (EU: iPhone iOS 17.6+ or iPad iPadOS 18+; Japan: iPhone iOS 26.2+ only).</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/9IfkSWHVH1kj1raEDKmE">Installation Guide →</a></td></tr><tr><td><strong>Sign up or log in</strong></td><td>Create an account to manage purchases.</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/QWgV74KLoCrxBkEgqgZF">Sign Up Guide →</a></td></tr><tr><td><strong>Browse, search, and discover apps</strong></td><td>Learn how the Onside Store is organized so you can find apps faster.</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/PueWuNi1XyHMFbdtX1R5">Explore the Onside Store →</a></td></tr><tr><td><strong>Payments in Onside Store</strong></td><td>See supported payment methods and currencies (card, Apple Pay, and more).</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/1vuwAgaiCM4ZOJxWrpSM">Payment Methods →</a></td></tr><tr><td><strong>Safety, trust, and privacy</strong></td><td>Understand how we review apps and what you can do to stay safe.</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/luqP6GmU7oqSvWPEWoOc">Safety &#x26; Security →</a></td></tr><tr><td><strong>For developers</strong></td><td>Start publishing on Onside: visit the developer site and use the Onside Developer Console.</td><td><a href="https://developer.onside.io/">Developer site →</a></td></tr></tbody></table>

<table data-view="cards"><thead><tr><th></th><th></th><th></th></tr></thead><tbody><tr><td><strong>Install Onside Store</strong></td><td>Install the Onside Store marketplace app and complete setup (EU: iPhone iOS 17.6+ or iPad iPadOS 18+; Japan: iPhone iOS 26.2+ only).</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/9IfkSWHVH1kj1raEDKmE">Installation Guide →</a></td></tr><tr><td><strong>Sign up or log in</strong></td><td>Create an account to manage purchases and other account-based features.</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/QWgV74KLoCrxBkEgqgZF">Sign Up Guide →</a></td></tr><tr><td><strong>Browse, search, and discover apps</strong></td><td>Learn how the Onside Store is organized so you can find apps faster.</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/PueWuNi1XyHMFbdtX1R5">Explore the Onside Store →</a></td></tr><tr><td><strong>Payments in Onside Store</strong></td><td>See supported payment methods and currencies (card, Apple Pay, and more).</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/1vuwAgaiCM4ZOJxWrpSM">Payment Methods →</a></td></tr><tr><td><strong>Safety, trust, and privacy</strong></td><td>Understand how we review apps and what you can do to stay safe.</td><td><a href="/spaces/K6W2Xq8YQWUKNGf4QCyg/pages/luqP6GmU7oqSvWPEWoOc">Safety &#x26; Security →</a></td></tr><tr><td><strong>For developers</strong></td><td>Start publishing on Onside: visit the developer site and use the Onside Developer Console.</td><td><a href="https://developer.onside.io/">Developer site →</a></td></tr></tbody></table>

***

### Availability & device requirements (details)

<details>

<summary><strong>Where is the Onside Store available?</strong></summary>

The Onside Store is currently available for users **physically located** in:

* An **EU** country (excluding the UK)
* **Japan**

On **iPad**, the Onside Store app is available **in the EU only**. It is **not available on iPad in Japan**.

A VPN will not bypass this requirement.

</details>

<details>

<summary><strong>Which devices and iOS versions are supported?</strong></summary>

* **iPhone:**
  * **EU:** iOS **17.6 or later**
  * **Japan:** iOS **26.2 or later**
* **iPad (EU only):** iPadOS **18 or later**

In **Japan**, Onside Store is **not supported on iPad**.

</details>

{% hint style="info" %}
If you meet the requirements, start with the [Installation Guide →](/store/getting-started/how-to-install-onside-store).
{% endhint %}

***

### Why Onside Store is available (DMA and MSCA)

In the **EU**, alternative iOS app marketplaces are enabled by the **Digital Markets Act (DMA)**.

In **Japan**, alternative iOS app marketplaces are enabled by the **Mobile Software Competition Act (MSCA)**.

Apple applies additional platform safeguards, including **Notarization** for iOS apps and an **authorization process** for app marketplaces.

***

### What’s different about the Onside Store?

Onside is for users who want more choice in where they get iOS apps, without compromising on safety.

<details>

<summary><strong>More apps and categories (including region-legal, age-gated content)</strong></summary>

The Onside Store can offer apps and games you might not find elsewhere. This can include:

* Popular titles and indie apps
* New releases
* Broader categories (for example: mature-audience apps, crypto, or gaming), where they are legally compliant and appropriately age-gated

</details>

<details>

<summary><strong>Potential for different prices and offers</strong></summary>

Onside gives developers more flexibility around distribution and monetization. In practice, that can translate into more competitive pricing and better offers on paid apps, in-app purchases, and subscriptions.

</details>

<details>

<summary><strong>A familiar app store experience on iPhone (and iPad in the EU)</strong></summary>

Use the Onside Store like any other iOS app marketplace:

* Browse curated collections and trending apps
* Search for apps by name or category
* Open detailed app pages for screenshots, descriptions, and version information

In many cases, app pages also include **reviews and ratings synced with the Apple App Store**.

</details>

<details>

<summary><strong>Security, privacy, and secure payments</strong></summary>

* **Privacy-first approach:** We minimize data collection and avoid collecting information that isn’t required to operate the Onside Store.
* **App review and checks:** Apps in the Onside Store go through moderation and automated checks before we list them.
* **Secure checkout:** Pay for apps, in-app items, and subscriptions using a **credit/debit card** or **Apple Pay**. We plan to add support for popular alternative payment methods soon.&#x20;
* **Updates:** You can keep the Onside Store app and your installed apps up to date (automatic updates depend on your iOS settings).

</details>


# How to Install Onside Store

The installation won’t take long—you’ll be using the app marketplace within minutes.

### Before you start

{% hint style="warning" %}
Your Apple Account **Country/Region** must be set to an **EU country/region** or **Japan**. Otherwise iOS blocks alternative marketplace installation.
{% endhint %}

{% hint style="warning" %}
On **iPad**, Onside Store is available **in the EU only** (iPadOS **18+**). It is **not available on iPad in Japan**.
{% endhint %}

Make sure you:

* **Use Safari or Google Chrome.** iOS blocks alternative marketplace installation flows in some other browsers.
* **Are physically located in the EU (excluding the UK) or Japan.** On iPad, this must be the **EU**. A VPN will not bypass this requirement.
* **Use an Apple Account with an EU country/region or Japan.**
  * Check: **Settings > \[Your Name] > Media & Purchases > View Account > Country/Region**.

<details>

<summary><strong>How to check or change your App Store country/region</strong></summary>

Go to **Settings > \[Your Name] > Media & Purchases > View Account**. You might need to sign in.

Tap **Country/Region > Change Country or Region**, select your country/region (EU country or Japan), and follow the on-screen instructions.

Changing your region may affect existing subscriptions and payment methods tied to your Apple ID.

</details>

* **Run a supported iOS/iPadOS version.**
  * **iPhone**
    * **EU:** iOS **17.6 or later**
    * **Japan:** iOS **26.2 or later** (MSCA support)
  * **iPad (EU only):** iPadOS **18 or later**

<details>

<summary><strong>How to check and update your iOS/iPadOS version</strong></summary>

Go to **Settings > General > Software Update**. If an update is available, install it.

</details>

{% hint style="info" %}
In Japan, iOS alternative app marketplaces are enabled under the **MSCA**. Apple may show additional authorization prompts during installation. Apps distributed outside the App Store also go through Apple’s baseline **Notarization** checks.
{% endhint %}

If you run into issues, see [Troubleshooting Installation Issues](/store/getting-started/how-to-install-onside-store/troubleshooting-installation-issues).

### Installation steps

<a href="https://onside.io" class="button primary">Install Onside</a>

After you confirm the prerequisites, follow the steps for your iOS/iPadOS version.

{% columns %}
{% column width="50%" %}

### iOS 18.6- installation flow

1. Open [onside.io](https://onside.io/) in Safari and tap **Install Onside**.
2. Tap **OK** to dismiss the iOS notice about installing an alternative app marketplace.

<div align="left" data-full-width="false"><figure><img src="/files/SbljZHJEfNqSGTOqOcfl" alt=""><figcaption></figcaption></figure></div>

3. Open **Settings** and allow marketplace installation for Onside.

<div align="left"><figure><img src="/files/Dc8Bkkc2J2VRc8C6Id6R" alt=""><figcaption></figcaption></figure></div>

4. Return to Safari and tap **Install Onside** again.

<div align="left"><figure><img src="/files/3hkECF1haBHvuIawp5us" alt=""><figcaption></figcaption></figure></div>

5. Tap **Install App Marketplace** to finish.

<div align="left"><figure><img src="/files/XBji2T7cDUaUmcd81ds9" alt=""><figcaption></figcaption></figure></div>

6. Open **Onside Store** and start downloading apps. You can create an account later.

<div align="left"><figure><img src="/files/lLh61zTp7ONG6mFPpcxI" alt=""><figcaption></figcaption></figure></div>

<a href="https://onside.io" class="button primary">Install Onside</a>
{% endcolumn %}

{% column %}

### iOS 18.6+ installation flow

1. Open [onside.io](https://onside.io/) in Safari and tap **Install Onside**.
2. Tap **Allow Installation**.

![](/files/Y016tWhCfOIqIKhEYayi)

3. Confirm installation via **Face ID** or your Apple ID password.

![](/files/fzvcKKaAc4134Ze6yXxD)

4. Tap **Open Onside Store**.

![](/files/eoTO1mKA19Xnclfqlril)

5. Start downloading apps. You can create an account later.

<a href="https://onside.io" class="button primary">Install Onside</a>
{% endcolumn %}
{% endcolumns %}

***

{% hint style="info" %}
If you still need help, contact Onside support via the chat in the bottom-right corner, or email <support@onside.io>.
{% endhint %}


# Troubleshooting Installation Issues

Fix Onside Store install problems on iOS: resolve region and device requirements, profiles/permissions, storage, network, and update errors. Includes quick checks, error codes, and support steps.

Having trouble installing the Onside Store app? This page lists common issues and how to resolve them.

{% hint style="warning" %}
If installation is blocked, first check your Apple Account **Country/Region**. It must be set to an **EU country/region** or **Japan**.
{% endhint %}

{% hint style="warning" %}
On **iPad**, Onside Store is available **in the EU only** (iPadOS **18+**). It is **not available on iPad in Japan**.
{% endhint %}

Check: **Settings > \[Your Name] > Media & Purchases > View Account > Country/Region**.

<details>

<summary><strong>I’m in the EU or Japan but I still can’t install the Onside Store.</strong></summary>

* **Update iOS/iPadOS:**

  * **iPhone**
    * **EU:** iOS **17.6 or later**
    * **Japan:** iOS **26.2 or later**
  * **iPad (EU only):** iPadOS **18 or later**

  Go to **Settings > General > Software Update**.
* **Confirm physical location:** You must be physically located within the **EU** (excluding the UK) or **Japan**. On iPad, this must be the **EU**. A VPN will not bypass this requirement.
* **Check your Apple Account region:** Your Apple Account must be set to an **EU country/region** or **Japan**.
* **Free up storage:** Your device might not have enough free space. The Onside Store app is about 100MB, but iOS/iPadOS needs extra space during installation. We recommend at least **2GB** free.
* **Check your internet connection:** Try switching between Wi-Fi and mobile data.
* **Restart your device:** Restarting can resolve temporary issues.

If the issue persists, contact support and include your device model, iOS/iPadOS version, and the approximate time you attempted the installation.

</details>

<details>

<summary><strong>The Install button on onside.io doesn’t work.</strong></summary>

* **Use Safari or Chrome:** iOS blocks alternative marketplace installation flows in some other browsers.
* **Clear Safari website data:** Clear cache and cookies in Safari.
* **Try Private Browsing:** Open [onside.io](https://onside.io) in a Private tab.
* **Disable content blockers:** If you use ad/content blockers, temporarily disable them for [onside.io](https://onside.io).

</details>

<details>

<summary><strong>I’m seeing an error when downloading or updating apps (after installation).</strong></summary>

* Most download/update errors come from internet connectivity issues, insufficient device storage, or the app’s iOS version requirements.
* If you previously installed the same app from another source, uninstall it, then download it again from the Onside Store.

</details>

<details>

<summary><strong>I see NSURLError when downloading an app.</strong></summary>

Turn off your VPN and try again. VPNs can cause network errors (including NSURLError) during downloads.

</details>

***

### FAQs

<details>

<summary><strong>Why can’t I install Onside on iOS 17.5 or earlier?</strong></summary>

In the EU, Apple’s framework for alternative app marketplaces is available on iOS **17.6** and later.

In Japan, alternative app marketplaces are available starting iOS **26.2** under the **MSCA**.

Onside Store requires the minimum iOS version that supports alternative app marketplaces in your region.

Staying on a recent iOS version also helps ensure you receive Apple’s latest security updates.

</details>

<details>

<summary><strong>How can I be sure the Onside website is official?</strong></summary>

The only official Onside website is [onside.io](https://onside.io).

Always check the domain name in your browser’s address bar. You can also check the website certificate by clicking the padlock icon; it should show “Onside” (or our registered company name) as the verified organization.

</details>

<details>

<summary><strong>Can I download apps from Onside without installing the Onside Store app?</strong></summary>

No. To install apps from Onside, you must first install the Onside Store marketplace app on your iPhone (or iPad in the EU).

</details>

<details>

<summary><strong>Can I download apps without creating an Onside account?</strong></summary>

No registration is currently required. You can download apps without creating an Onside account.

</details>

<details>

<summary><strong>If I delete an old version of a game and download a new one from Onside, will I lose my progress?</strong></summary>

It depends on the game. Some games support progress sync or account-based save data.

Check the game’s app page for developer instructions.

</details>

<details>

<summary><strong>How do I uninstall the Onside Store?</strong></summary>

Uninstall the Onside Store app like any other iOS app:

1. Press and hold the Onside Store app icon on your Home Screen (or in the App Library).
2. Tap **Remove App**.
3. Tap **Delete App** to confirm.

</details>

<details>

<summary><strong>Where are apps from Onside installed on my iPhone?</strong></summary>

Apps you download from Onside install to your device’s internal storage, like apps from other sources. They appear on your Home Screen and in the App Library.

</details>

<details>

<summary><strong>How much free storage do I need to install the Onside Store?</strong></summary>

The Onside Store app is relatively small (around 100MB). However, iOS needs extra space to complete installations.

We recommend at least **2GB** of free storage.

</details>

<details>

<summary><strong>Is installing Onside safe?</strong></summary>

If you install the Onside Store from our official website, [onside.io](https://onside.io/), it is safe.

Apps listed in the Onside Store also undergo review and security checks before we make them available.

</details>

<details>

<summary><strong>Can I install Onside on iPad, Apple Watch, or a computer?</strong></summary>

The Onside Store currently supports:

* **iPhone:** iOS **17.6+** in the EU, iOS **26.2+** in Japan
* **iPad (EU only):** iPadOS **18+**

In **Japan**, Onside Store is **not supported on iPad**.

We do not currently support Apple Watch, or macOS.

</details>

***

{% hint style="info" %}
If you still need help, contact Onside support via the chat in the bottom-right corner, or email <support@onside.io>.
{% endhint %}


# How to Update Onside Store

Keeping your Onside Store app up-to-date is important for the latest features, security, and the best experience. Here’s how to manage updates for the Onside marketplace app itself.

### Automatic updates (recommended)

This is the recommended way to keep the Onside Store up to date.

With automatic updates enabled, iOS/iPadOS typically installs a new version within 24 hours after we release it (depending on your device settings and connectivity).

<details>

<summary><strong>Enable iOS/iPadOS automatic app updates</strong></summary>

1. Go to **Settings > Apps > App Installation**.
2. Ensure **App Updates** is toggled **On**.

This is a general iOS setting that affects many apps.

<div align="left"><figure><img src="/files/JJnbPKwR2qDA4yI4Vok8" alt="" width="188"><figcaption></figcaption></figure></div>

</details>

{% hint style="info" %}
Automatic updates work best with a stable Wi‑Fi connection and sufficient battery.
{% endhint %}

***

### Manual update

If you prefer to manage updates yourself (or if automatic updates are off), you can check for updates manually.

<details>

<summary><strong>Manual update steps</strong></summary>

1. Go to **Settings > Apps**.
2. Scroll down and tap **Onside**.
3. In **App Information**, look for an **Update** button.

{% hint style="info" %}
If you only see the version number and no **Update** button, you already have the latest version installed.
{% endhint %}

4. Tap **Update** to download and install the latest version.

<div align="left"><figure><img src="/files/aXUqPvN25p9xrKOhloFa" alt="" width="188"><figcaption></figcaption></figure></div>

</details>

***

### Troubleshooting

<details>

<summary><strong>The Update button isn’t showing, and the app isn’t updating automatically.</strong></summary>

If you’ve confirmed you have a stable internet connection and you’re sure a newer version is available but it isn’t appearing, reinstall the Onside Store.

{% hint style="warning" %}
Use this as a last resort. Your settings or locally stored data for the Onside Store app might be reset.
{% endhint %}

1. Delete the Onside Store app from your iPhone or iPad.
2. Reinstall the latest version from our official website.

<a href="https://onside.io" class="button secondary">Reinstall from onside.io →</a>

</details>

**If automatic updates aren’t working, also check:**

<details>

<summary><strong>Background App Refresh</strong></summary>

1. Go to **Settings > Apps**.
2. Tap **Onside**.
3. Ensure **Background App Refresh** is toggled **On**.

<div align="left"><figure><img src="/files/zEmdtCNSFXkS4EITGLAZ" alt="" width="188"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>In‑App Content</strong></summary>

1. Go to **Settings > Apps > App Installation**.
2. Ensure **In‑App Content** is toggled **On**.

This is a general iOS setting that affects many apps.

<div align="left"><figure><img src="/files/JJnbPKwR2qDA4yI4Vok8" alt="" width="188"><figcaption></figcaption></figure></div>

</details>

***

{% hint style="info" %}
If you still need help, contact Onside support via the chat in the bottom-right corner, or email <support@onside.io>.
{% endhint %}


# Notifications

Stay up-to-date with your apps! This page explains how to manage notifications from Onside Store.

{% hint style="info" %}
Notifications help you stay informed about app updates for apps you installed through the Onside Store, plus important account and platform messages.
{% endhint %}

### Manage Onside notifications

You control whether the Onside Store can send you notifications.

#### During Onside setup

When you install and set up the Onside Store app, iOS/iPadOS asks whether you want to allow notifications.

* To receive updates and alerts, tap **Allow** when prompted.

<div align="left"><figure><img src="/files/VisfIkDFgRckrKkikY88" alt=""><figcaption></figcaption></figure></div>

#### Change notification settings in iOS Settings

If you declined notifications initially (or want to change your settings later):

1. Open the **Settings** app on your iPhone or iPad.
2. Tap **Notifications**.
3. Scroll down and tap **Onside**.
4. Toggle **Allow Notifications** on or off.

<div align="left"><figure><img src="/files/LMOv01pnWAw9XU6AolM5" alt="" width="188"><figcaption></figcaption></figure> <figure><img src="/files/lhZhkqaNs2nQlXyOkAWN" alt="" width="188"><figcaption></figcaption></figure></div>

***

{% hint style="info" %}
If you still need help, contact Onside support via the chat in the bottom-right corner, or email <support@onside.io>.
{% endhint %}


# Can I Trust Onside?

A guide to understanding Onside's commitment to security, how we ensure apps are safe, and the best practices for using our marketplace securely.

Your trust and security are fundamental to us at Onside. We are committed to creating a safe, reliable, and transparent marketplace for you to discover and enjoy apps on your iPhone. This page outlines the measures we take to ensure Onside is a platform you can depend on.

***

### Our Commitment to Your Security

We understand that using an alternative app marketplace involves trust. Here's how we work to earn and maintain it:

<details>

<summary><strong>1. Operating Legally and Responsibly</strong></summary>

* **EU:** Onside operates in compliance with applicable **European Union (EU) requirements** for digital marketplaces and data protection (including **GDPR**, where relevant).
* **Japan:** Onside operates in compliance with applicable requirements in Japan (including the **Act on the Protection of Personal Information (APPI)**, where relevant).
* **Japan (iOS platform rules):** Alternative app marketplaces in Japan are enabled under the **MSCA**. Apple also applies platform safeguards like **Notarization** and an **authorization process** for app marketplaces.
* **Best Practices:** We are diligent in ensuring our services and practices meet these legal standards to provide you with a secure environment.

</details>

<details>

<summary><strong>2. Protecting Your Data</strong></summary>

* **Data Minimization:** We strive to collect only the minimum data required for Onside to function. We don't collect unnecessary information.
* **Secure Handling:** We employ modern security technologies and best practices to protect the data we do handle, focusing on preventing unauthorized access or data leaks. What you install and use generally stays strictly between you and the app.
* **Learn More:** You can always find more details in our Privacy Policy.

</details>

<details>

<summary><strong>3. Building a Secure Platform</strong></summary>

* **Internal Security Reviews:** While we are a growing platform, we take security very seriously. Our development process includes internal security reviews and testing to identify and address potential vulnerabilities within the Onside Store app itself.
* **Continuous Improvement:** We continuously work to strengthen our infrastructure and implement robust security measures to protect the platform.

</details>

***

### Ensuring Safe Apps on Onside

It's not just about the Onside app itself; we also care deeply about the safety and quality of the apps available *through* Onside.

> Our **"Security Guarantee"** is our promise to you that we take these steps seriously for every app in our store.

<details>

<summary><strong>App Review Process</strong></summary>

* Every app submitted to Onside undergoes a **moderation process**. This includes automated and manual checks for viruses, malware, and other potentially harmful behavior.
* We aim to offer only reliable and verified apps, so you can download with confidence and avoid unpleasant surprises.

</details>

<details>

<summary><strong>Apple platform safeguards</strong> </summary>

In the EU and Japan, iOS applies a baseline security review called **Notarization** to iOS apps distributed outside the App Store.

Notarization helps reduce serious threats (like known malware), but it is **less comprehensive** than Apple’s App Store App Review.

</details>

<details>

<summary><strong>Developer Accountability</strong></summary>

* Developers who publish on Onside are registered with us and must agree to our terms and conditions. This helps to create an accountable and responsible developer ecosystem.

</details>

<details>

<summary><strong>Content Standards &#x26; Legal Compliance</strong></summary>

* We expect all apps on Onside to be lawful in the regions they are distributed.
* For apps in categories like Adult Content, Crypto, or Gambling, we require them to be fully compliant with all local laws, including appropriate age-gating and responsible presentation.
* Our moderation team also works to ensure that apps do not direct users to malicious websites or intentionally harm your device or data.

</details>

<details>

<summary><strong>Secure In-App Payments</strong></summary>

* When you make purchases for paid apps or in-app content through Onside, we use secure and trusted payment methods like Apple Pay and direct card payments. Your payment information is handled securely by our payment processors according to industry standards.

</details>

***

### Our Promise to You

We are dedicated to building Onside into a trustworthy alternative for discovering and enjoying iOS apps. This means:

* **Transparency:** Being open and clear about our practices.
* **Security Focus:** Continuously working to protect our users and their data.
* **Responsiveness:** Listening to user feedback to improve our platform.

While no system can be 100% immune to all threats, we are committed to employing best efforts and industry-standard practices to make Onside a safe and reliable choice for you.

{% hint style="info" %}
**Downloading Onside Safely:** To ensure you are getting the genuine and secure version of Onside Store, please **only download it from our official website:** [**onside.io**](https://onside.io). Be wary of third-party websites offering Onside downloads, as these may not be safe.
{% endhint %}

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# How to Sign Up?

Create an Onside account with your phone number to manage purchases, your profile, and other account-based features.

To make purchases in Onside Store, you need to sign in or create an account. Currently, Onside supports signing in with your mobile phone number using **SMS** or **WhatsApp**.

You can browse and download apps without an account.

Sign in if you want to manage purchases, your profile, and other account-based features.

Currently, Onside supports signing in with your mobile phone number using **SMS** or **WhatsApp**.

![](/files/XPMuqM1Sl1i6vxX51EZj)

We take privacy seriously. We store your phone number securely and do not share it with third parties for marketing.

### Register or sign in

1. **Open the Onside Store app**
   * Launch the Onside Store app on your iPhone (or iPad in the EU).
   * On the welcome screen, tap **Log in with mobile number**.
2. **Enter your phone number**

   * Select your country code. You can register with a phone number from any country (not only the EU or Japan).
   * Enter your mobile phone number.
   * Choose how you want to receive your verification code: **WhatsApp** or **SMS**.
   * By continuing, you agree to our [Terms of Use](https://onside.io/terms-of-use) and [Privacy Policy](https://onside.io/privacy-policy).

   ![](/files/vaCmfbEoIaGvzhl5UxBK)
3. **Enter the verification code (OTP)**

   * We send a one-time code using your chosen method.
   * Enter the code in the Onside Store app.
   * If you don’t receive the code, tap **Send again** or switch the delivery method after the waiting period.

   ![](/files/osjTdzFhUqMq61QyV0ZW)
4. **Complete your profile**

   * Enter the following details:
     * **First name**
     * **Last name**
     * **Date of birth** (used for age-appropriate content)
     * **Country** (used to determine availability and default language; you can change this later)
   * Tap **Next**.

   ![](/files/eUvGZoTYTPtn0ztU7mkt)
5. **Allow notifications (optional)**

   * iOS will ask whether Onside can send notifications.
   * Allowing notifications helps you stay informed about app updates and account or billing events. You can change this later in iOS Settings or in the Onside Store app.

   ![](/files/nyYPzhN0U5IIHun3bp8T)
6. **You’re all set**
   * Your Onside account is ready. You can now manage purchases, your profile, and other account-based features.

### Future sign-in options

We plan to add additional sign-in methods in the future.

***

If you have questions, contact support via the chat in the bottom-right corner or email <support@onside.io>.


# How to Log Out?

Log out of the Onside Store safely: find the logout option, end sessions on iOS, switch accounts, and troubleshoot issues (missing button, lost device, session not closing).

If you need to sign out of your Onside Store account on your iPhone (or iPad in the EU), follow the steps below. After you log out, you must sign in again to access your profile, downloads, and personalized content.

1. **Open the Onside Store app**
2. **Open your profile**
   * Tap your profile icon (top corner of the main screen).
3. **Log out**
   * Scroll to the bottom of the profile menu.
   * Tap **Log out**.

You are now signed out. To use personalized features again, sign in with your mobile number.

***

If you have questions, contact support via the chat in the bottom-right corner or email <support@onside.io>.


# How to Delete Your Account?

If you wish to permanently delete your Onside Store account and its associated data, please follow these steps.

Deleting your Onside Store account is permanent. Read the details below before you continue.

### Before you delete your account

* **Permanent action:** You cannot undo account deletion.
* **Loss of data:** You lose access to your Onside profile, Onside purchase history, your download history in Onside, and any account-level settings.
* **Installed apps:** Apps you already installed remain on your device. However, you may lose access to Onside-specific features (such as updates) if they require an active Onside account.
* **Subscriptions:** If you have active subscriptions managed through Onside, cancel or manage them **before** you delete your account to avoid unintended charges. Deleting your Onside account may not automatically cancel third-party subscriptions.

### Delete your account

1. **Open the Onside Store app**
2. **Open your profile**
   * Tap your profile icon (top corner of the main screen).
3. Open **Settings**
4. **Delete account**
   * In **Settings**, scroll down.
   * Tap **Delete account**.
5. **Confirm deletion**
   * Follow the on-screen prompts to finish.

After deletion, you can’t sign in with the same account. If you want to use Onside again, you must create a new account.

***

If you have trouble deleting your account, or you have questions before you continue, contact support at <support@onside.io>.


# Main Sections

Understanding the Onside App Interface

This guide explains how to navigate the Onside Store app and what each main section does.

To open Onside Store, tap the Onside Store icon on your iPhone’s (or iPad’s) Home Screen.

{% hint style="info" %}
iPad support is available **in the EU only** (iPadOS **18+**). Onside Store is not available on iPad in Japan.
{% endhint %}

If it’s your first time using Onside Store (or you logged out), the app will ask you to **Log in**.

![](/files/tqjji8eFKT60LQdcJMtP)

### Main navigation

Onside Store has four main sections, accessible from the bottom navigation bar:

1. ![](/files/3p7jOLPAduby609iWGIU) **Now**: A curated feed of recommendations.
2. ![](/files/OlHih1yrUe86fULtpvaE) **Apps**: Browse all non-game apps.
3. ![](/files/XN53bHkN43HaABiaMquK) **Games**: Browse all games.
4. ![](/files/AwoTJkZZLKkhHJhMsAHx) **18+**: Restricted categories (availability depends on your age and region).

***

### Now

The **Now** tab is your discovery hub. It shows curated collections that can include apps, games, or both.

* Tap the **Now** icon in the bottom navigation bar.
* Open a collection to see the apps and games inside it.

***

### Apps

The **Apps** tab contains non-game applications.

* Tap the **Apps** icon in the bottom navigation bar.
* Browse featured apps, categories, and curated lists.
* Tap an app to open its page, where you can review details and install or update it.

***

### Games

The **Games** tab contains games.

* Tap the **Games** icon in the bottom navigation bar.
* Browse featured games, categories, and curated lists.
* Tap a game to open its page, where you can review details and install or update it.

***

### 18+

The 18+ tab includes apps from restricted categories. Onside Store only shows this content when it is legal for your region and appropriate for your account.

* Tap the 18+ icon in the bottom navigation bar.

{% hint style="warning" %}
Access to content in **18+** may require age verification and may be limited by regional legal restrictions.
{% endhint %}

***

If you have questions, contact support via the chat in the bottom-right corner or email <support@onside.io>.


# App Page

A detailed guide to all the sections and features you'll find on an app or game's product page in the Onside Store.

When you tap on any app or game in Onside, you'll land on its **App Page**. This page is your hub for all the key details you need to decide if you want to install it. Here’s a breakdown of what you’ll find.

<div align="left"><figure><img src="/files/IafmccNQjd8bsw9BrtM7" alt="" width="188"><figcaption></figcaption></figure></div>

***

### The Main Action Button (Install / Update / Open)

This is the primary action button, always visible at the bottom of the screen. Its text changes based on the app's status:

* **Install:** Appears if the app is not on your device. If it's a paid app, the price will be displayed here.
* **Update:** Appears if you have an older version of the app installed.
* **Open:** Appears if you already have the latest version installed.

***

### App Information & Previews

This section gives you an at-a-glance look at the app.

* **App Header:** At the top, you'll see the app's **Icon**, **Name** (e.g., MR RACER), and a short **Subtitle** (e.g., High-Speed Supercar Racing).
* **Description:** A summary of the app's purpose. Tap **More** to read the full text.
* **Screenshots & Videos:** A visual gallery showcasing the app's interface and features in action. You can often find gameplay videos here.

***

### Ratings, Version & Developer Details

Here you can dig deeper into the app's specifics and community feedback.

* **Ratings & Reviews:** Check the average star rating and read opinions from other users to help you decide. You may also be able to rate and review the app yourself.
* **Last Version:** See the latest version number, release date, and a "What's New" section detailing recent changes. If a developer hasn't provided notes, you'll see a placeholder like "A new version is here."
* **App Developer:** Find out who made the app. Tapping this may show you more apps from the same developer.

<details>

<summary><strong>View Detailed Information</strong></summary>

This expandable section contains key technical details:

* **Category:** The app's genre (e.g., Productivity, Racing).
* **Size:** How much storage space the app requires.
* **Compatibility:** The minimum iOS version needed (e.g., iOS 16.4 or later).
* **Languages:** The languages the app supports.
* **Age Rating:** The recommended age for users (e.g., 4+).
* **In-App Purchases:** Indicates if the app offers items for sale within the app itself.

</details>

***

### Support & More to Discover

* **Privacy Policy & App Support:** At the bottom of the information section, you'll find direct links to the developer's privacy policy and their support page if you need help.
* **"More by..." Section:** Below the main details, Onside will suggest other similar apps or more titles from the same developer to help you find new favorites.

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# Changing the App Theme

Onside Store is designed to look great on your iPhone, and we offer different appearance options to suit your preference.

Your trust and security are fundamental to Onside. We aim to operate a safe, reliable, and transparent marketplace where you can discover and install apps on your iPhone.

***

### Our commitment to security

Using an alternative app marketplace requires trust. Here’s what we do to earn and maintain it.

<details>

<summary><strong>1. Operating legally and responsibly</strong></summary>

* **EU and Japan compliance:** Onside operates in compliance with applicable requirements for digital marketplaces and data protection, including **GDPR** (EU) and **APPI** (Japan), where relevant.
* **Best practices:** We regularly review our processes to ensure we meet these standards and provide a secure environment.

</details>

<details>

<summary><strong>2. Protecting your data</strong></summary>

* **Data minimization:** We aim to collect only the data required for Onside to function.
* **Secure handling:** We use modern security controls and best practices to protect the data we handle and reduce the risk of unauthorized access.
* **Learn more:** See our Privacy Policy for details.

</details>

<details>

<summary><strong>3. Building a secure platform</strong></summary>

* **Security reviews:** Our development process includes internal security reviews and testing to identify and address vulnerabilities in the Onside Store app.
* **Continuous improvement:** We continuously strengthen our infrastructure and platform security controls.

</details>

***

### App safety on Onside

Security is not only about the Onside Store app itself. We also work to improve the safety and quality of apps distributed through Onside.

<details>

<summary><strong>App review process</strong></summary>

* Every app submitted to Onside goes through a **moderation process**. This includes automated and manual checks intended to detect malware, suspicious behavior, and policy violations.

{% hint style="info" %}
Moderation reduces risk, but it cannot guarantee that every issue will be detected.
{% endhint %}

</details>

<details>

<summary><strong>Developer accountability</strong></summary>

* Developers who publish on Onside must be registered and agree to our terms. This helps us maintain accountability and enforce platform policies.

</details>

<details>

<summary><strong>Content standards &#x26; legal compliance</strong></summary>

* We expect all apps on Onside to be lawful in the regions where they are distributed.
* For categories such as Adult Content, Crypto, or Gambling, we require compliance with local laws, including appropriate age gating and responsible presentation.
* We also review apps to help prevent redirection to malicious websites or intentional harm to devices or user data.

</details>

<details>

<summary><strong>Secure in-app payments</strong></summary>

* For paid apps and in-app purchases supported by Onside, we use trusted payment methods such as Apple Pay and card payments. Payment details are handled by our payment processors according to industry standards.

</details>

***

### Our promise to you

We aim to build Onside into a trustworthy way to discover and install iOS apps. This means:

* **Transparency:** We explain our practices clearly.
* **Security focus:** We continuously improve our protections.
* **Responsiveness:** We use user feedback to guide improvements.

While no system is immune to all threats, we apply best efforts and industry-standard practices to make Onside a safe and reliable choice.

{% hint style="info" %}
**Download Onside safely:** Only download the Onside Store from our official website: [onside.io](https://onside.io).
{% endhint %}

***

If you have questions, contact support via the chat in the bottom-right corner or email <support@onside.io>.


# Updating Apps in Onside

Learn how to update apps in Onside: check for new versions, enable auto-updates in settings, troubleshoot failed updates, and manage rollbacks.

Keeping your apps up-to-date is important for getting the latest features, bug fixes, and security improvements. Onside Store makes it easy to manage updates for apps you've installed through our marketplace.

### Why Update Your Apps?

Developers regularly release new versions of their apps to:

* Fix bugs and improve performance.
* Introduce new features and content.
* Enhance security and stability.
* Ensure compatibility with the latest iOS updates.

Regularly updating your apps ensures you have the best and most secure experience.

### How App Updates Work in Onside

There are two main ways your apps can be updated through Onside:

1. **Automatic Updates via iOS Settings (Recommended)**
2. **Manual Updates through the Onside App**

#### 1. Automatic Updates via iOS Settings

This is the most seamless way to keep your Onside apps (and all your other iPhone/iPad apps) current.

* **How it Works:**\
  Your iPhone (or iPad) has a built-in feature to automatically update apps. When developers release new versions of apps you've installed (whether from Onside or the main App Store), and Onside makes these updates available, your device can download and install them automatically in the background, usually when connected to Wi-Fi and charging.
* **Enabling iOS Automatic App Updates:**
  1. Go to your iPhone/iPad **Settings > Apps > App Installation**.
  2. Under "Automatic Downloads," make sure **App Updates** is toggled **ON.**

<div align="left"><figure><img src="/files/JJnbPKwR2qDA4yI4Vok8" alt="" width="188"><figcaption></figcaption></figure></div>

{% hint style="info" %}
We strongly recommend enabling "App Updates" in your iPhone's settings for a convenient, hands-off experience! Onside ensures updates are ready, and iOS takes care of the rest if this setting is active.
{% endhint %}

#### 2. Manual Updates through the Onside App

If you prefer to review and install updates yourself, or if iOS automatic updates are turned off, you can easily manage them manually within the Onside Store app.

**A. From the "Updates" Section in Your Onside Profile:**

This is your central hub for all pending updates for apps installed via Onside.

1. Open the **Onside Store** app.
2. Navigate to your **Profile** screen.
3. Tap on the **"Updates"** section.
4. You'll see a list of all apps that have an update available from Onside.
   * **Update All:** Look for an **"Update All"** button to update every app in the list simultaneously.
   * **Update Individually:** Tap the **"Update"** button next to each specific app to update it separately.

**B. From an App's Page in Onside:**

You can also initiate an update directly from an app's product page.

1. Find the app within the Onside Store (e.g., by browsing the Now, Apps, or Games tabs, or by searching).
2. Tap the **"Update"** button to download and install the latest version.

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# Troubleshooting App Updates

Find solutions for Onside app update issues: enable auto-updates, fix storage or network problems, restart the app or iPhone, and resolve stuck or failed installs.

Having trouble with app updates from Onside, or just have some questions? This page should help!

### Troubleshooting Common Update Issues

<details>

<summary>Why aren't my apps updating automatically through Onside?</summary>

* **Check iOS/iPadOS Settings:** First and foremost, ensure "App Updates" is enabled in your iPhone/iPad settings. This is the primary control for all app auto-updates on your device. If this is off, Onside cannot force an automatic update.
* **Wi-Fi Connection:** iOS usually performs automatic updates when connected to Wi-Fi to save mobile data. Ensure you've been on a stable Wi-Fi network.
* **Battery Level & Storage:** Low battery (typically below 50% unless charging) or insufficient storage space on your device can prevent automatic updates.
* **Background App Refresh for Onside:** While iOS manages the core auto-update, ensuring Onside has "Background App Refresh" enabled (in **Settings > General > Background App Refresh**) might help it detect and prepare updates more effectively for iOS to then process.
* **Open Onside Occasionally:** Sometimes, simply opening the Onside app can help it sync the latest update information, which iOS can then use for automatic updates if they are enabled.

</details>

<details>

<summary>What if an update through Onside fails or gets stuck?</summary>

* **Check Internet Connection:** A stable Wi-Fi or mobile data connection is crucial.
* **Check Storage Space:** Make sure your iPhone has enough free storage. Updates need space to download and install.
* **Restart the Onside App:** Force close the Onside app and then reopen it.
* **Restart Your Device:** This classic troubleshooting step can resolve many temporary glitches.
* **App No Longer Available or Update Pulled?** In rare instances, a developer might remove an app from Onside, or an update might be temporarily withdrawn if an issue is found.
* **Contact Support:** If the issue persists for a specific app update from Onside, please let us know at <support@onside.io>. Be sure to mention the app name and any error messages you see.

</details>

<details>

<summary>Why does Onside sometimes show an update for an app I think is already updated (e.g., via the main App Store)?</summary>

This can occasionally happen due to slight delays in how different platforms report version numbers, or if you have the app installed from multiple sources.

* **Recommendation:** If an app is available on both Onside and the main App Store, it's generally best to consistently update it from one primary source to avoid version discrepancies.
* **If this occurs:** Try opening the app in question. If it's indeed the latest version, the "Update" prompt in Onside for that specific app should eventually clear after Onside syncs. A quick app restart (for Onside) or a device restart might also help.

</details>

<details>

<summary>Why don't I see an "Update" button for some of my apps in Onside?</summary>

* **Already Updated:** You likely already have the latest version that Onside is aware of for that app.
* **Installed from Elsewhere:** The app might have been installed from a different source (e.g., directly from the main App Store, if it's also listed there). Onside typically manages updates for apps that it tracks as being installed *through* the Onside marketplace.

</details>

### General App Update FAQs

<details>

<summary>Can I trust updates coming through Onside? Are they secure?</summary>

Yes. Apps and their updates made available through Onside are subject to our review processes, which include security checks. We are committed to providing a safe and trustworthy source for your app updates.

</details>

<details>

<summary>How often should I check for manual updates if I have automatic updates turned off?</summary>

If you prefer manual updates, it's a good idea to check the "Updates" section in your Onside Profile at least once a week. Developers release updates at different times, so regular checks will ensure you don't miss out on important improvements.

</details>

Regularly checking for and installing updates helps ensure your apps run smoothly, securely, and with all the latest features. Onside aims to make this process as straightforward as possible for you!

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# Payment Methods & Currencies

An overview of the payment methods and currencies currently supported by Onside for all purchases.

Onside offers secure and convenient ways to pay for apps, in-app purchases, and subscriptions. This guide outlines the payment methods and currencies we currently accept.

***

### Accepted Payment Methods

We currently support the following payment methods for all your purchases within the Onside Store:

* **Bank Cards:** We accept major credit and debit cards.
  * Visa
  * Mastercard
* **Apple Pay:** You can use Apple Pay for a quick and secure checkout experience if you have it set up on your iPhone with a valid payment card.

> **Upcoming Payment Options:** We are always working to expand your payment choices! We plan to add support for popular alternative payment methods in the future. Stay tuned for updates!

***

### Supported Currencies

Currency support depends on your region and payment method.

Today, we natively support transactions in a set of currencies commonly used across the EU.

<details>

<summary><strong>View full list of supported currencies</strong></summary>

* Euro (EUR)
* Czech Koruna (CZK)
* Danish Krone (DKK)
* Hungarian Forint (HUF)
* Polish Złoty (PLN)
* Romanian Leu (RON)
* Swedish Krona (SEK)

</details>

#### What if my local currency is not on the list?

If your local currency is not yet directly supported, you will see prices displayed in **Euros (€)** within the Onside Store.

{% hint style="info" %}
Even if prices are shown in Euros, you will still pay in your local currency. Your bank will handle the conversion automatically at the current exchange rate during the transaction. Please be aware that in some cases, your bank may apply a small conversion fee for this service.
{% endhint %}

We are working on expanding our list of supported currencies to provide an even more seamless experience for all users.

***

If you have any questions about payment methods, please contact our support team at <support@onside.io>.


# Linking a Bank Card to Onside

Link your bank card to your Onside account for fast and easy purchases of apps, in-app items, and subscriptions.

Link your bank card or use Apple Pay to make purchasing apps, in-app items, and subscriptions fast, easy, and secure. This guide covers how to add and manage your payment options.

***

### How to Add a Payment Method

There are two main ways to add a payment method to your Onside account:

<details>

<summary><strong>1. Through Your Onside Profile</strong> </summary>

1. Open the **Onside Store** app on your iPhone.
2. Go to your **Profile** screen.
3. In your Profile, tap on **"Payment methods"**.
4. Tap **"Add Card"** (or a similar button).
5. Enter your card details: card number, expiration date, and CVV/CVC code.
6. Your bank will likely require you to verify the card through a standard security step (like a code sent via SMS or a banking app notification). Follow the on-screen instructions to complete the verification.

</details>

<details>

<summary><strong>2. During the Checkout Process</strong></summary>

You can also add a new card when you make your first purchase.

1. Select an app, in-app item, or subscription you wish to purchase.
2. Proceed to the payment or checkout screen.
3. If no payment method is on file, you'll be prompted to enter your card details or select Apple Pay.
4. Follow the on-screen instructions. You will have the option to save the new card to your Onside account for future purchases.

</details>

> **Using Apple Pay:** Onside seamlessly integrates with Apple Pay. You can select Apple Pay as your payment method during any checkout without needing to manually link each card within Onside's settings first.

***

### Security of Your Payment Information

At Onside, the security of your payment information is a top priority.

* **Secure Storage & Tokenization:** Your full card details are never stored directly by Onside in an unencrypted format. We use industry-standard security practices like **tokenization**, which replaces your actual card number with a unique, secure token for processing payments.
* **Encryption:** All payment information transmitted between your device and our servers is protected using strong encryption (SSL/TLS).
* **Compliance:** We adhere to relevant payment industry security standards to ensure your data is handled safely.

***

### Frequently Asked Questions (FAQs)

<details>

<summary><strong>Why should I link a card?</strong></summary>

* **Faster Checkouts:** Purchase apps and make in-app payments quickly, often with just a tap, without re-entering your details every time.
* **Convenient Subscriptions:** Easily manage and renew your app subscriptions. With a linked payment method, subscriptions can auto-renew seamlessly (you will always be informed before any renewal charge).

</details>

<details>

<summary><strong>How do I manage or remove a linked card?</strong></summary>

You can view and manage your linked payment methods at any time.

1. Open the **Onside Store** app.
2. Go to your **Profile > Payment methods**.
3. Here you can:
   * See a list of your linked cards (showing only the last few digits for security).
   * Set a default payment method if you have multiple linked.
   * **Remove a Card:** Select the card you wish to remove and tap the delete icon. Confirm the removal.

</details>

<details>

<summary><strong>What types of cards are accepted on Onside?</strong></summary>

Onside supports major credit and debit cards such as Visa and Mastercard. We also fully support payments via Apple Pay. Specific card acceptance may depend on your region and our payment processing partners.

</details>

<details>

<summary><strong>Why am I having trouble linking my card?</strong></summary>

* **Check Card Details:** Ensure you've entered the card number, expiration date, CVV/CVC, and all other required information correctly.
* **Card Expiration:** Verify that your card has not expired.
* **Sufficient Funds/Credit:** Make sure your card has enough funds or available credit for any small verification hold that might be temporarily placed (this is usually refunded quickly).
* **Bank Restrictions:** Your bank might have restrictions on online or international payments. Contact your bank to ensure such transactions are enabled for your card.
* **VPN/Location Issues:** Turn off any VPN services. Your card's issuing country should generally align with your Onside account region.
* **Temporary Service Issues:** Occasionally, there might be temporary issues with payment gateways. If you're certain your card details are correct and the card is active, please try again after a short while.
* **Still having issues?** If problems persist, contact your bank first to check for issues with your card. If your bank confirms the card is fine, then contact Onside support at <support@onside.io> for assistance.

</details>

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# Purchasing Paid Apps

A guide on how to purchase apps with a one-time cost in the Onside Store, including information on refunds and re-downloading.

This guide explains how to purchase apps that have a one-time cost, as well as how to manage those purchases.

***

### How to Purchase a Paid App

The process is designed to be simple and secure.

1. **Find the App You Want**
   * Browse or search for the app in the **Now, Apps, or Games** tabs within the Onside Store.
   * Tap on the app to open its dedicated App Page.
2. **Tap the Price Button to Start**
   * On the app's page, the main action button at the bottom will display the price (e.g., **€1.99**).
   * Tap this button to begin the purchase process.
3. **Confirm Your Purchase**
   * An Apple Pay confirmation sheet will appear.
   * Authenticate the purchase using **Face ID, Touch ID, or your device passcode**, just like you would for any other secure transaction.
4. **Download & Install**
   * After your purchase is confirmed, the app will automatically begin to download and install on your device.
   * The button will show the download progress and will change to **"Open"** once the installation is complete.

{% hint style="info" %}
Once you've purchased a paid app, it's linked to your Onside account forever. The button on its app page will change from a price to "Open" (if installed) or "Download" (if not currently on your device), and you will never be charged for it again.
{% endhint %}

***

### Common Questions About Paid Apps

<details>

<summary><strong>What if I change my mind? How do refunds work?</strong></summary>

If you are unsatisfied with a purchase or made one by mistake, you can request a refund directly through the Onside app.

1. Go to your **Profile** screen in the Onside app.
2. Tap on the **"Purchases"** section to see your order history.
3. Find the specific order you want a refund for.
4. Tap the **"Support chat"** button associated with that order.
5. Tapping this button will guide you to our support team, who will review your request and assist you with the next steps.

<div align="left"><figure><img src="/files/Nmv0dkS2dVR9C5qId1WO" alt="" width="188"><figcaption></figcaption></figure></div>

</details>

<details>

<summary><strong>Can I re-download a paid app I've already purchased?</strong></summary>

Yes. Any paid app you purchase is permanently linked to your Onside account. You can re-download it for free at any time on any iPhone where you are logged in with the **same Onside account**. Simply find the app in the store, and the button will show "Download" instead of a price.

Onside Store availability depends on your device and region (for example, iPad support is EU-only).

</details>

<details>

<summary><strong>Do I need to have a card linked to Onside to buy an app?</strong></summary>

You need a valid payment method. This can be a card you have linked directly to your Onside account or a card associated with your Apple Pay, which you can select during the checkout process.

</details>

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# Troubleshooting Payment Issues

A guide to resolving common payment problems, from declined transactions to refund requests and questions about currencies.

Having trouble with a payment? It can be frustrating, but don't worry. This guide covers the most common payment issues and how to solve them.

***

### Common Payment & Refund Questions

<details>

<summary><strong>My payment was declined. What should I do?</strong></summary>

A "Payment Declined" error can happen for several reasons. Here is a checklist of things to verify:

* **Check Card Details:** Double-check that you've entered the card number, expiration date, and CVV/CVC code correctly. A simple typo is a common cause.
* **Ensure Sufficient Funds:** Make sure your bank account has enough funds or your credit card has enough available credit to cover the purchase.
* **Verify Card is Active:** Ensure your card has not expired.
* **Check Bank Restrictions:** Some banks have restrictions on online or international payments. You may need to contact your bank to ensure such transactions are enabled for your card.
* **Disable VPN:** If you are using a VPN, try turning it off temporarily, as it can sometimes interfere with payment gateway security checks.

If you have checked all of the above and the payment still fails, we recommend contacting your bank first to see if they are blocking the transaction. If your bank confirms there is no issue on their end, please contact our support team at <support@onside.io>.

</details>

<details>

<summary><strong>How do I request a refund?</strong></summary>

The process for requesting a refund depends on what you purchased.

* **For Paid App Purchases (bought directly from Onside):**\
  If you purchased an entire app from the Onside Store and would like a refund, please contact our support team directly.
  1. Go to your **Profile** screen in the Onside app.
  2. Tap on the **"Purchases"** section to see your order history.
  3. Find the order you want a refund for and tap the **"Support chat"** button.
  4. This will guide you to our support team, who will review your request and assist you. *Alternatively, you can go to **Settings > Contact Us** or email us directly at* [*support@onside.io*](mailto:support@onside.io)*.*

<div align="left"><figure><img src="/files/hvrFzJpkypWykXyJY9gx" alt="" width="188"><figcaption></figcaption></figure></div>

* **For In-App Purchases & Subscriptions (bought&#x20;*****inside*****&#x20;an app):**\
  If you purchased an item, subscription, or feature *inside* an app (e.g., in-game currency, an ad-free upgrade), these purchases are managed by the app's developer. **You must contact the developer of that specific app directly to request a refund.** You can usually find their contact information or a "Support" link on their App Page within Onside.

</details>

<details>

<summary><strong>I was charged, but I didn't get my app or item. What should I do?</strong></summary>

First, try these simple steps:

* Completely close and restart the app you were using (or the Onside Store app if it was a paid app purchase).
* Restart your iPhone.

If you still haven't received your purchase:

* For an **In-App Purchase or Subscription**, please contact the app's developer directly.
* For a **Paid App** that failed to download after payment, please contact Onside support at <support@onside.io> with your purchase details.

</details>

<details>

<summary><strong>Can I pay in a currency that is different from what's displayed?</strong></summary>

Yes. If your local currency is not yet directly supported by Onside, you will see prices displayed in **Euros (€)**. However, you will still pay in your local currency. Your bank will handle the conversion automatically at the current exchange rate during the transaction.

{% hint style="info" %}
Please be aware that in some cases, your bank may apply a small conversion fee for transactions made in a different currency. This is determined by your bank's policies. We are continuously working to support more local currencies.
{% endhint %}

</details>

<details>

<summary><strong>Where can I find my purchase history?</strong></summary>

You can view a complete history of your purchases made through Onside by navigating to your **Profile > Purchases** within the Onside Store app.

</details>

***

If you have any further questions, please contact our support team. You can reach us via the chat in the bottom-right corner or by email at <support@onside.io>.


# Keeping Your iPhone Secure

Learn the essential security practices for your iPhone and understand Onside's commitment to creating a safe marketplace.

Your iPhone's security is important. While Onside Store strives to provide a safe marketplace, practicing good general security habits will help protect your device and personal information. Here are some key recommendations.

***

### Onside's Commitment to Your Security

> We are committed to earning and maintaining your trust. Here’s how we help keep you safe:
>
> * **App Moderation:** Apps available on Onside Store undergo a review process, including security checks, to help ensure they are reliable and safe.
> * **Secure Platform:** We use industry-standard practices to protect our platform and your interactions with it. We comply with applicable requirements in the EU and Japan.

By following the general security practices below and relying on trusted sources like Onside Store, you can help keep your iPhone and your data safe.

***

### Essential Security Practices for Your iPhone

<details>

<summary><strong>Download from Trusted Sources</strong></summary>

Always download and update apps from official sources like the Onside Store and Apple's App Store. Avoid downloading apps from unofficial websites or unverified links. The only official Onside website is [**onside.io**](https://onside.io).

</details>

<details>

<summary><strong>Be Wary of "Pirated" or Modified Apps</strong></summary>

Installing unofficial or "cracked" versions of paid apps can be risky. These modified apps might contain malware or spyware that could compromise your device, including access to sensitive information like banking details. Stick to legitimate app versions from trusted marketplaces.

</details>

<details>

<summary><strong>Recognize Suspicious Device Activity</strong></summary>

Pay attention if your iPhone starts behaving unusually. Examples include:

* Sending messages or making calls you didn't initiate.
* Apps opening or closing on their own.
* Unexplained spikes in data usage.
* Rapid battery drain.

If you notice such activity, immediately disconnect from the internet (e.g., enable Airplane Mode). Review recently installed apps and remove any that seem suspicious.

</details>

<details>

<summary><strong>Keep Your iOS Updated</strong></summary>

Apple regularly releases iOS updates that include important security patches. Enable automatic updates by going to **Settings > General > Software Update > Automatic Updates** and turning on both **Download iOS Updates** and **Install iOS Updates**.

</details>

<details>

<summary><strong>Use Strong, Unique Passcodes &#x26; Passwords</strong></summary>

* **Device Passcode:** Set a strong passcode (or use Face ID/Touch ID) to unlock your iPhone. This is your first line of defense if your device is lost or stolen.
* **App & Account Passwords:** For all your online accounts, including Onside and your Apple ID, use strong, unique passwords. Avoid reusing passwords across different services.

</details>

<details>

<summary><strong>Be Cautious with Links and Messages</strong></summary>

Do not click on suspicious links or open attachments in messages (SMS, iMessage, email, etc.) from unknown senders. If you receive a suspicious message claiming to be from a company, contact them directly through their official website or app, not by using the links in the message.

</details>

<details>

<summary><strong>Don't Install Apps at the Request of Strangers</strong></summary>

If someone calls you unexpectedly and asks you to install an app or configuration profile (perhaps claiming to be tech support), it's very likely a scam. Do not follow their instructions. Always find official information and safe apps through trusted sources.

</details>

<details>

<summary><strong>Enable Two-Factor Authentication (2FA)</strong></summary>

Wherever possible, enable 2FA for your important accounts (especially your Apple ID, email, and banking). 2FA adds an extra layer of security, ensuring only you can access your account, even if someone else learns your password. For your Apple ID, you can check your 2FA status in **Settings > \[Your Name] > Password & Security**.

</details>

<details>

<summary><strong>Review App Permissions Regularly</strong></summary>

iOS allows you to control what data apps can access (e.g., your location, photos, microphone). Periodically review these permissions in **Settings** by tapping on an individual app. Only grant permissions that are necessary for the app to function as you expect.

</details>

***

If you ever have security concerns related to the Onside Store or an app downloaded from it, please contact us immediately at <support@onside.io>.


# Participate in UX Research (Users)

An invitation for Onside Store users to participate in feedback sessions and help us build an even better app marketplace.

Hi there! At Onside, we're building an app store that users love. To do that, we need to hear directly from you. Your experience and ideas are essential to helping us grow and improve. ❤️

We are currently looking for users to join us for a friendly, **30-minute online chat in English** to discuss your experience with the Onside Store. This is your chance to share your thoughts directly with our product team!

If you're ready to share your experience, you can sign up right away: <a href="https://forms.gle/X4THkBA1KJQr6rm77" class="button primary" data-icon="heart">Share Your Thoughts</a>

***

### What's in It for You?

We know your time is valuable, and we want to thank you for helping us out! As a token of our appreciation, all interview participants are eligible for great benefits:

* 🎁 **A special bonus from us** as a thank you for your time and insights.
* ✨ **A chance to directly influence** new features and improvements.
* 🚀 **An early look** at what's coming next for the Onside Store.

Interested in the benefits and ready to share your feedback?&#x20;

<a href="https://forms.gle/X4THkBA1KJQr6rm77" class="button primary" data-icon="heart">Fill Out the Form</a>

***

### What We'll Talk About

This is a relaxed, informal conversation. We simply want to hear about your experience with the Onside Store. For example:

* What you enjoy most about using Onside.
* Anything that you find confusing or think could be easier to use.
* What new features or types of apps you would love to see in the store.

Your voice helps us build a better app store for everyone. We truly appreciate your willingness to help! 🙏💙

&#x20;<a href="https://forms.gle/X4THkBA1KJQr6rm77" class="button primary">Yes, I'll Help!</a>


# Data Collection Policy

How Onside collects, uses, protects, and handles personal information in Japan, including user rights under APPI, cross-border data handling, and contact details for privacy requests.

## Commitment to User Privacy

At Onside, we are committed to protecting your privacy and ensuring transparency in how your personal information is collected, used, and managed. This policy explains our data collection and processing practices in accordance with the Act on the Protection of Personal Information of Japan (APPI) and related guidelines.

Onside is operated by Onside.io B.V.

## 1. Collection And Use Of Personal Information

We collect and use personal information only to the extent necessary to operate the marketplace and provide related services.

### **1.1 User Authorization And Marketplace Access**

To provide access to the Onside app marketplace, we collect the following personal information during account creation and authentication:

* Full name – to identify the user and manage the account
* Date of birth – to verify age eligibility and comply with applicable legal requirements
* Phone number – to identify and authenticate users
* Email address – to communicate with users regarding their account, security, and marketplace activity

Purpose Of Use:

* User identification and authentication
* Account creation and management
* Age verification and compliance with legal requirements
* Security, fraud prevention, and customer support

Legal Basis:

Personal information is processed based on proper acquisition and use within the scope of the stated purpose, as permitted under APPI.

### **1.2 Marketing And Marketplace Communications**

With your consent, we may use personal information to send information related to the marketplace, including product updates, service announcements, and promotional materials.

For this purpose, we may collect and use:

* Full name – to personalize communications
* Email address – to deliver communications
* Usage data – to understand user preferences and improve the relevance of marketplace content

Legal Basis:

* User consent, where required under APPI
* Legitimate use within the disclosed purpose of utilization

Users may opt out of marketing communications at any time.

## 2. Management And Protection Of Personal Information

We implement appropriate technical and organizational measures to prevent unauthorized access, loss, alteration, or leakage of personal information. Access to personal data is limited to authorized personnel and trusted service providers who require such access for operational purposes.

## 3. User Rights Under Japanese Law

In accordance with APPI, users have the following rights regarding their personal information:

### **3.1 Right To Disclosure**

Users may request disclosure of the personal information we hold about them, including the purpose of use.

### **3.2 Right To Correction Or Deletion**

Users may request correction, addition, or deletion of inaccurate or outdated personal information.

### **3.3 Right To Suspension Of Use**

Users may request suspension of use or deletion of personal information if it is handled beyond the stated purpose or obtained improperly.

### **3.4 Right To Opt Out Of Marketing**

Users may object to the use of their personal information for marketing purposes at any time.

## 4. Cross-Border Data Handling

Personal information may be processed or stored outside Japan, including in the European Union. In such cases, we ensure that appropriate safeguards are in place and that personal information is handled in accordance with APPI requirements.

## 5. Contact And Inquiries

For questions, requests, or complaints regarding personal information or this policy, please contact:

Email: <legal@onside.io>

Mailing Address:

Onside.io B.V.

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

We will respond without undue delay and in accordance with applicable Japanese law.

#### 6. Changes To This Policy

We may update this Data Collection Policy from time to time to reflect changes in legal requirements or marketplace operations. Any material changes will be communicated through the marketplace or via email where required.

#### Acknowledgment

By using Onside, you acknowledge that you have read, understood, and agree to this Data Collection Policy.


# App Marketplace Guidelines

Rules for distributing apps through Onside in Japan, including legal compliance, intellectual property, fraud prevention, quality standards, and the review and appeals process.

## Commitment To Safe And Secure Apps

At Onside, we are committed to providing a safe, secure, and trustworthy environment for users in Japan. As part of this commitment, we continuously monitor and review apps distributed through our marketplace to detect and prevent fraudulent, malicious, or illegal behavior.

These App Marketplace Guidelines define the requirements that all apps and developers must follow in order to distribute apps through the Onside marketplace.

Onside is operated by Onside.io B.V.

***

## 1. Compliance With Laws And Regulations

### 1.1 Legal Compliance

All apps must comply with applicable laws and regulations in Japan and in any other region where the app is made available. This includes, but is not limited to, laws related to:

* Data protection and privacy
* Consumer protection
* Intellectual property
* Content regulation

### 1.2 Prohibited Content

Apps must not contain, promote, or facilitate illegal or harmful activities. This includes, but is not limited to:

* Distribution of illegal substances
* Fraud, scams, or deceptive practices
* Harassment, abuse, or exploitation
* Human trafficking or sexual exploitation
* Unauthorized file sharing or copyright infringement

## 2. Intellectual Property Protection

### 2.1 Respect For Intellectual Property

Apps must respect the intellectual property rights of third parties. Developers must not use unauthorized trademarks, copyrighted materials, patents, or proprietary technology.

### 2.2 Intellectual Property Disputes

Developers must respond promptly to any intellectual property claims or inquiries. Failure to address such claims may result in app suspension or removal from the marketplace.

## 3. Fraudulent And Malicious Behavior

### 3.1 Prohibition Of Fraud

Apps must not engage in fraudulent or deceptive behavior, including:

* Misleading app descriptions or functionality
* Manipulating user reviews or ratings
* Impersonation or false representation

### 3.2 Malware And Harmful Code

Apps must not contain malware or harmful components, including viruses, trojans, worms, spyware, or any code intended to disrupt systems or compromise user security.

### 3.3 User Data Protection

Apps must handle user data responsibly and transparently. User data must not be collected, processed, or shared without proper user consent and disclosure.

## 4. Monitoring And Enforcement

### 4.1 App Reviews And Audits

Onside performs ongoing monitoring of apps using a combination of automated tools and manual reviews to identify violations of these guidelines.

### 4.2 User Reporting

Users are provided with mechanisms to report suspicious apps or behavior. All reports are reviewed, and appropriate action is taken where necessary.

### 4.3 Developer Accountability

Developers are responsible for maintaining compliance throughout the lifecycle of their apps. Apps found to be in violation may be suspended or removed, and developer accounts may be restricted or terminated.

## 5. Quality And Accuracy Standards

### 5.1 Functionality

Apps must function as described and must not be intentionally incomplete or unstable. Apps with excessive crashes, critical bugs, or non-functional features may be rejected or removed.

### 5.2 User Experience

Apps should provide a clear, usable, and reliable user experience, with interfaces appropriate for their intended audience.

### 5.3 Accuracy Of Information

All app metadata, including descriptions, screenshots, pricing, and promotional content, must be accurate and not misleading.

## 6. Review And Appeals Process

### 6.1 App Review

All apps distributed through the Onside marketplace undergo a review process to assess compliance with these guidelines and applicable legal requirements.

### 6.2 Appeals

Developers may appeal decisions related to app rejection or removal. Appeals must be submitted through designated support channels and will be reviewed by Onside.

## 7. Contact Information

For questions or concerns regarding these App Marketplace Guidelines, please contact:

Email: <legal@onside.io>

Mailing Address:

Onside.io

Keizersgracht 555

1017 DR Amsterdam

The Netherlands


# Intellectual Property Review Mechanism For App Distribution

How Onside reviews apps for intellectual property compliance in Japan before distribution, including submission requirements, manual screening, developer remediation, and ongoing enforcement.

## Commitment To Intellectual Property Protection

At Onside, we are committed to protecting the intellectual property rights of Apple, app developers, rights holders, and other stakeholders. To support this commitment, we operate a structured manual intellectual property review process to evaluate apps for potential infringement prior to distribution through the Onside marketplace.

This mechanism is designed to prevent the distribution of apps that infringe copyrights, trademarks, patents, or other proprietary rights.

Onside is operated by Onside.io B.V.

## Manual Pre-Distribution Intellectual Property Review Process

Before an app is made available on the marketplace, it undergoes a manual review focused on intellectual property compliance. The process includes the following steps:

## 1. Submission Requirements

Developers are required to submit comprehensive information as part of the app submission process, including:

* App description and functional overview
* App name, icons, screenshots, and promotional materials
* Declarations regarding ownership or lawful use of all content
* Supporting intellectual property documentation where applicable (e.g., licenses, trademarks, copyrights, patents)

Developers must also explicitly agree to Onside’s terms and conditions, confirming that their app does not infringe the intellectual property rights of any third party.

## 2. Initial Intellectual Property Screening

Upon submission, Onside conducts an initial manual screening to identify obvious or high-risk intellectual property concerns. This screening includes:

* Review of app name, branding, and visual assets
* Detection of potentially unauthorized use of trademarks or copyrighted material
* Identification of impersonation or misleading representation of third-party brands or services

Apps with unresolved high-risk indicators may be rejected at this stage.

## 3. Detailed Intellectual Property Analysis

Apps that pass the initial screening may undergo a deeper manual analysis conducted by reviewers trained in intellectual property compliance. This analysis may include:

* Examination of app content, features, and user flows
* Comparison against known public trademark and copyright references
* Evaluation of originality claims provided by the developer
* Review of submitted licenses or permissions

Where appropriate, publicly available trademark, copyright, or patent databases may be consulted to assess potential conflicts.

## 4. Developer Communication And Remediation

If potential intellectual property issues are identified:

* The developer is notified of the specific concerns
* The developer may be asked to provide additional documentation, clarification, or licenses
* The developer may be required to modify app content, branding, or functionality

Apps remain unpublished until identified issues are adequately resolved.

## **5. Approval Or Rejection**

* Apps that successfully resolve all identified intellectual property concerns may be approved for distribution
* Apps that fail to address intellectual property issues, or where infringement risk remains, are rejected
* Developers are informed of rejection decisions and the underlying reasons

## Ongoing Monitoring And Enforcement

Intellectual property compliance is continuously enforced after publication. Onside maintains the following ongoing controls:

* Periodic Audits: Manual and automated reviews of published apps to identify new or emerging intellectual property risks
* Rights Holder And User Reporting: A reporting mechanism allowing rights holders, developers, and users to report suspected infringements
* Timely Enforcement Actions: Prompt investigation and, where necessary, suspension or removal of infringing apps

Developers are expected to maintain ongoing compliance throughout the lifecycle of their apps.

## Contact Information For Intellectual Property Inquiries

For questions, notices, or complaints related to intellectual property matters, please contact:

Email: [support@onside.io](mailto:legal@onside.io)

Mailing Address:

Onside.io

Keizersgracht 555

1017 DR Amsterdam

The Netherlands


# Intellectual Property Dispute Resolution Policy

How Onside handles intellectual property disputes in Japan, including notice requirements, review steps, developer responses, content removal, and repeat infringement enforcement.

## Mechanism For Notification Of Intellectual Property Disputes

At Onside, we are committed to respecting and protecting intellectual property rights. We provide a clear and accessible mechanism for developers, users, Apple, rights holders, and other parties to notify us of intellectual property disputes related to the Onside marketplace or iOS apps distributed through it.

Onside is operated by Onside.io B.V.

Notifications of alleged intellectual property infringement may be submitted through the following channels:

Email: <legal@onside.io>

Mailing Address:

Onside.io

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

## Required Information For Intellectual Property Notifications

To allow us to process intellectual property complaints efficiently, all notifications must include the following information:

1. Identification of the intellectual property right claimed to have been infringed (e.g., copyright, trademark, patent).
2. Identification of the material alleged to be infringing, with information reasonably sufficient to locate it within the marketplace.
3. Contact information of the complaining party, including name, address, telephone number, and email address.
4. A statement that the complaining party has a good faith belief that the use of the material is not authorized by the intellectual property owner, its agent, or applicable law.
5. A statement that the information provided in the notification is accurate and that the complaining party is authorized to act on behalf of the intellectual property rights holder.

Notifications that do not include the required information may be delayed or rejected.

## Handling Of Intellectual Property Disputes

Upon receipt of a complete intellectual property notification, Onside will take the following steps:

1. Acknowledge receipt of the notification without undue delay.
2. Review the claim to assess its validity and scope.
3. Notify the developer or party responsible for the allegedly infringing content and provide an opportunity to respond or provide clarification.
4. Take appropriate action based on the outcome of the review, including removal or disabling access to the content where infringement is reasonably determined.

## Removal Or Disabling Of Infringing Content

If content distributed through the marketplace is determined to infringe intellectual property rights, Onside will take prompt action, which may include:

* Temporarily disabling access to the content while the dispute is under review
* Permanently removing infringing content from the marketplace
* Suspending updates or distribution of the affected app

Actions are taken proportionately based on the nature and severity of the infringement.

## Counter-Notifications And Developer Response

Developers may respond to intellectual property claims by submitting clarification, evidence of authorization, or other relevant documentation. Where appropriate, Onside may reinstate content if the dispute is resolved or if infringement is not established.

## Repeat Infringers

Onside maintains a policy to address repeated intellectual property violations. If a developer is found to repeatedly distribute infringing content, Onside may take escalating enforcement actions, including:

1. Issuing formal warnings and compliance notices
2. Increased monitoring of submissions
3. Suspension or termination of the developer’s account
4. Permanent removal from the marketplace in cases of continued non-compliance

## Contact Information

For questions, complaints, or notices related to this Intellectual Property Dispute Resolution Policy, please contact:

Email: <legal@onside.io>

Mailing Address:

Onside.io

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

## Acknowledgment

By using the Onside marketplace, developers and users acknowledge that they have read, understood, and agree to comply with this Intellectual Property Dispute Resolution Policy.


# Governmental And Legal Compliance Policy For App Takedown Requests

How Onside handles governmental, legal, intellectual property, and policy-based app takedown requests in Japan, including review steps, temporary restrictions, developer notices, and contact details.

## Handling Governmental And Other App Takedown Requests

At Onside, we are committed to ensuring that all apps distributed through our marketplace comply with applicable laws, respect intellectual property rights, and adhere to our marketplace terms and policies. To support this commitment, we maintain a clear and structured process for handling governmental, legal, and other valid requests to remove or restrict access to apps.

Onside is operated by Onside.io B.V.

***

## Takedown Requests Based On Illegality Or Legal Requirements

Onside promptly reviews and acts on requests from governmental authorities or courts asserting that an app is illegal or otherwise prohibited under applicable law.

Upon receiving such a request, we will:

1. Acknowledge receipt of the request without undue delay
2. Review the request to confirm its authenticity, scope, and legal basis
3. Temporarily disable access to the app while the review is conducted, where appropriate
4. Permanently remove the app from the marketplace if it is determined to violate applicable laws
5. Notify the app developer of the request and the resulting action, unless prohibited by law

Where required, Onside cooperates with lawful governmental investigations and enforcement actions.

## Takedown Requests Based On Intellectual Property Violations

Onside takes intellectual property rights seriously and handles requests related to alleged infringement in accordance with its Intellectual Property Review and Dispute Resolution policies.

Upon receiving a valid intellectual property takedown request, we will:

1. Acknowledge receipt of the request
2. Review the claim to assess its validity and legal basis
3. Notify the app developer of the alleged infringement and allow an opportunity to respond
4. Temporarily disable access to the app during the review process, where appropriate
5. Permanently remove the app if infringement is reasonably determined
6. Notify the requesting party and the developer of the final decision

## Takedown Requests Based On Violation Of Marketplace Terms Or Policies

Onside maintains marketplace standards designed to protect users, developers, and platform integrity. Requests or internal findings indicating that an app violates marketplace terms or policies are handled as follows:

1. Acknowledge receipt of the request or initiate internal review
2. Assess the app against applicable marketplace terms and guidelines
3. Notify the app developer of the potential violation and provide an opportunity to respond
4. Temporarily disable access to the app during the review, if necessary
5. Permanently remove the app if a violation is confirmed
6. Notify the relevant parties of the outcome

## Temporary Measures And Proportionality

Onside applies enforcement actions proportionately, taking into account:

* The severity of the alleged violation
* Legal or regulatory urgency
* Risk to users, rights holders, or public interest

Temporary suspension may be used while investigations are ongoing.

## Required Information For Takedown Requests

All governmental and other takedown requests must include:

1. Identification of the app or content in question
2. The legal basis, regulation, court order, or specific policy allegedly violated
3. Contact details of the requesting authority or party (name, address, email, phone number)
4. A statement that the request is made in good faith and that the information provided is accurate

Incomplete requests may be delayed or rejected.

## Contact Information For Takedown Requests

All governmental and legal app takedown requests should be directed to:

Email: <legal@onside.io>

Mailing Address:

Onside.io

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

## Acknowledgment

By distributing apps through the Onside marketplace, developers acknowledge and agree that their apps may be subject to removal or restriction in accordance with this Governmental And Legal Compliance Policy.


# Data Collection Policy

How Onside collects, uses, protects, and processes personal information in Brazil, including user rights under LGPD and information about international data transfers and privacy requests.

### Commitment to User Privacy

At Onside, we are committed to protecting your privacy and ensuring transparency in how your personal information is collected, used, and managed. This policy explains our data collection and processing practices in accordance with the Brazilian General Data Protection Law (Lei Geral de Proteção de Dados – LGPD) and other applicable laws and regulations.

Onside is operated by Onside.io B.V.

### 1. Collection And Use Of Personal Information

We collect and use personal information only to the extent necessary to operate the marketplace and provide related services.

#### 1.1 User Authorization And Marketplace Access

To provide access to the Onside app marketplace, we collect the following personal information during account creation and authentication:

* Full name – to identify the user and manage the account
* Date of birth – to verify age eligibility and comply with applicable legal requirements
* Phone number – to identify and authenticate users
* Email address – to communicate with users regarding their account, security, and marketplace activity

Purpose Of Processing:

* User identification and authentication
* Account creation and management
* Age verification and compliance with legal requirements
* Security, fraud prevention, and customer support

Legal Basis:

Personal information is processed in accordance with one or more legal bases provided under LGPD, including:

* Performance of a contract or procedures related to a contract
* Compliance with legal or regulatory obligations
* Legitimate interests of Onside, provided that such interests do not override the rights and freedoms of users
* User consent, where required by applicable law

#### 1.2 Marketing And Marketplace Communications

With your consent, where required, we may use personal information to send information related to the marketplace, including product updates, service announcements, and promotional materials.

For this purpose, we may collect and use:

* Full name – to personalize communications
* Email address – to deliver communications
* Usage data – to understand user preferences and improve the relevance of marketplace content

Legal Basis:

* User consent, where required by LGPD
* Legitimate interests related to improving and promoting marketplace services, where permitted by law

Users may opt out of marketing communications at any time.

### 2. Management And Protection Of Personal Information

We implement appropriate technical and organizational measures to prevent unauthorized access, loss, alteration, disclosure, or misuse of personal information. Access to personal data is limited to authorized personnel and trusted service providers who require such access for operational purposes.

We regularly review our security measures and implement safeguards appropriate to the nature of the personal information processed.

### 3. User Rights Under LGPD

In accordance with LGPD, users have the following rights regarding their personal information:

#### 3.1 Right To Confirmation And Access

Users may request confirmation of whether we process their personal information and obtain access to such information.

#### 3.2 Right To Correction

Users may request correction of incomplete, inaccurate, or outdated personal information.

#### 3.3 Right To Deletion, Anonymization, Or Blocking

Users may request deletion, anonymization, or blocking of personal information processed in violation of applicable law or no longer necessary for the stated purpose.

#### 3.4 Right To Data Portability

Users may request portability of their personal information to another service provider, where technically feasible and as permitted by applicable law.

#### 3.5 Right To Information About Data Sharing

Users may request information regarding the public and private entities with which their personal information has been shared.

#### 3.6 Right To Withdraw Consent

Where processing is based on consent, users may withdraw their consent at any time. Withdrawal of consent does not affect the lawfulness of processing carried out before withdrawal.

#### 3.7 Right To Opt Out Of Marketing

Users may object to the use of their personal information for marketing purposes at any time.

### 4. International Transfers Of Personal Information

Personal information may be processed or stored outside Brazil, including in the European Union and other jurisdictions where Onside or its service providers operate.

Where personal information is transferred internationally, we implement appropriate safeguards and ensure that such transfers are conducted in accordance with LGPD and applicable data protection requirements.

### 5. Contact And Inquiries

For questions, requests, or complaints regarding personal information, data processing activities, or this policy, please contact us:

Data Controller: Onside.io B.V.

Email: <legal@onside.io>

Mailing Address:

Onside.io B.V.\
Keizersgracht 555\
1017 DR Amsterdam\
The Netherlands

We will respond to requests without undue delay and in accordance with applicable law.

Users may contact us to exercise their rights under LGPD, including requests for access, correction, deletion, portability, or information regarding the processing of personal data.

### 6. Changes To This Policy

We may update this Data Collection Policy from time to time to reflect changes in legal requirements, marketplace operations, or our data processing practices. Any material changes will be communicated through the marketplace or via email where required by applicable law.

### Acknowledgment

By using Onside, you acknowledge that you have read, understood, and agree to this Data Collection Policy.


# App Marketplace Guidelines

Rules for distributing apps through Onside in Brazil, including legal compliance, safety requirements, quality standards, and app review procedures.

### Commitment To Safe And Secure Apps

At Onside, we are committed to providing a safe, secure, and trustworthy environment for users. As part of this commitment, we continuously monitor and review apps distributed through our marketplace to detect and prevent fraudulent, malicious, or illegal behavior.

These App Marketplace Guidelines define the requirements that all apps and developers must follow in order to distribute apps through the Onside marketplace.

This policy applies to the Onside marketplace application (bundle identifier: com.onside.marketplace-app) distributed to users in Brazil.

Onside is operated by Onside.io B.V.

***

### 1. Compliance With Laws And Regulations

#### 1.1 Legal Compliance

All apps must comply with applicable laws and regulations in Brazil and in any other region where the app is made available. This includes, but is not limited to, laws related to:

* Data protection and privacy
* Consumer protection
* Intellectual property
* Content regulation
* Electronic commerce and digital services

#### 1.2 Prohibited Content

Apps must not contain, promote, or facilitate illegal or harmful activities. This includes, but is not limited to:

* Distribution of illegal substances
* Fraud, scams, or deceptive practices
* Harassment, abuse, or exploitation
* Human trafficking or sexual exploitation
* Unauthorized file sharing or copyright infringement
* Distribution of malware or malicious software

### 2. Intellectual Property Protection

#### 2.1 Respect For Intellectual Property

Apps must respect the intellectual property rights of third parties. Developers must not use unauthorized trademarks, copyrighted materials, patents, trade secrets, or proprietary technology.

#### 2.2 Intellectual Property Disputes

Developers must respond promptly to any intellectual property claims or inquiries. Failure to address such claims may result in app suspension or removal from the marketplace.

### 3. Fraudulent And Malicious Behavior

#### 3.1 Prohibition Of Fraud

Apps must not engage in fraudulent or deceptive behavior, including:

* Misleading app descriptions or functionality
* Manipulating user reviews or ratings
* Impersonation or false representation
* Concealing material information from users

#### 3.2 Malware And Harmful Code

Apps must not contain malware or harmful components, including viruses, trojans, worms, spyware, ransomware, or any code intended to disrupt systems, compromise user security, or gain unauthorized access to devices or data.

#### 3.3 User Data Protection

Apps must handle user data responsibly and transparently. User data must not be collected, processed, or shared without proper user consent, legal basis where required, and clear disclosure to users.

### 4. Monitoring And Enforcement

#### 4.1 App Reviews And Audits

Onside performs ongoing monitoring of apps using a combination of automated tools and manual reviews to identify violations of these guidelines.

#### 4.2 User Reporting

Users are provided with mechanisms to report suspicious apps or behavior. All reports are reviewed, and appropriate action is taken where necessary.

#### 4.3 Developer Accountability

Developers are responsible for maintaining compliance throughout the lifecycle of their apps. Apps found to be in violation may be suspended or removed, and developer accounts may be restricted or terminated.

### 5. Quality And Accuracy Standards

#### 5.1 Functionality

Apps must function as described and must not be intentionally incomplete or unstable. Apps with excessive crashes, critical bugs, security vulnerabilities, or non-functional features may be rejected or removed.

#### 5.2 User Experience

Apps should provide a clear, usable, and reliable user experience, with interfaces appropriate for their intended audience.

#### 5.3 Accuracy Of Information

All app metadata, including descriptions, screenshots, pricing, subscription terms, and promotional content, must be accurate and not misleading.

### 6. Review And Appeals Process

#### 6.1 App Review

All apps distributed through the Onside marketplace undergo a review process to assess compliance with these guidelines and applicable legal requirements.

#### 6.2 Appeals

Developers may appeal decisions related to app rejection, suspension, or removal. Appeals must be submitted through designated support channels and will be reviewed by Onside.

### 7. Contact Information

For questions or concerns regarding these App Marketplace Guidelines, please contact:

Email: <legal@onside.io>

Mailing Address:

Onside.io B.V.

Keizersgracht 555

1017 DR Amsterdam

The Netherlands


# Intellectual Property Review Mechanism For App Distribution

How Onside reviews apps for intellectual property compliance before and after distribution in Brazil.

### Commitment To Intellectual Property Protection

At Onside, we are committed to protecting the intellectual property rights of Apple, app developers, rights holders, and other stakeholders. To support this commitment, we operate a structured manual intellectual property review process to evaluate apps for potential infringement prior to distribution through the Onside marketplace.

This mechanism is designed to prevent the distribution of apps that infringe copyrights, trademarks, patents, trade secrets, or other proprietary rights.

This policy applies to the Onside marketplace application (bundle identifier: com.onside.marketplace-app) distributed to users in Brazil.

Onside is operated by Onside.io B.V.

### Manual Pre-Distribution Intellectual Property Review Process

Before an app is made available on the marketplace, it undergoes a manual review focused on intellectual property compliance. The process includes the following steps:

### 1. Submission Requirements

Developers are required to submit comprehensive information as part of the app submission process, including:

* App description and functional overview
* App name, icons, screenshots, and promotional materials
* Declarations regarding ownership or lawful use of all content
* Supporting intellectual property documentation where applicable (e.g., licenses, trademarks, copyrights, patents)

Developers must also explicitly agree to Onside’s terms and conditions, confirming that their app does not infringe the intellectual property rights of any third party.

### 2. Initial Intellectual Property Screening

Upon submission, Onside conducts an initial manual screening to identify obvious or high-risk intellectual property concerns. This screening includes:

* Review of app name, branding, and visual assets
* Detection of potentially unauthorized use of trademarks or copyrighted material
* Identification of impersonation or misleading representation of third-party brands, products, or services

Apps with unresolved high-risk indicators may be rejected at this stage.

### 3. Detailed Intellectual Property Analysis

Apps that pass the initial screening may undergo a deeper manual analysis conducted by reviewers trained in intellectual property compliance. This analysis may include:

* Examination of app content, features, and user flows
* Comparison against known public trademark and copyright references
* Evaluation of originality claims provided by the developer
* Review of submitted licenses or permissions

Where appropriate, publicly available trademark, copyright, or patent databases may be consulted to assess potential conflicts.

### 4. Developer Communication And Remediation

If potential intellectual property issues are identified:

* The developer is notified of the specific concerns
* The developer may be asked to provide additional documentation, clarification, or licenses
* The developer may be required to modify app content, branding, or functionality

Apps remain unpublished until identified issues are adequately resolved.

### 5. Approval Or Rejection

* Apps that successfully resolve all identified intellectual property concerns may be approved for distribution
* Apps that fail to address intellectual property issues, or where infringement risk remains, are rejected
* Developers are informed of rejection decisions and the underlying reasons

### Ongoing Monitoring And Enforcement

Intellectual property compliance is continuously enforced after publication. Onside maintains the following ongoing controls:

#### Periodic Audits

Manual and automated reviews of published apps to identify new or emerging intellectual property risks.

#### Rights Holder And User Reporting

A reporting mechanism allowing rights holders, developers, and users to report suspected intellectual property infringements.

#### Investigation And Enforcement

Onside reviews reports of alleged infringement and may request additional information from the reporting party and the developer. Where appropriate, apps may be suspended, restricted, or removed while an investigation is conducted.

#### Timely Enforcement Actions

Prompt investigation and, where necessary, suspension or removal of apps that violate intellectual property rights or applicable law.

Developers are expected to maintain ongoing compliance throughout the lifecycle of their apps.

### Contact Information For Intellectual Property Inquiries

For questions, notices, or complaints related to intellectual property matters, please contact:

Email: [support@onside.io](mailto:legal@onside.io)

Mailing Address:

Onside.io B.V.

Keizersgracht 555

1017 DR Amsterdam

The Netherlands


# Intellectual Property Dispute Resolution Policy

Process for reporting, reviewing, and resolving intellectual property disputes related to apps distributed through Onside in Brazil.

### Mechanism For Notification Of Intellectual Property Disputes

At Onside, we are committed to respecting and protecting intellectual property rights. We provide a clear and accessible mechanism for developers, users, Apple, rights holders, and other parties to notify us of intellectual property disputes related to the Onside marketplace or iOS apps distributed through it.

This policy applies to the Onside marketplace application (bundle identifier: com.onside.marketplace-app) distributed to users in Brazil.

Onside is operated by Onside.io B.V.

Notifications of alleged intellectual property infringement may be submitted through the following channels:

Email: <legal@onside.io>

Mailing Address:

Onside.io B.V.

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

### Required Information For Intellectual Property Notifications

To allow us to process intellectual property complaints efficiently, all notifications should include the following information:

1. Identification of the intellectual property right claimed to have been infringed (e.g., copyright, trademark, patent).
2. Identification of the material alleged to be infringing, with information reasonably sufficient to locate it within the marketplace.
3. Contact information of the complaining party, including name, address, telephone number, and email address.
4. A statement that the complaining party has a good faith belief that the use of the material is not authorized by the intellectual property owner, its agent, or applicable law.
5. A statement that the information provided in the notification is accurate and that the complaining party is authorized to act on behalf of the intellectual property rights holder.

Notifications that do not include sufficient information may be delayed or rejected.

### Handling Of Intellectual Property Disputes

Upon receipt of a complete intellectual property notification, Onside will take the following steps:

1. Acknowledge receipt of the notification without undue delay.
2. Review the claim to assess its validity and scope.
3. Notify the developer or party responsible for the allegedly infringing content and provide an opportunity to respond or provide clarification.
4. Take appropriate action based on the outcome of the review, including removal or disabling access to the content where infringement is reasonably determined.

### Removal Or Disabling Of Infringing Content

If content distributed through the marketplace is determined to infringe intellectual property rights, Onside will take prompt action, which may include:

* Temporarily disabling access to the content while the dispute is under review
* Permanently removing infringing content from the marketplace
* Suspending updates or distribution of the affected app

Actions are taken proportionately based on the nature and severity of the infringement.

### Counter-Notifications And Developer Response

Developers may respond to intellectual property claims by submitting clarification, evidence of authorization, ownership documentation, licenses, or other relevant materials.

Where appropriate, Onside may reinstate content if the dispute is resolved or if infringement is not established.

### Repeat Infringers

Onside maintains a policy to address repeated intellectual property violations. If a developer is found to repeatedly distribute infringing content, Onside may take escalating enforcement actions, including:

1. Issuing formal warnings and compliance notices
2. Increased monitoring of future submissions
3. Suspension or termination of the developer account
4. Permanent removal from the marketplace in cases of continued non-compliance

### Contact Information

For questions, complaints, or notices related to this Intellectual Property Dispute Resolution Policy, please contact:

Email: <legal@onside.io>

Mailing Address:

Onside.io B.V.

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

### Acknowledgment

By using the Onside marketplace, developers and users acknowledge that they have read, understood, and agree to comply with this Intellectual Property Dispute Resolution Policy.


# Governmental And Legal Compliance Policy For App Takedown Requests

Process for handling governmental, legal, intellectual property, and policy-based app takedown requests on Onside in Brazil.

### Handling Governmental And Other App Takedown Requests

At Onside, we are committed to ensuring that all apps distributed through our marketplace comply with applicable laws, respect intellectual property rights, and adhere to our marketplace terms and policies. To support this commitment, we maintain a clear and structured process for handling governmental, legal, and other valid requests to remove or restrict access to apps.

This policy applies to the Onside marketplace application (bundle identifier: com.onside.marketplace-app) distributed to users in Brazil.

Onside is operated by Onside.io B.V.

***

### Takedown Requests Based On Illegality Or Legal Requirements

Onside promptly reviews and acts on requests from governmental authorities, regulatory bodies, law enforcement agencies, or courts asserting that an app is illegal or otherwise prohibited under applicable law.

Upon receiving such a request, we will:

1. Acknowledge receipt of the request without undue delay.
2. Review the request to confirm its authenticity, scope, and legal basis.
3. Temporarily disable access to the app while the review is conducted, where appropriate.
4. Permanently remove the app from the marketplace if it is determined to violate applicable laws.
5. Notify the app developer of the request and the resulting action, unless prohibited by law.

Where required, Onside cooperates with lawful governmental investigations and enforcement actions.

### Takedown Requests Based On Intellectual Property Violations

Onside takes intellectual property rights seriously and handles requests related to alleged infringement in accordance with its Intellectual Property Review Mechanism and Intellectual Property Dispute Resolution Policy.

Upon receiving a valid intellectual property takedown request, we will:

1. Acknowledge receipt of the request.
2. Review the claim to assess its validity and legal basis.
3. Notify the app developer of the alleged infringement and allow an opportunity to respond.
4. Temporarily disable access to the app during the review process, where appropriate.
5. Permanently remove the app if infringement is reasonably determined.
6. Notify the requesting party and the developer of the final decision.

### Takedown Requests Based On Violation Of Marketplace Terms Or Policies

Onside maintains marketplace standards designed to protect users, developers, and platform integrity. Requests or internal findings indicating that an app violates marketplace terms or policies are handled as follows:

1. Acknowledge receipt of the request or initiate internal review.
2. Assess the app against applicable marketplace terms, policies, and guidelines.
3. Notify the app developer of the potential violation and provide an opportunity to respond.
4. Temporarily disable access to the app during the review, if necessary.
5. Permanently remove the app if a violation is confirmed.
6. Notify the relevant parties of the outcome.

### Temporary Measures And Proportionality

Onside applies enforcement actions proportionately, taking into account:

* The severity of the alleged violation
* Legal or regulatory urgency
* Risk to users, rights holders, or the public interest

Temporary suspension may be used while investigations are ongoing.

### Required Information For Takedown Requests

All governmental and other takedown requests should include:

1. Identification of the app or content in question.
2. The legal basis, regulation, court order, or specific policy allegedly violated.
3. Contact details of the requesting authority or party, including name, address, email address, and telephone number.
4. A statement that the request is made in good faith and that the information provided is accurate.

Incomplete requests may be delayed or rejected.

### Contact Information For Takedown Requests

All governmental and legal app takedown requests should be directed to:

Email: <legal@onside.io>

Mailing Address:

Onside.io B.V.

Keizersgracht 555

1017 DR Amsterdam

The Netherlands

### Acknowledgment

By distributing apps through the Onside marketplace, developers acknowledge and agree that their apps may be subject to removal, suspension, or restriction in accordance with this Governmental And Legal Compliance Policy.


# Welcome to Onside Console

An introduction to the Onside Developer Console, outlining its main features and guiding you to your first steps on the platform.

Welcome, developer! The Onside Console is your central hub for publishing, managing, and growing your iOS apps on our alternative marketplace.

This documentation will guide you through everything you need to know, from setting up your account to analyzing your app's performance.

### Jump Right In

Ready to get started? Here are the most important first steps:

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Set Up Your Account</strong></td><td>Complete your profile and company verification to get started.</td><td><a href="/pages/JF6tmPPiI5C3dBHBlyRr">/pages/JF6tmPPiI5C3dBHBlyRr</a></td></tr><tr><td><strong>Publish Your First App</strong></td><td>Follow our step-by-step guide to get your native iOS app live on Onside.</td><td><a href="/pages/O6JSj1zf3R8uOpcon7Y7">/pages/O6JSj1zf3R8uOpcon7Y7</a></td></tr><tr><td><strong>Explore Monetization</strong></td><td>Learn about our 10% payment commission and how to set up paid apps, IAPs, and subscriptions.</td><td><a href="/pages/1vwqWfNNiHcZCOrOAqqi">/pages/1vwqWfNNiHcZCOrOAqqi</a></td></tr><tr><td><strong>Turn Your PWA into an App</strong></td><td>Have a web app? We can wrap it into a native iOS app for you, no extra coding required.</td><td><a href="/pages/CVZ032MODgVBlWV0TRjT">/pages/CVZ032MODgVBlWV0TRjT</a></td></tr></tbody></table>

***

### The Developer Journey on Onside

Your lifecycle as an Onside developer generally follows these four key stages. Use our documentation to master each one.

{% stepper %}
{% step %}

#### Publish & Manage

The first step is getting your app onto the Onside Store. Our guides cover the entire process, from connecting your App Store account for seamless build syncing to managing updates and new versions after launch.
{% endstep %}

{% step %}

#### Monetize

Once your app is live, you can start earning. We offer flexible monetization models, including paid apps, in-app purchases, and subscriptions, all supported by our simple-to-integrate SDK.
{% endstep %}

{% step %}

#### Grow & Analyze

Drive your app's success by leveraging our platform's growth tools. Get featured, apply ASO tips tailored for Onside, and dive deep into your performance with our comprehensive analytics dashboard.
{% endstep %}

{% step %}

#### Administer & Collaborate

Manage the business side of your account. Set up your payout and financial details, and (soon) invite team members and manage their access with specific roles and permissions
{% endstep %}
{% endstepper %}

***

### Key Platform Features

Onside is built with developers in mind. Here are some of the core features that set our platform apart.

{% tabs %}
{% tab title="Fair Monetization" %}
We believe you should keep more of what you earn.

* **Clear Fees:** Payments processed through Onside are subject to a **10% commission**. No separate payout fee applies.
* **Flexible Models:** Full support for Paid Apps, In-App Purchases, and Subscriptions.

> This developer-first approach allows you to maximize your revenue while we grow and support the Onside ecosystem.
> {% endtab %}

{% tab title="Publishing Freedom" %}
We provide a more flexible path to publishing your app.

* **Fast Notarization:** For Onside-exclusive apps, you can bypass lengthy reviews and go live quickly through Apple's streamlined Notarization process.
* **Broader Content Guidelines:** We support a wider range of app categories, including those for mature audiences, Crypto, and Gambling, provided they are fully compliant with all applicable EU laws and regulations.
  {% endtab %}

{% tab title="Powerful Tooling" %}
Our console is equipped with tools to make your life easier.

* **Seamless App Store Sync:** Connect your App Store account to automatically pull your app builds and sync metadata, saving you time.
* **PWA Wrapper:** Easily convert your Progressive Web App into a full-featured Onside Store application with minimal effort.
* **Advanced Analytics:** Get the insights you need to make data-driven decisions about your app's performance.
  {% endtab %}
  {% endtabs %}

***

{% hint style="info" %}
**Can't find what you need?** Our developer support team is always ready to help. Don't hesitate to reach out to us at [**support@onside.io**](mailto:support@onside.io).
{% endhint %}


# Create Onside Account

A step-by-step guide to creating your Onside developer account, choosing your account type, and connecting it to the App Store for seamless app management.

Welcome to Onside! Setting up your developer account is the first step toward publishing your apps on our marketplace. This guide will walk you through the entire process, from initial registration to publishing your first app.

{% hint style="info" %}
If you already have an Onside developer account, you can go directly to the [Log In page](https://console.onside.io/login). For our legal terms, please see [Onside Legal Documents](/console/account-and-platform/onside-legal-documents).
{% endhint %}

{% stepper %}
{% step %}

#### Register Your Onside Account

First, go to the [**Onside Developer Console**](https://console.onside.io/signup) and create account. You will need to choose between a Business or Individual account type.

<div align="left"><figure><img src="/files/pwuOWTjZMioQiiAjqj1I" alt="" width="563"><figcaption></figcaption></figure></div>

{% tabs %}
{% tab title="Business Account" %}
Choose this option if you are registering as a legally recognized business entity (e.g., a corporation, partnership, LLC).

**You will need to provide:**

* Legal Company Name
* D-U-N-S Number

<details>

<summary><strong>What is a D-U-N-S Number and why is it required for businesses?</strong></summary>

A D-U-N-S Number is a unique nine-digit identifier for businesses, provided by Dun & Bradstreet (D\&B) and used globally as a standard business identifier.

Like many platforms, including Apple, we require it to verify your organization's identity and legal entity status. This is a crucial step to ensure we maintain a trusted and secure ecosystem of verified business developers on Onside.

**How to Get a D-U-N-S Number:**

1. **Look Up Your Number:** First, use the [D\&B lookup tool ](https://www.dnb.com/duns-number/lookup.html)to check if your company already has a D-U-N-S Number. Many businesses already do.
2. **Request a New Number:** If you don't have one, you can request it. The process is free for Apple developers, and you can start it through their portal.

> For the official lookup tool and detailed instructions, please refer to [**Apple's D-U-N-S Number help page**](https://www.dnb.com/duns-number/lookup.html).

**Important Information on Timing:**

* The process is **free**, but it is **not instant**.
* It may take up to **5 business days** for D\&B to issue your number after you submit your information.
* After your number is issued, it can take up to an additional **2 business days** for the information to be updated and verifiable by partner systems like ours.

Please plan accordingly, as this verification step is required before your business account can be fully approved.

</details>

* Country / Legal Address
* Developer Email & Phone Number

{% hint style="info" %}
Your company name, legal address, developer email, and phone number will be displayed on your Onside Store product page.
{% endhint %}

<div align="left"><figure><img src="/files/l9ZsJVkO3e5sgV4Ipea0" alt="" width="563"><figcaption></figcaption></figure></div>
{% endtab %}

{% tab title="Individual Account" %}
Choose this option if you are a sole proprietor or an individual developer who has not formally incorporated a business.

**You will need to provide:**

* Your Full Legal Name
* Country / Address
* Developer Email & Phone Number

{% hint style="info" %}
Your developer name, country, and developer email will be displayed on your Onside Store product page.
{% endhint %}

<div align="left"><figure><img src="/files/RbULz7puYvndZyv375wT" alt="" width="563"><figcaption></figcaption></figure></div>
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### Verify email and Accept Terms of Use

<details>

<summary>Verify email</summary>

* Check your email inbox for a verification code from Onside. (Check spam/junk if not found).
* Enter this code on the Onside verification page.
* If you didn't receive it, use the "Resend code" option.

<figure><img src="/files/3PmkUfC11BbGpdLbiGOg" alt=""><figcaption></figcaption></figure>

</details>

<details>

<summary>Accept <strong>Free Apps Agreement</strong></summary>

This is the final step to activate your account.

1. **Review the Terms:** You will be presented with the **Free Apps Agreement.** Please read these terms carefully as they govern your use of the console. You can **"Download PDF"** for your records.

<figure><img src="/files/3hGpJtN3XTJiNfQdqKO9" alt=""><figcaption></figcaption></figure>

2. Click the **"Let's start"** button to accept the terms.
3. By doing so, you also acknowledge our general [Terms of Use](https://onside.io/terms-of-use) and [Privacy Policy](https://onside.io/privacy-policy).

</details>
{% endstep %}

{% step %}

#### Connect to the App Store

**Step-by-step guide:** [Link Apple Developer Account to Onside](/console/getting-started-and-publishing/link-apple-developer-account-to-onside)
{% endstep %}

{% step %}
**Publish your App**

**Step-by-step guide:** [How to Publish an App](/console/publishing-your-app/how-to-publish-an-app)
{% endstep %}

{% step %}
**Enjoy Freedom and new Customers!**

{% endstep %}
{% endstepper %}


# Link Apple Developer Account to Onside

A step-by-step guide to connecting your AppStore Connect account to Onside using an API Key and Marketplace Token for seamless app management.

Connecting your Apple Developer account is a crucial step that allows Onside to securely and automatically sync your app builds. This guide will walk you through the two parts of the connection process: generating an **API Key** and a **Marketplace Token**.

{% hint style="warning" %}
To complete this process, you will need **Admin** access to your App Store Connect account.
{% endhint %}

***

### Part 1: Generating and Submitting Your API Key

The API Key allows Onside to communicate with App Store Connect on your behalf to manage app information.

<div align="left"><figure><img src="/files/8G3v6OjXxcPovTJ2ljZr" alt="" width="563"><figcaption></figcaption></figure></div>

{% stepper %}
{% step %}

#### Go to the API Keys Section in App Store Connect

Click this direct link to open the correct page:

<a href="https://appstoreconnect.apple.com/access/integrations/api" class="button secondary" data-icon="link">Go to App Store Connect Keys →</a>

You will land on the **Users and Access > Integrations > App Store Connect API** page.
{% endstep %}

{% step %}

#### Generate a New API Key

Click the **(+)** button to generate a new key. In the pop-up window:

1. **Name:** Give the key a memorable name, like "Onside Marketplace".
2. **Access:** Assign it the **Admin** role.
3. Click **Generate**.
   {% endstep %}

{% step %}

#### Download the .p8 Key File

After generating the key, you will see a new entry in your list of active keys. Click the **Download** button.

{% hint style="danger" %}
You can only download the `.p8` key file **once**. Please save it in a secure location, as you will need to upload it to Onside.
{% endhint %}
{% endstep %}

{% step %}

#### Copy & Paste Credentials into Onside

On the same App Store Connect page, you will find your **Issuer ID** and the **Key ID** for the new key you just created.

1. **Copy** the **Issuer ID**.
2. **Copy** the **Key ID**.
3. Go back to the Onside Console "Link Developer account" page.
4. **Paste** the **Apple Developer ID (Issuer ID)** into the corresponding field.
5. **Upload** the `.p8` file you downloaded.
6. **Paste** the **Key ID** into its field.
7. Click **Submit**.

<details>

<summary><strong>Is it safe to give you an API Key with Admin access? Can you delete my app?</strong></summary>

We understand this concern completely. Security and trust are fundamental to our partnership with developers.

* **Secure, Encrypted Storage:** Once you provide the API key, it is immediately encrypted and stored securely in our system. It is never exposed in plain text.
* **Purpose-Built Operations:** Our systems are designed to use the API key for specific, necessary operations only, such as syncing app builds, viewing metadata, and managing app availability on Onside. We do not have automated or manual processes designed to delete your apps from App Store Connect.
* **Your Control:** You always remain in full control. You can revoke the API key you generated for Onside at any time in your App Store Connect account, which will immediately cut off our access.

We require the Admin or App Manager role because these are the roles Apple has designated with the necessary permissions to manage builds and app metadata through the API. We are committed to using this access responsibly and only for the stated purposes of operating the Onside marketplace.

</details>

<details>

<summary><strong>Do I need a separate API Key for each of my apps?</strong></summary>

No. You only need to generate **one** App Store Connect API Key for Onside. This single key will allow us to sync all the apps you choose to make available on our platform from your developer account.

</details>

<details>

<summary><strong>What happens if my `.p8` API Key file is lost or compromised?</strong></summary>

If you lose the `.p8` file before uploading it to us, or if you suspect it has been compromised, you should immediately go to App Store Connect and **revoke that specific API key**. The key will instantly become invalid. You can then generate a new one and update the credentials in your Onside Console. The old, revoked key will no longer provide any access.

</details>

<details>

<summary><strong>Can I use an existing API Key that I use for other services?</strong></summary>

Yes, you can, provided it has the required **Admin** or **App Manager** permissions. However, for security and better management, we **highly recommend generating a new, dedicated API Key specifically for Onside** and naming it accordingly (e.g., "Onside Marketplace"). This makes it easy to track and manage access, and if you ever need to revoke access for one service, it won't affect others.

</details>
{% endstep %}
{% endstepper %}

### Part 2: Generating and Submitting Your Marketplace Token

The Marketplace Token specifically authorizes Onside to distribute your selected apps.

<div align="left"><figure><img src="/files/fMG10gLjMe2kQscuMgJw" alt="" width="563"><figcaption></figcaption></figure></div>

{% stepper %}
{% step %}

#### Sign the Alternative Terms Addendum

Before you can generate a token, you must accept Apple's "Alternative Terms Addendum for Apps in the EU". You will be prompted to do this in App Store Connect if you haven't already.

<a href="https://developer.apple.com/contact/request/alternative-eu-terms-addendum/" class="button secondary" data-icon="file-signature">Sign the Alternative Terms Addendum →</a>
{% endstep %}

{% step %}

#### Go to the Marketplace Distribution Section

In App Store Connect, navigate to **Users and Access > Alternative Distribution**.
{% endstep %}

{% step %}
**Connect your marketplace token**

* Click the **Add Marketplace** button.
* **Copy the Marketplace Token from the Onside Console** and paste it into the field in App Store Connect.&#x20;
  {% endstep %}

{% step %}

#### Select Apps and Enable Notifications

* **Select the apps** you want to make available for distribution on Onside. Click **Next**.
* When asked about App Notifications, select **"Yes, send notifications to this marketplace."** This is required for Onside to receive important updates about your apps.
* Click **Save**.
  {% endstep %}

{% step %}

#### Submit to Onside

Once the Marketplace is configured in App Store Connect, return to the Onside Console page and click **"It's done"** to finalize the process.
{% endstep %}
{% endstepper %}

***

### Troubleshooting & FAQs

<details>

<summary><strong>Why is this connection necessary?</strong></summary>

Onside does not accept direct app build uploads. Instead, we securely sync your builds directly from your App Store Connect account. This streamlined process means you only need to upload your builds to one place, and we handle the rest. This connection also enables us to auto-sync metadata like descriptions and screenshots, saving you time.

</details>

<details>

<summary><strong>Why do I need both an API Key and a Marketplace Token?</strong></summary>

These two credentials serve different purposes as required by Apple's system:

* The **API Key** grants us general, secure access to your App Store Connect account to perform technical actions like syncing builds and metadata.
* The **Marketplace Token** is a specific, explicit permission you grant within App Store Connect that authorizes Onside to act as an alternative marketplace for distributing your apps in the EU.

Both are necessary to create a secure and fully authorized connection between your developer account and the Onside platform.

</details>

<details>

<summary><strong>I'm seeing an error toast. What does it mean?</strong></summary>

Here are some common errors and how to solve them:

* **"This Apple Developer ID is already linked..." or "...already in use"**: This means the Apple Developer account has already been connected to another Onside account. An Apple Developer account can only be linked to one Onside account at a time.
* **"The API Key or Key ID you provided is incorrect"**: Double-check that you have copied and pasted the **Issuer ID** and **Key ID** correctly, without any extra spaces.
* **"Make sure the API Key has at least the App Manager role"**: When you generated the key in App Store Connect (Step 2 of Part 1), you may have selected a role with insufficient permissions. Please generate a new key with either the **App Manager** or **Admin** role.

</details>

<details>

<summary><strong>I don't see the "Integrations" tab or "Team Keys" section in App Store Connect.</strong></summary>

This typically means you do not have the required **Admin** role for your App Store Connect account. Only users with the Admin role can view this section and generate API keys. Please contact the Account Holder or an existing Admin on your team to either grant you Admin access or to generate the API key for you.

</details>

<details>

<summary><strong>I don't see the "Alternative Distribution" or "Marketplace" tab.</strong></summary>

This section is only visible to accounts that are eligible for alternative marketplace distribution under Apple's terms for the EU. Please ensure:

1. You have accepted the "[Alternative Terms Addendum for Apps in the EU.](https://developer.apple.com/contact/request/alternative-eu-terms-addendum/)" If you believe you are eligible but cannot see this section, you may need to contact Apple Developer Support.

</details>


# How to Publish an App

A comprehensive guide on how to publish your app on Onside, explaining the two review paths: the standard App Store Review and the Onside-only Notarization.

Once your account is set up and connected, you're ready to publish. The process starts in App Store Connect, where you'll upload your build and submit it for review.

Onside offers two distinct paths for publishing your app, each with its own benefits. Choose the one that best suits your distribution strategy.

***

### Choosing Your Publishing Path

{% tabs %}
{% tab title="App Store Review (Onside + AppStore)" %}
This is the standard path for developers who want their app to be available on **both the main App Store and Onside.**

* **Best For:** Developers looking to maximize their reach by being on both platforms and who want to easily sync their existing App Store presence with Onside.
* **Process:** Your app goes through the standard App Store Review process. Once approved by Apple, it will appear on both stores.
* **Syncing:** This path enables seamless, automatic syncing of your app builds and metadata from the App Store to Onside. You can set it up once and get additional visibility and installs from Onside with minimal extra effort.

{% hint style="info" %}
This is the recommended path for most developers who already have or plan to have a presence on the main App Store.
{% endhint %}
{% endtab %}

{% tab title="Notarization (Publish on Onside Only)" %}
This is a streamlined path for developers who want their app to be available **exclusively on Onside.**

* **Best For:** Developers looking for a faster path to market or those with apps in categories that benefit from more flexible guidelines (provided they are fully compliant with EU law).
* **Process:** Your app goes through Apple's Notarization process. This is a lighter, automated review focused primarily on security, checking for malware and basic functionality, rather than the full set of App Store content guidelines.
* **Speed:** Notarization is typically much faster than a full App Store Review. While the first notarization can sometimes take longer, subsequent ones are often very quick.

{% hint style="info" %}
Even with Notarization, your app must meet certain basic functionality requirements outlined in Apple's [Notarization Review Guidelines](https://developer.apple.com/app-store/review/guidelines/). For example, if your app includes "Sign in with Apple," it must function correctly. If your app requires a login, you must provide test credentials in the review notes.
{% endhint %}
{% endtab %}
{% endtabs %}

***

### Publishing Steps in App Store Connect

The first part of the process happens in your App Store Connect account.

{% stepper %}
{% step %}

#### Upload Your Build

Prepare and upload your app build to **App Store Connect** just as you normally would. Onside will sync this build automatically.
{% endstep %}

{% step %}

#### Prepare for Submission

* In App Store Connect, select your app and navigate to the version you want to submit.
* Scroll down to the **App Review Information** section.
* If your app requires a login to access its core features, you **must** provide a valid username and password in the "Sign-in information" section. This is required for both App Store Review and Notarization.

<div align="left"><figure><img src="/files/hqD21enhF4gA66QGKjTN" alt="" width="563"><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

#### Select Your Review Type

* In the "App Review Information" section, find **Review Type** and click **Edit**.
* A pop-up will appear. Choose either **App Store** or **Notarization**, depending on the path you selected above.
* Click **Save**.

<div align="left"><figure><img src="/files/PgOoSY4ldK7gqXFqnCNO" alt="" width="375"><figcaption></figcaption></figure> <figure><img src="/files/AjfrtNxzvZUbrOgDbFBz" alt="" width="375"><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

#### Submit for Review

* At the top right of the page, click **Add for Review**.
* Follow the final prompts to **Submit to App Review**.

<div align="left"><figure><img src="/files/4jMcerRRkhDm9GgUmvPH" alt="" width="563"><figcaption></figcaption></figure></div>
{% endstep %}
{% endstepper %}

***

### Final Steps in the Onside Console

After your app successfully passes either App Store Review or Notarization, it will appear in your Onside Developer Console, with a status "Missing Info".

1. Go to the **Apps** section in your Onside Console.
2. Click on your app.
3. Complete the remaining setup steps, such as setting the app's **Price** and **Availability** (the countries where it will be available).
4. Once all information is complete, click **Publish**.
5. After you publish the app for the first time, it goes to Onside moderation. This review can take up to 24 hours. We check that the app is ready to be displayed in the Onside Store and that your setup is complete.

After moderation is complete, your app will be live on the Onside Store.

***

### Troubleshooting & FAQs

<details>

<summary><strong>I don't see the "Notarization" option in the Review Type pop-up.</strong></summary>

This usually means the app has not been made eligible for alternative marketplace distribution in your App Store Connect settings.

1. Go to **App Store Connect > Users and Access > Alternative Distribution**.
2. Click on the **Onside** marketplace entry.
3. Ensure the app you are trying to publish is **checked** in the list of eligible apps.
4. Click **Save**.

</details>

<details>

<summary><strong>My app was approved by Apple, but I don't see it in my Onside Console.</strong></summary>

This can happen for a couple of reasons:

* **App Not Selected for Onside:** The most common reason is that the app hasn't been enabled for Onside distribution. Follow the steps in the expandable section above to ensure your app is selected in the **Alternative Distribution** settings in App Store Connect.

{% hint style="danger" %}
Please note: If you add a **new app** to your App Store Connect account, it is **not** automatically enabled for Onside. You must manually go into the **Alternative Distribution** settings each time to add new apps to the list.
{% endhint %}

* **Marketplace Token Issue:** In rare cases, there might have been an issue with your Marketplace Token connection. Please go to **App Store Connect > Users and Access > Alternative Distribution** and verify that the Onside marketplace is listed and active. If not, you may need to re-complete the connection steps.

</details>


# App Requirements & Guidelines

A practical guide to the Onside App Guidelines, our commitment to safety and quality, and how to prepare your app for a smooth review process.

To publish on the Onside Store, your application must first pass a Review or Notarization (light version of Review) by Apple. This is a mandatory step that ensures all apps on the platform meet foundational security and quality standards.

The specific Apple review your app undergoes depends on whether you want to publish exclusively on Onside or on both Onside and the main App Store.

***

{% stepper %}
{% step %}

### Meeting Apple's Review Requirements

{% tabs %}
{% tab title="Onside Only (Notarization Review)" %}
This is the path for apps that will be distributed **exclusively on Onside**. Your app must pass Apple's **Notarization** process, which is a streamlined, automated review focused on security, privacy, and ensuring the app is free of malware.

**Your app must comply with Apple's Notarization Review Guidelines.**

{% hint style="info" %}
**How to View the Correct Guidelines:**

1. Go to the official [**Apple App Store Review Guidelines**](https://developer.apple.com/app-store/review/guidelines/).
2. At the top of the page, find and check the box that says: **"Show Notarization Review Guidelines Only"**&#x20;

   <div align="left"><figure><img src="/files/OY7VzkyZ20rLp6YIm773" alt="" width="239"><figcaption></figcaption></figure></div>
3. This filters the page to show the exact, more concise set of rules that apply to the Notarization process.
   {% endhint %}
   {% endtab %}

{% tab title="Onside + App Store (App Store Review)" %}
This is the path for apps that will be available **on both Onside and the main App Store**. Your app must pass the full, comprehensive **App Store Review** process.

**Your app must comply with the complete App Store Review Guidelines.**

You can find the full, unfiltered guidelines here: [**Apple App Store Review Guidelines**](https://developer.apple.com/app-store/review/guidelines/) (view the page without checking the "Notarization Only" box).
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

### Aligning with Onside's Platform Principles

Once your app has successfully passed Apple's review, it must also align with our own platform principles. We focus on ensuring a high-quality, transparent, and fair ecosystem for everyone.

<details>

<summary><strong>🛡️ Safety &#x26; Reviewability</strong></summary>

* **Provide Full Access for Review:** This is one of the most common reasons for rejection during Apple's review. If your app requires a login, you **must** provide a valid demo username and password in the "Review Notes" section of your submission in App Store Connect. If a reviewer cannot fully access your app, it cannot be approved.
* **Objectionable Content:** Apps with offensive, insensitive, or mean-spirited content will not be approved. User-generated content must have robust filtering and a clear method for users to report abuse.

</details>

<details>

<summary><strong>⚙️ Performance &#x26; Stability</strong></summary>

* **No Crashes or Bugs:** Your app must be a complete, stable product that does not crash or exhibit obvious bugs.
* **Accurate Metadata:** Your app's name, description, screenshots, and previews must accurately reflect its content and functionality.

</details>

<details>

<summary><strong>💼 Business &#x26; Transparency</strong></summary>

* **Sign in with Apple:** This is a mandatory requirement. If your app uses any third-party or social login service (like Facebook, Google, or email login), you **must** also offer **Sign in with Apple** as an equivalent option.
* **Clear Monetization:** All in-app purchases and subscriptions must be presented honestly and transparently, with no hidden terms.

### Where Onside Offers More Flexibility

While all apps must follow the foundational rules above, our content policies are more flexible than those of the main App Store, particularly in the following areas:

* **Broader Content Categories:** We provide a platform for apps in categories that may have limited distribution elsewhere, such as **Adult Content** (where legally permissible and appropriately age-gated), **Crypto Applications**, and certain types of **Gambling or Casino-style Entertainment**.

> Our primary focus in these categories is ensuring that your app is **fully compliant with all applicable local and EU laws**, rather than enforcing stricter, platform-specific content rules.

</details>
{% endstep %}
{% endstepper %}


# Wrap your Site as App

Learn how Onside can turn your existing Progressive Web App (PWA) or website into a native iOS app for distribution on our marketplace.

{% columns %}
{% column width="41.66666666666667%" %}
**Don't have a native iOS app?** No problem. If you have a Progressive Web App (PWA) or a mobile-optimized website, Onside's **PWA Wrapper Service** can quickly convert it into a native iOS app, ready for distribution on our marketplace.

This is a fast, cost-effective way to reach a new audience on iOS without needing to rebuild your application from scratch.
{% endcolumn %}

{% column %}
![](/files/r0HqK5u3Hz2iRKfHe95C)
{% endcolumn %}
{% endcolumns %}

***

#### How It Works & Key Benefits

Our system takes your web app and wraps it inside a lightweight, native iOS container. The result is a fully installable iOS app that appears on a user's Home Screen with its own icon. When they launch it, it opens directly into your web experience.

* ✅ **Zero Native Code Required:** We handle the entire wrapping process, saving you significant development time and resources.
* 🚀 **Fast Time-to-Market:** Go from a web link to a live iOS app in a matter of days, not months.
* 📲 **Access Native Features:** Unlike a standard website, a wrapped app can access powerful iOS functionalities like **Push Notifications** to re-engage your users.
* 🌐 **New Distribution Channel:** Reach a new audience on iOS and test the market before committing to full native development.

### **A Step-by-Step Guide to Publishing**

The process is designed to be simple, with minimal effort required from your side.

> 📌 **Before You Begin**\
> You must have an active **Apple Developer Account**. If you don't have one, you can enroll here: [Apple Developer Program](https://developer.apple.com/programs/enroll/).

{% stepper %}
{% step %}

#### Prepare Your Web App URL

Before we can wrap your app, you need a single, public URL where your web application lives.

**Web App URL:** The `https://` link your native app will load on launch.

{% hint style="warning" %}
**Ensure your web app is mobile-ready:**

* ✅ It must have a responsive design that works well on iPhones.
* ✅ All navigation must be handled within your app (no browser "back" buttons).
* ✅ It cannot rely on browser-specific features like multiple tabs or the address bar.
  {% endhint %}
  {% endstep %}

{% step %}

#### [**Create your developer account** ](/console/getting-started-and-publishing/create-onside-account)in the **Onside Developer Console**

{% endstep %}

{% step %}

#### **Grant Us Access to Your Apple Developer Account**

To handle the app notarization and publishing process on your behalf, we need access to your App Store Connect account.

➡️ **Action:** Please invite the user **<ifresh.wp@gmail.com>** to your App Store Connect team with the **Admin** role. This allows us to manage app listings and builds without accessing your account's financial information.
{% endstep %}

{% step %}

#### **Provide Your App's Metadata and** Site URL

Next, we need the metadata for your app's public store listing page and the URL of your web app.

To make it easy, please copy the template below into an email or document, fill in your information, and send it back to us.

[**Download Metadata template ->**](https://docs.google.com/document/d/1KUy9heaA55R-MMu2zvia872WwK30KnGx7975K6kCZr8/edit?usp=sharing)

Here is a detailed breakdown of each required field:

| Field Name                    | Requirements & Description                                                                                                                                                             |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **App Identity**              |                                                                                                                                                                                        |
| Bundle ID                     | **Required.** Your app's unique identifier. Format: com.companyname.appname                                                                                                            |
| Version Number                | **Required.** The initial version number. We recommend 1.0.0.                                                                                                                          |
| App Name                      | **Required.** The name displayed in the Onside store (max 30 characters).                                                                                                              |
| App Home Screen Name          | **Required.** The short name displayed under the icon on a user's phone (max 10-12 characters).                                                                                        |
| **Store Listing Details**     |                                                                                                                                                                                        |
| Subtitle                      | A short, catchy phrase appearing below your app's name (max 30 characters).                                                                                                            |
| Description                   | **Required.** Your app's full description (max 4,000 characters). Use this to detail features and benefits.                                                                            |
| Keywords                      | Comma-separated search terms that help users find your app (max 100 characters total).                                                                                                 |
| Primary Language              | **Required.** The default language code for your app (e.g., en for English).                                                                                                           |
| Age Rating                    | **Required.** Select one: 4+, 9+, 12+, 17+, or 18+.                                                                                                                                    |
| **Legal & Support**           |                                                                                                                                                                                        |
| Support URL                   | **Required.** A public URL where users can find support.                                                                                                                               |
| Privacy Policy URL            | **Required.** A public URL for your app's privacy policy.                                                                                                                              |
| Marketing URL                 | Optional. A link to a promotional website for your app.                                                                                                                                |
| Copyright                     | **Required.** The copyright notice, typically © \[Year] \[Your Company Name].                                                                                                          |
| **Technical & Visual Assets** |                                                                                                                                                                                        |
| Supported Orientations        | **Required.** Choose one: Portrait, Landscape, or Both.                                                                                                                                |
| App Icon                      | **Required.** Must be **1024x1024 pixels** in PNG format, with **no transparency**.                                                                                                    |
| Screenshots                   | **Required.** Minimum of 3, maximum of 10 PNG images. Use the correct dimensions for your app's orientation. Common sizes are 1290x2796 px for portrait or 2796x1290 px for landscape. |
| {% endstep %}                 |                                                                                                                                                                                        |

{% step %}

#### We Wrap Your Web App

Our system takes your web app and wraps it inside a lightweight, native iOS container. This container is essentially a dedicated, full-screen browser (a `WebView`) focused exclusively on your web experience.
{% endstep %}

{% step %}

#### It Becomes a Real iOS App

The result is a fully installable iOS app. Users can download it from the Onside Store, and it will appear on their Home Screen with its own icon, just like any other app. When they launch it, it opens directly into your web experience.
{% endstep %}
{% endstepper %}

### Key Benefits of Using the PWA Wrapper

{% tabs %}
{% tab title="Zero Rebuilding" %}
You don't need to write a single line of native iOS code. We handle the "wrapping" process. This saves you significant development time and resources.
{% endtab %}

{% tab title="Unlock Native Features" %}
Unlike a standard website in a browser, a wrapped PWA can access powerful native iOS functionalities, giving your users a richer experience:

* **Push Notifications:** Re-engage your users directly on their devices.
* **Works Offline:** If your PWA has service workers, it can provide offline functionality.
* **And more...** The wrapper can be configured to access other native features as needed.
  {% endtab %}

{% tab title="New Channel for Growth" %}
Publishing on Onside gives you a new channel to acquire users and generate traffic. It's an excellent, low-cost way to test the waters with an iOS audience before committing to full native development.
{% endtab %}
{% endtabs %}

***

### Requirements for Your Web App

To ensure a high-quality user experience, your web app should be:

* **Mobile-Optimized & Responsive:** Your site must be fully responsive and provide a great user experience on iPhone screen sizes.
* **Secure (HTTPS):** Your website must be served over HTTPS.
* **A PWA (Recommended):** While most mobile-friendly sites will work, Progressive Web Apps with a manifest file and service workers will provide the best experience, enabling features like offline access and a more app-like feel.

{% hint style="info" %}
**What's the difference?**&#x20;

A user can browse your **Website** in Safari.&#x20;

The **Store App** created by our wrapper feels like a dedicated application, launching full-screen from the user's home screen and enabling extra features like push notifications.
{% endhint %}

#### Ready to Submit?

Once you have everything collected, you can submit your app details through the Onside Console.

<p align="center"><a href="https://console.onside.io" class="button primary" data-icon="browser">Go to Onside Console</a></p>

If you have any questions about whether your web app is a good fit for our PWA Wrapper Service, please contact our developer support team at [**support@onside.io**](mailto:support@onside.io).


# Edit Your App Metadata

A guide to managing your app's store listing information on Onside, including syncing with the App Store and making manual edits for instant updates.

The Onside Developer Console gives you flexible and powerful ways to manage your app's product page information. You can choose to automatically synchronize your listing with App Store Connect or take manual control for instant updates.

<figure><img src="/files/4iPPctrTDqFrZhRIUofa" alt="" width="563"><figcaption></figcaption></figure>

***

### Sync with App Store Connect (Default Mode)

By default, your app's information is set to **Sync with App Store Connect**. This is the easiest way to keep your listings consistent across platforms.

<figure><img src="/files/5MC7ivpqvMPt8Miw3OKp" alt="" width="318"><figcaption></figcaption></figure>

* **How it Works:** When this toggle is **ON**, Onside automatically pulls your app's metadata—such as its name, description, subtitle, and keywords—directly from your App Store Connect listing.
* **Update Speed:** When you release a new version with updated metadata on the App Store, the changes will typically appear on your Onside product page **within an hour**.

> This feature is designed to save you time. You only need to manage your metadata in one place (App Store Connect), and we handle the rest.

***

### Manual Editing Mode

If you want to tailor your app's listing specifically for the Onside audience or make instant changes, you can switch to manual editing.

To enable this, simply turn **OFF** the **"Syncing with App Store Connect"** toggle at the top of the Information page. A confirmation will appear, noting that editing is now enabled.

<figure><img src="/files/fwuFNNkSjmtLxd8fl5Np" alt="" width="287"><figcaption></figcaption></figure>

Once in manual mode, you can edit the following fields:

{% tabs %}
{% tab title="Localizable Information" %}
This is the descriptive content that users see on your product page. All fields can be localized for different languages. Character limits are aligned with Apple's to ensure you can easily switch back to sync mode if needed.

* **Name:** Your app's official name.
* **Subtitle:** A short summary that appears under your app's name.
* **Description:** A detailed description of your app’s features and functionality.
* **Keywords:** Add up to 10 keywords that best describe your app to improve search visibility.
* **Support URL:** A link to your website where users can find help and support.
* **Marketing URL (Optional):** A link to a webpage with more marketing information about your app.

<details>

<summary><strong>Supported Languages for Localization</strong></summary>

Onside supports all languages available in App Store Connect. You can provide localized metadata for each language you support. The table below shows common European languages as an example.

| Country                                     | Default Language | Additional Supported Language |
| ------------------------------------------- | ---------------- | ----------------------------- |
| Austria (AUT)                               | German           | English                       |
| Belgium (BEL)                               | English          | Dutch, French                 |
| Bulgaria (BGR)                              | English          | -                             |
| Croatia (HRV)                               | English          | Croatian                      |
| Cyprus (CYP)                                | English          | Greek, Turkish                |
| ...and all other App Store Connect locales. |                  |                               |

</details>
{% endtab %}

{% tab title="General Information" %}
This section contains general details and classifications for your app.

* **Type:** The app's type (e.g., App, Game). This is synced and cannot be edited here.
* **Category:** The primary category that best describes your app. This can be changed from the App Store Connect category.
* **Spicy Category:** Onside offers a unique "Spicy" category for apps in genres that may have limited distribution elsewhere. This includes apps related to **Adult Content** (where legally compliant and age-gated), **Crypto**, or **Gambling-style Entertainment**. Selecting this ensures your app reaches the appropriate audience responsibly.
* **Copyright (Optional):** The name of the person or entity that owns the exclusive rights to your app.
* **Content Rights (Optional):** Clarify if your app contains, shows, or accesses third-party content. If it does, you must confirm you have the necessary rights to use it.
* **Age Rating:** The app's age rating, which is synced and cannot be edited here.
* **Primary Language:** The main language of your app.
  {% endtab %}
  {% endtabs %}

### Content That is Always Synced

{% hint style="info" %}
For consistency and security, some core app components are **always** synced from your App Store Connect account, even when you are in manual editing mode.

* **App Icon:** Your app's icon can only be updated on Xcode.
* **App Previews & Screenshots:** All visual previews and screenshots are pulled directly from your App Store Connect listing.
* **The App Build Itself:** The installable file (`.ipa`) is always sourced from your App Store Connect account.
  {% endhint %}

***

### Important Content Rules

* All metadata you provide (text and URLs) must be legal and adhere to our **Content Policies**.
* Explicit 18+ content is **not permitted** in the app's name, description, icon, or screenshots. Such content must only be available within the app itself, which should be correctly categorized as "Spicy" and have the appropriate age rating.

***

### Frequently Asked Questions

<details>

<summary><strong>How long do my manual edits take to appear in the Onside Store?</strong></summary>

Changes made in manual editing mode are typically live on your product page **almost immediately**, without needing to submit a new app build.

</details>

<details>

<summary><strong>Can I edit my app's icon or screenshots in the Onside Console?</strong></summary>

No. Currently, the app icon, screenshots, and video previews are always synchronized from your App Store Connect listing. To change them, you must update them on the App Store.

</details>

<details>

<summary><strong>What happens if I switch back from manual mode to "Sync with App Store"?</strong></summary>

If you re-enable the sync, your manually entered metadata will be overwritten by the current information from your App Store Connect listing during the next sync cycle.

</details>


# Managing User Access and Roles

Manage user access in Onside: invite team members, assign roles and permissions, set organization ownership, and revoke access—all from the Developer Console.

## Coming Soon!

We are actively working on a robust user and role management system to give you granular control over your team's access to the Onside Developer Console.


# Onside Legal Documents

Access Onside’s legal documents: Terms of Use, Privacy Policy, Developer Agreement, Data Processing, and compliance policies—latest versions and change history.

To use the Onside Developer Console and publish apps on the Onside Store, you must agree to our legal terms. We recommend you review these documents carefully.

### Key Documents

* **Free Apps Agreement:**
  * These specific terms govern your access to and use of the Onside Developer Console, including your responsibilities, content management, and other critical aspects of publishing. You accept these during the final step of the Developer Console registration process.
* **General Onside Terms of Use:**
  * These are the overall terms that apply to the use of all Onside services, including the Onside Store by end-users and general platform usage.
  * You can access the full document here: [Onside Terms of Use](https://onside.io/terms-of-use)
* **Onside Privacy Policy:**
  * This document details how Onside collects, uses, shares, and protects your personal information and the data of your users.
  * You can access the full document here: [Onside Privacy Policy](https://onside.io/privacy-policy)

***

It is important that you understand these terms. If you have any questions regarding these documents, please feel free to contact our support team.


# Overview & Commission

An introduction to Onside's monetization framework, starting with your choice of payment processing, and then detailing our commission and tax policies.

Welcome to the Onside monetization guide! We offer a flexible, developer-first framework to help you generate revenue from your applications.

Your first and most important decision is **how you will process payments**. This choice determines which monetization models are available to you and what fees apply.

***

### Your Two Options for Processing Payments

{% columns %}
{% column %}
**Use Onside's Integrated Payments (Recommended)**

This is the most straightforward, feature-rich, and cost-effective way to monetize on our platform.

* **How it Works:** You integrate our simple [**Onside Payment SDK**](/sdk). We handle all payment processing, security, and provide detailed analytics.
* **Available Monetization Models:** With this option, you can use **all** of Onside's monetization features:
  * [Paid Apps](/console/monetization/paid-apps)
  * [In-App Purchases (IAPs)](/console/monetization/in-app-purchases)
  * [Subscriptions](/console/monetization/subscriptions)
* **Fees:** Payments processed through Onside are subject to a **10% commission**.
  {% endcolumn %}

{% column %}
[**Use Your Own Third-Party Payment Provider**](/console/monetization/using-a-third-party-payment-provider)

This option is for developers with specific, existing payment infrastructures.

{% hint style="warning" %}
**Important Limitation: Not for Paid Apps**

You **cannot** use your own payment infrastructure if your app is a **Paid App**. The initial purchase of any paid application **must** be processed through Onside's integrated payment system. This option is only available for apps that are free to download.
{% endhint %}

* **How it Works:** You process payments for your in-app digital goods and services using your own provider (e.g., Stripe, Adyen).
* **Responsibilities:** You are responsible for the entire payment lifecycle and **must report all transactions** to us via our [**Transaction Reporting API**](/api/transactions-reporting-api).
* **Commission:** This option is subject to a different, higher commission structure, which will be outlined in your developer agreement.

[**Learn More About Using a Third-Party Provider →**](/console/monetization/using-a-third-party-payment-provider)
{% endcolumn %}
{% endcolumns %}

***

### Onside's Monetization Models

If you choose to use our recommended **Integrated Payments**, you can leverage the following models.

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><a href="/spaces/V24VBLkTP47H3Kf48DFs/pages/cEHgDv7NGBquDGHeLyQ4"><strong>Paid Apps</strong> →</a></td><td>Offer your application for a one-time upfront price. This purchase is always processed through Onside's system.</td><td><a href="/spaces/V24VBLkTP47H3Kf48DFs/pages/cEHgDv7NGBquDGHeLyQ4">/spaces/V24VBLkTP47H3Kf48DFs/pages/cEHgDv7NGBquDGHeLyQ4</a></td></tr><tr><td><a href="/pages/ZXxfwYY21Yz9PCv9X7ja"><strong>In-App Purchases (IAPs)</strong> →</a></td><td>Sell digital content and features directly within your app, powered by our SDK.</td><td><a href="/pages/ZXxfwYY21Yz9PCv9X7ja">/pages/ZXxfwYY21Yz9PCv9X7ja</a></td></tr><tr><td><a href="/pages/QwHFLVleY7QejI9V4gc3"><strong>Subscriptions</strong> →</a></td><td>Offer recurring access to content and services using our integrated subscription management.</td><td><a href="/pages/QwHFLVleY7QejI9V4gc3">/pages/QwHFLVleY7QejI9V4gc3</a></td></tr></tbody></table>

***

### Our commission

The rate below applies when you use **Onside's Integrated Payments**.

* **Payment commission:** **10%** on payments processed through Onside.

{% hint style="info" %}
No separate payout fee applies.
{% endhint %}

***

### How We Handle Value-Added Tax (VAT) in the EU

Navigating taxes can be complex, so we simplify it for you.

For sales of apps and in-app content processed through Onside's system, **we are responsible for collecting and remitting Value-Added Tax (VAT)** to the appropriate tax authorities in the EU.

The price you set in the Onside Console is inclusive of any applicable VAT. We calculate the tax based on the customer's country and handle the payment on your behalf. This means you don't have to worry about the complexities of EU VAT.

> **Disclaimer:** While Onside handles VAT for sales on our platform, you are still responsible for any other taxes on your income (such as corporate or personal income tax) in accordance with the laws of your country. We always recommend consulting with a local tax advisor.


# Using a Third-Party Payment Provider

A guide for developers who choose to use their own payment infrastructure and need to report transactions to Onside.

Onside offers the flexibility for you to use your own existing payment processing infrastructure instead of our integrated Payment SDK. This guide outlines the responsibilities and technical requirements for this option.

{% hint style="warning" %}
This option is intended for developers with specific business needs and an existing, robust payment system. It involves a higher commission rate and mandatory technical integration for reporting. For most developers, using the integrated [**Onside Payment SDK**](/sdk) is the more straightforward and profitable choice.
{% endhint %}

***

### Developer Responsibilities

If you choose to process payments using your own provider (e.g., Stripe, Adyen, PayPal), you are responsible for:

* **All Payment Processing:** Handling the entire transaction lifecycle, including charging the user, processing refunds, and managing subscription billing logic.
* **Security & Compliance:** Ensuring your payment flow is secure and compliant with all relevant regulations (like PCI DSS).
* **Customer Support:** Providing direct support to users for any payment-related issues.
* **Mandatory Transaction Reporting:** Reporting every successful transaction from Onside users back to us via our API.

***

### Mandatory Transaction Reporting

To ensure accurate commission calculation and platform accounting, you **must** report every successful purchase or subscription renewal made by users who downloaded your app from Onside.

This is done programmatically via our dedicated [**Transaction Reporting API**](/api/transactions-reporting-api).

Failure to report transactions accurately and in a timely manner is a violation of the Onside Developer Agreement and may result in the removal of your app from the marketplace.

***

### Commission Structure

Using a third-party payment provider is subject to a **different commission model** than our standard integrated SDK. The commission rate will be higher to account for the use of the platform and our role in app discovery and distribution.

The specific terms will be outlined in your Onside Developer Agreement or discussed with you directly. Please contact our developer support team for details.


# Paid Apps

Configure and manage in-app products in Onside. Set pricing, regions, and billing options for iOS apps directly from the Developer Console.

Onside provides developers with the flexibility to offer their applications as paid downloads. This guide outlines how to set up pricing for your app and provides information on our commission structure.

### Setting Up Your App as a Paid Application

You can define your app as paid and set its price during the app submission process or when editing an existing app within the Onside Developer Console.

**Flow for Setting Up App Price:**

1. **Navigate to Availability and Price:**
   * In the Onside Developer Console, select your app.
   * Go to the **"Apps"** section in the left-hand menu.
   * Within your app's settings, find and select the **"Availability and Price"** tab.
   * *(Screenshot 1: Onside Developer Console - Availability and Price page)*
2. **Add or Edit Price:**
   * If you haven't set a price before, you'll see an option to **"Add Price."** Click this button.
   * If you are editing an existing price, you'll manage it from this section.
   * *(Screenshot 1: Detail of the "Add Price" button on the Availability and Price page)*
3. **Set Up Base App Price:**
   * A modal or new page will appear titled **"Set up app price."**
   * Here, you will select a **"Base price"** from a dropdown list of available price tiers (e.g., €0.00, €0.99, €1.99, etc.).
   * The base price you set here determines the Onside Store price for users and is the foundation for calculating your proceeds.
   * *(Screenshot 2 & 3: "Set up app price" modal showing the "Base price" dropdown and available price tiers)*
   * **Important for Paid Apps:** If your app is free, choose €0.00. If you set your app as paid (any price above €0.00), you must review and accept the **Paid Application Agreement**. This agreement will be presented to you if it's your first time setting a paid price.
   * *(Screenshot 4: "Paid Apps Agreement" page)*
   * Click **"Next"** or **"Sign"** (after reviewing the agreement if applicable).
4. **Review Country Prices (If Applicable):**
   * Onside may automatically generate comparable prices for all supported countries based on your selected base price.
   * You will have a chance to review these **"Country prices."** While the system aims for accurate conversions and regional pricing conventions, you may have options to adjust these if needed (please refer to specific console instructions for adjustments, if available).
   * *(Screenshot 5: "Country prices" page showing base price and per-country prices)*
   * Once you are satisfied, click **"Confirm"** or **"Save."**

Your app will then be set as a paid application with the pricing you've configured. Changes to pricing typically require the app to go through a quick update process or review before they are live in the Onside Store.

### Commission structure for paid apps

Paid apps use a fixed fee structure on Onside.

* **Payment commission:** **10%** of the app sale price.
* **No payout fee:** Onside does not charge a separate payout fee.
* **Why this commission applies:** It helps cover platform operations, including infrastructure, team costs, and Apple's Core Technology Fee (CTF).

For detailed terms, please refer to the Onside Publisher Agreement provided during your account setup or contact our developer support for a personalized discussion.

***

To learn how to set up prices for in-app purchases or subscriptions within your app, please see the respective sections in our documentation:

* [In-App Purchases](broken://pages/CyH2xJQs9yWJ1S8BYNav#import)
* [Subscriptions](/console/monetization/subscriptions)


# In-App Purchases

Configure and manage in-app purchases in Onside: set consumables, non-consumables, entitlement logic, product bundles, and pricing across regions using the Developer Console.

In-App Purchases (IAPs) allow you to sell a variety of virtual content directly within your app, such as premium features, virtual currency, or extra content. This guide explains how to set up and manage IAPs in the Onside Developer Console.

### Overview: How to Set Up In-App Purchases

Setting up In-App Purchases on Onside involves a few key steps:

1. **Sign the Paid Apps Agreement:** If you haven't already (e.g., when setting up a paid app), ensure that your developer account has accepted the necessary terms for offering paid content. This is typically handled in the Business section of the console.
2. **Create an In-App Purchase in the Onside Console:** Define each IAP item, its type, name, description, price, and availability directly in the console.
3. **Integrate the Onside Payment SDK:** Implement our Payment SDK within your app to handle the purchase flow, communicate with Onside servers, and unlock content for users.

This guide focuses on step 2: Creating and managing IAP items in the Onside Developer Console.

***

### Creating an In-App Purchase Item

Follow these steps to define a new IAP item in the Onside Developer Console:

1. **Navigate to In-App Purchases:**
   * From the Onside Developer Console, select your app (e.g., "Superlist").
   * In the left-hand menu, under your app's settings, click on **"In-App Purchases."**
   * If you have no IAPs yet, you'll see an invitation to create one. Click the **"Create"** button.**(Image: Screenshot of the empty "In-App Purchases" tab with the "Create" button.)**
2. **Define Basic IAP Details (Initial Modal):**\
   A modal titled **"Create an In-App Purchase"** will appear.**(Image: Screenshot of the "Create an In-App Purchase | empty" modal.)**
   * **Select type:** This is a crucial step. Choose the type of IAP you are creating.
     * **(Image: Screenshot of the "Select type" dropdown showing Consumable and Non-Consumable options with their descriptions.)**
     * **Consumable:** A product that is used up and can be purchased multiple times. For example, virtual currency, hints in a game, or extra lives.
     * **Non-Consumable:** A product that is purchased once and does not expire or decrease with use. For example, unlocking a pro version of an app, removing ads, or accessing a specific feature set permanently.
     * **Important:** The In-App Purchase type **cannot be changed after creation**. Choose carefully.
   * **Primary language:** Select the primary language for your IAP's display information (e.g., English). You can add more localizations later.
   * **Product name:** Enter a user-facing name for your IAP (e.g., "Superlist Lifetime"). This name will be displayed on the Onside Store product page.
     * Must be less than **30 characters**.
     * Cannot be a product name already used by another IAP in this app.
   * **Description:** Provide a compelling description of what the user gets with this purchase (e.g., "Unlock premium maps and trail recommendations").
     * Must be less than **45 characters** (for the initial short description, longer descriptions may be editable on the full IAP page).
     * This description will be displayed on the Onside Store product page.
   * Click **"Create."**
3. **Configure Full IAP Details:**\
   After clicking "Create," you'll be taken to the full configuration page for your new IAP item (e.g., "Superlist Lifetime," initially in "Draft" status).**(Image: Screenshot of the main IAP configuration page, e.g., "Superlist Lifetime | Draft" status, showing sections like Localizable, General, Image, Availability, Price.)**

   Here, you'll need to set up several aspects:

   * **Localizable:**
     * Review and edit the **Product name** and **Description** for your primary language. You can also add localizations for other languages by selecting them from the language dropdown (e.g., "English").
   * **General:**
     * **Type:** Confirms the IAP type you selected (e.g., "Non-Consumable"). This cannot be changed.
     * **Product ID:** A unique identifier for this IAP, often in reverse domain notation (e.g., "com.superlist.lifetime"). This ID is used in your app's code to reference the purchase.
   * **Image (Optional):**
     * You can **Upload image** to represent your IAP. This is recommended for better visual appeal.
     * Specs: JPG, PNG, or HEIC, at least 1024x1024 pixels, and up to 1MB.
   * **Availability:**
     * Click **"Set Up Availability"** (or "Edit" if already set).
     * Select the countries where you want this IAP to be available for purchase.
   * **Price:**
     * Click **"Add Pricing"** (or "Edit" if already set).
     * This follows a similar flow to setting the price for a paid app:
       * Set a **Base price** for the IAP.
       * Review and confirm **Country prices** that are automatically generated based on your base price.**(Image: Screenshot of the "Price added | Draft" state, showing Country prices.)**
4. **Save and Activate:**
   * Once all details are configured, click **"Save."** The IAP will remain in "Draft" status.
   * When you are ready to make the IAP live, click **"Activate product."**
   * After activation, the status will change to **"Active."(Image: Screenshots for "In-App Purchase saved | Draft" and "In-App Purchase activated | Active" states.)**

***

### Understanding In-App Purchase Types

Choosing the correct IAP type is essential as it cannot be changed after creation.

* **Consumable:**
  * **What it is:** A product that users can purchase multiple times. Its benefit is "consumed" or used up.
  * **Examples:** Virtual currency (gems, coins), extra lives in a game, a pack of filters for a photo app, hints for a puzzle.
  * **User Experience:** Once used, the user may need to purchase it again to regain its benefit.
* **Non-Consumable:**
  * **What it is:** A product that users purchase once to unlock content or features permanently.
  * **Examples:** Unlocking the "pro" version of an app, removing advertisements, accessing a specific premium feature set, unlocking a new game level or character pack.
  * **User Experience:** Once purchased, the user has permanent access to this item or feature on all their devices associated with their Onside account.

***

### Integrating the Onside Payment SDK

After you have created and activated your In-App Purchase items in the Onside Developer Console, the next crucial step is to integrate the **Onside Payment SDK** into your application.

This SDK will allow your app to:

* Fetch the list of available IAPs you've configured.
* Initiate the purchase process for a selected IAP.
* Securely handle the transaction with Onside's servers.
* Verify successful purchases.
* Unlock the purchased content or feature for the user.

Please refer to the detailed **\[Onside Payment SDK Documentation (Link\_To\_SDK\_Docs\_Here)]** for comprehensive integration instructions, code samples, and best practices.

***

### Editing an In-App Purchase

You can modify the details of your IAPs after creation. The behavior depends on the IAP's status:

* **If the In-App Purchase is in "Draft" status:**
  * Any changes you make to its name, description, price, availability, or image will be saved directly and will be reflected when you activate it.
* **If the In-App Purchase is in "Active" status:**
  * **Price Changes:** Modifying the price will generally apply to all *future* purchases of that IAP.
  * **Name and Description Changes:**
    * For **new users** or users who have not yet purchased the IAP, the updated name and description will be displayed immediately in the Onside Store.
    * For **existing users who have already purchased** a Non-Consumable IAP, their purchase history or entitlement record within your app might still reflect the **original name** associated with their purchase transaction. The functionality they unlocked remains, but the display name in historical records might not change.
  * Changes to active IAPs may go through a quick review or update process before they are live.

Always ensure your IAP information is clear and accurately reflects what the user will receive.


# Monetization FAQ

Frequently asked questions about Onside monetization: commissions, app pricing, in-app products, subscriptions, payment flow, refunds, and policy clarifications.

***

Here are answers to some common questions about monetization on Onside.

<details>

<summary>What fees apply to sales through Onside?</summary>

Sales processed through Onside are subject to a **10% payment commission**. No separate payout fee applies.

</details>

<details>

<summary>Are there any fees besides the Onside commission?</summary>

No. Onside charges a **10% payment commission** for sales processed through the platform. No separate payout fee applies. This commission helps cover platform operations, including infrastructure, team costs, and Apple's Core Technology Fee (CTF).

</details>

<details>

<summary>Who is responsible for taxes like VAT?</summary>

If Onside processes the payments for your app, Onside handles the collection and remittance of Value-Added Tax (VAT) for sales made through the platform in the EU. However, you are responsible for any other taxes on your income (such as corporate or personal income tax) in accordance with the laws of your country. We always recommend consulting with a local tax advisor.

</details>

<details>

<summary>Do I have to sign the Paid Application Agreement for free apps with In-App Purchases or Subscriptions?</summary>

Yes. The Paid Application Agreement covers all forms of monetization on Onside. You will need to review and accept this agreement before you can set a price for your app or offer any In-App Purchases or Subscriptions.

</details>

<details>

<summary>Can I change an In-App Purchase from 'Consumable' to 'Non-Consumable' after creating it?</summary>

No. The type of an In-App Purchase (Consumable or Non-Consumable) is permanent and cannot be changed after it has been created. This is because our system architecture and user entitlement records depend on this initial setting. If you have chosen the wrong type, you will need to delete the incorrect IAP and create a new one with the correct type.

</details>

<details>

<summary>Do I have to use the Onside Payment SDK for my purchases?</summary>

Using the Onside Payment SDK is the standard, recommended, and most straightforward way to implement In-App Purchases and Subscriptions. It's designed to work seamlessly with our platform.

However, we offer the flexibility for developers to use their own payment infrastructure. Please be aware that this option is subject to different commercial terms, which may affect your commission rate. For more details, please contact <support@onside.io> to discuss your specific needs.

</details>

<details>

<summary>What happens to existing subscribers if I delete a subscription's base plan?</summary>

When you delete a base plan, it becomes unavailable for new purchases immediately. Existing subscribers who are on that plan will **retain access** to their subscription content until the end of their current, already-paid billing period. After that, their subscription will **not auto-renew**, and it will expire. Before deleting, you must make the plan unavailable and ensure users have been notified, as prompted in the developer console.

</details>

<details>

<summary>How are prorated refunds for subscription upgrades calculated?</summary>

Onside's system handles all prorated calculations automatically. When a user upgrades to a higher-tier base plan within the same subscription group, our system calculates the value of the unused time on their current (lower-tier) plan. This amount is credited to the user and applied toward the price of the new, upgraded plan. The user pays the difference, and a new billing cycle begins immediately for the upgraded plan.

</details>


# ASO & Discovery Tips

Learn how to optimize your app's product page for better visibility, more downloads, and greater success on the Onside Store.

App Store Optimization (ASO) is the process of improving your app's visibility in a marketplace to increase downloads. Onside is a new and growing platform, which means there is less competition and a greater opportunity for your app to stand out.

This guide provides a checklist of best practices to help you succeed.

{% hint style="info" %}
**Your First-Mover Advantage on Onside:** Because our marketplace is growing, effective ASO can have a huge impact. Optimizing your page now gives you a significant advantage in ranking well and being discovered by our expanding user base.
{% endhint %}

***

### Your Onside ASO Checklist

A great product page is crucial for converting views into downloads. Use this checklist to optimize every element of your app's listing.

<details>

<summary><strong>Click here to expand the ASO checklist</strong></summary>

* **1. App Name**
  * Your app's name should be unique, relevant, and easy to remember. While you can include a keyword or two if it feels natural, prioritize brand identity and clarity. (Character limit: 30)
* **2. Subtitle**
  * This is a short phrase that appears under your app's name. Use it to summarize your app's main purpose with strong, compelling keywords. (Character limit: 30)
* **3. Keywords**
  * Choose up to 10 relevant keywords that users might search for to find your app. Think about your app's features, category, and target audience. Research competitors to see what terms they use.
* **4. App Icon**
  * Your icon is your app's first impression. It should be simple, recognizable, and look great at all sizes. Avoid using words in your icon. Remember, this is synced from your App Store Connect listing.
* **5. Screenshots & App Previews (Videos)**
  * Showcase your app's best features in action. The first 1-3 screenshots are the most important. Use them to tell a story and highlight your app's core value proposition. A short video preview is highly effective. These are also synced from App Store Connect.
* **6. Description**
  * The first few lines are the most critical. Start with a compelling sentence that clearly explains what your app does and for whom. Use bullet points or short paragraphs to list key features and benefits, making it easy to read.
* **7. Ratings and Reviews**
  * High ratings and positive reviews significantly impact downloads. While Onside is a new platform, we often sync reviews from the App Store to provide immediate social proof. Encourage your happy users to leave reviews directly on Onside to build your reputation here.

</details>


# "Download from Onside" Button for Your Website

Include Onside badge on your website as a clear call to action to get your app

## Button Styles

### Multicolored

Multicolored versions are preferred for button usage. All elements, including the outline, are integral parts of the button and should not be edited. Place the button in the same row as other app store buttons if they are present in the layout. Position the Onside button first in the row to ensure it is visible and accessible to users. Ensure the button contrasts with the background: use a black button on a light background and a white button on a dark background. The button color should match the buttons of other app stores if they are placed nearby. Use the white version only if the Onside button is the only one in the layout (i.e., no other app store buttons are present).

<div align="left"><figure><img src="/files/rn46FurWJzLUDieEeDfR" alt="" width="375"><figcaption><p>Dark</p></figcaption></figure> <figure><img src="/files/7GPcQE6acOa1fHKeJTJj" alt="" width="375"><figcaption><p>Light</p></figcaption></figure></div>

### Download Multicolored Version (PNG, SVG)

{% file src="/files/haz0p8zwxypEu6ZoKGpg" %}
Multicolored Version (PNG, SVG)
{% endfile %}

### Monochrome

Monochrome versions are designed to give you more flexibility in adapting the button to your marketing materials. A monochrome version is suitable when the button is the only one in the layout and blends well with surrounding colors. If other app store buttons are present, use the preferred multicolored version. Ensure the monochrome version remains clearly visible and accessible against the layout background. We recommend using the black and blue versions on a light background and the white and gradient versions on a dark background to maximize visibility.

<figure><img src="/files/T11QuLpMZSqr4iBRFFfv" alt=""><figcaption><p>Monochrome</p></figcaption></figure>

### Download Monochrome Version (PNG, SVG)

{% file src="/files/WwgwXF48UtGJCL6hZurD" %}
Monochrome version (PNG, SVG)
{% endfile %}

### Standout

Accent buttons with extended wording are available for use. Use them if you want to promote multiple apps or draw extra attention to the button. All usage rules for the accent button remain the same as for the multicolored and monochrome versions.

<figure><img src="/files/REAzLtJbIADUIOoeGooB" alt=""><figcaption><p>Standout</p></figcaption></figure>

### Download standout version (PNG, SVG)

{% file src="/files/3Fjtd7RbDHgRnFUasf0L" %}
Standout Version (PNG, SVG)
{% endfile %}

## Button Use

### Graphic Standards

The minimum recommended button size is 40 pixels or 10 mm. The minimum padding around the button should be 1/5 of its height.

<figure><img src="/files/VB0nnVFGoFexWztuIzYz" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/SWOv5kgquyNPmVy1ZnR2" alt=""><figcaption></figcaption></figure>

To maximize the button’s effectiveness in promoting your app, follow these guidelines and avoid any modifications to its design.

<figure><img src="/files/SgPY2XIzLkVoH2RPguRg" alt=""><figcaption></figcaption></figure>

### Placement

Place the button where it is highly visible and easily accessible. Use only on clean, solid, or gradient backgrounds with sufficient contrast, avoiding excessive graphics. Don’t place the button on a visually noisy background with many small details, as it may get lost. Don’t use a button color that differs from other app store buttons. Don’t place the button on a background that blends with its color. The button must remain in a straight position — rotations at any angle are not recommended. Ensure the button integrates seamlessly into the overall layout without overwhelming it or getting lost among other elements.

<figure><img src="/files/9VpDbeM7oDIEf6oZ1PCT" alt=""><figcaption></figcaption></figure>


# Grow with Onside

Onside helps publishers grow through Web2App distribution, running and optimizing campaigns across Google Ads and Meta Ads — from setup to scale.

### What we can do for you

We can run and optimize campaigns on your behalf, including:

#### Paid acquisition channels

* Google Ads (Search, Performance Max, Display)
* Meta Ads (Facebook & Instagram)

#### Campaign management

* Campaign setup and launch
* Creative recommendations and testing
* Landing page optimization
* Web2App funnel optimization
* Attribution and analytics setup

#### Measurement

We help configure tracking and measure:

* Clicks
* Store installs
* App installs
* Key in-app conversion events (e.g. registration, purchase)

We can see every step a user takes—from clicking your ad, visiting your page, installing the app, to finally using it—so you know exactly where your customers are coming from and which campaigns are working. We tailor our services to fit your specific needs and objectives.

For more information: [Onside Attribution](/api/onside-attribution)

### Two ways to grow with Onside

#### 1. Test & Scale

**Validate Web2App performance before committing significant budget**

For publishers looking to test alternative distribution, Onside offers a structured way to validate performance before scaling.

**What’s included**

* Up to **$10K in marketing budget**
* Campaign execution in **Google Ads and/or Meta**
* Funnel setup and optimization
* Attribution and performance measurement
* Hands-on support throughout the testing phase

**How it works**

1. Publisher integrates Onside tracking
2. Onside launches and optimizes campaigns
3. If targets are met, the publisher scales independently

**Best for**

* Publishers testing Web2App
* Teams exploring alternative distribution
* Apps validating acquisition performance

***

#### 2. Revenue Share Growth

**Onside funds growth — partners share the upside**

For apps already demonstrating strong performance, Onside can take on marketing investment and acquisition execution.

**What Onside covers**

* **100% of marketing spend**
* Campaign execution in **Google and Meta**
* Optimization of the full Web2App funnel
* Continuous performance improvements

**Commercial model**\
Revenue sharing is based on performance.

Onside succeeds only when acquisition performs — aligning incentives around sustainable growth.

**Typically a fit for**

* Apps already performing well in the App Store
* Products with proven monetization
* Clear conversion signals and measurable LTV

> Available for selected partners only.

### Interested in growing with Onside?

Interested in Google Ads, Meta campaigns, Web2App acquisition, or one of Onside’s growth programs?

Contact <support@onside.io> to discuss eligibility, GEOs, launch timelines, and the best growth model for your app.


# Landing Page Asset Requirements

What to send Onside when we create a distribution landing page for your app.

If Onside is creating a distribution landing page for your app, send the assets below.

### Logo or app icon

Provide your logo or app icon in one of these formats:

* **Vector preferred:** `SVG`, `PDF`, or `AI`
* **Raster:** `PNG` or `JPG` with a minimum width of `1024 px`

Use a clean export:

* No background
* No shadows
* No raster effects

### Cover image

This is the main visual at the top of the landing page.

Provide a desktop cover image with these specs:

* **Formats:** `.jpg`, `.webp`, or `.png`
* **Aspect ratio:** `5.03:1`
* **Minimum resolution:** `1440x286`

### Screenshot carousel section

Provide `4–6` key images for the screenshot carousel.

Use these rules for all carousel images:

* Use the same aspect ratio across all visuals
* **Portrait recommended:** `19.5:9`
* **Landscape recommended:** `9:19.5`
* **Formats:** `.jpg`, `.webp`, or `.png`

### Description

Provide a short description for the landing page:

* `1–2` short sentences
* Maximum `160` characters
* Focus on user benefit, not product features
* Use a friendly, confident, and conversational tone

### Writing principles

Follow these rules for the description:

* **User-first perspective:** describe what users gain
* **Light, modern tone:** keep the copy human
* **Value-driven language:** use active verbs like `Enjoy`, `Bring back`, `Stay updated`, `Keep both`, and `Upgrade easily`

### Submission checklist

Before you send assets, make sure you have:

* A logo or app icon
* A cover image
* `4–6` screenshot carousel images
* A short landing page description


# Participate in UX Research (Developers)

An invitation for Onside developers to participate in UX research interviews to help shape the future of our platform.

At Onside, we build our platform *together* with developers like you. Your feedback is the most important part of our process, and we'd love to hear your honest thoughts. ❤️

We are currently looking for developers to join us for a **30-minute online interview in English** to discuss your experience with the Onside Developer Console. This is your chance to speak directly with our product team and influence our roadmap.

If you're ready to share your feedback, you can sign up right away: <a href="https://forms.gle/uLoy4FMaqgPkCbeg9" class="button primary" data-icon="heart">Give Feedback</a>

***

### What's in It for You?

We know your time is valuable, and we want to thank you properly for your contribution! As a token of our appreciation, all interview participants are eligible for exclusive benefits:

* 🌟 **Priority Featuring** for your app on the Onside Store's homepage.
* 💰 **Favorable Commission Rates** on your app's sales.
* 🎁 **A Special Bonus** for your time and insights.

Interested in the rewards and ready to share your thoughts?

<a href="https://forms.gle/uLoy4FMaqgPkCbeg9" class="button primary" data-icon="heart">Get My Bonus</a>

***

### What We'll Discuss

We want to have an open, friendly conversation about your experience. What do you love? What is frustrating? What's missing? There are no right or wrong answers—we just want to help build a platform that truly works for you.

We truly appreciate your willingness to help. Let's build a better Onside, together! 🙏💙

<a href="https://forms.gle/uLoy4FMaqgPkCbeg9" class="button primary" data-icon="heart">I'll Help!</a>


# Welcome

An overview of the Onside Payment SDK, designed for easy integration to help you monetize your apps with in-app purchases and subscriptions.

#### Monetize Your App with Ease

The Onside Payment SDK (**OnsideKit**) lets you integrate in-app purchases and subscriptions into your iOS app. It handles the entire payment flow — from product display to checkout — providing a secure experience for your users while keeping integration simple.

Currently supported payment methods: **Apple Pay** and **bank cards**. See [Managing Payment Methods](/sdk/purchasing/payment-methods) and [Apple Pay](/sdk/purchasing/apple-pay).

{% hint style="info" %}
**New to OnsideKit?** Start with the [Quick Start](/sdk/getting-started/quick-start) for a minimal, end-to-end integration you can copy-paste, then come back here for the full guides.
{% endhint %}

#### Get Started

Follow these steps to integrate OnsideKit into your app:

<table><thead><tr><th width="80">Step</th><th width="280">Guide</th><th>Description</th></tr></thead><tbody><tr><td>1</td><td><a href="/pages/fE4i5CW7M0ZeaZqcsDJp">Installation</a></td><td>Add OnsideKit to your project via Swift Package Manager, CocoaPods, or a manual framework install.</td></tr><tr><td>2</td><td><a href="/pages/p3ct9vTDKLqewcHrq3SF">Initializing the SDK</a></td><td>Initialize OnsideKit, register your callback URL scheme, and forward incoming URLs for app-to-app login.</td></tr><tr><td>3</td><td><a href="/pages/IDqAGYlWbxSmIjHh88J7">Authentication &#x26; User Account</a></td><td>Understand the login flows — explicit, implicit (on-demand), and session management.</td></tr><tr><td>4</td><td><a href="/pages/c1zXVakY1NYA1etwIIJj">Fetching Products</a></td><td>Fetch your product catalog, handle regional pricing, and display offerings to users.</td></tr><tr><td>5</td><td><a href="/pages/HhnXQpRNBjCAhuOmnFSN">Making a Purchase</a></td><td>Initiate purchases, process transactions, restore previous purchases, and validate on your backend.</td></tr></tbody></table>

#### Requirements

* **iOS 16.0** or later
* **Xcode 26** or later
* Distributed as a binary `xcframework` via **Swift Package Manager** and **CocoaPods** — see the [Installation](/sdk/getting-started/installation) guide.

{% hint style="info" %}
OnsideKit ships in two flavors: **OnsideKit** (the full SDK, with Apple Pay) and **OnsideKitLite** (the same SDK without the PassKit dependency, for apps that cannot include PassKit — Apple Pay is unavailable there). See [OnsideKit vs OnsideKitLite](/sdk/advanced-and-tooling/onsidekit-lite).
{% endhint %}

#### Designed for a Seamless Transition

OnsideKit is intentionally **modeled on Apple's native StoreKit framework**. If your team has experience with StoreKit, integration will feel familiar — the same delegate patterns, a similar queue-based transaction flow, and comparable product-request APIs. See [Migrating from StoreKit](/sdk/reference/migrating-from-storekit) for a side-by-side mapping.

#### Key Features

* **Familiar & easy integration:** StoreKit-inspired API for a minimal learning curve.
* **Purchases & subscriptions:** one unified flow for consumables, non-consumables, and auto-renewable subscriptions.
* **Apple Pay & bank cards:** Apple Pay (full OnsideKit build) plus a built-in card-management UI.
* **Clear responses & error handling:** informative value types and a unified set of typed errors — see the [Error Reference](/sdk/reference/errors).
* **Native, web & Unity:** a native Swift API, a [JavaScript bridge](/sdk/integrations/js-bridge) for `WKWebView`-based apps, and a [Unity package](/sdk/integrations/unity).
* **Test without a backend:** develop and QA the purchase flow offline with [Local Testing](/sdk/advanced-and-tooling/local-testing) and a `.storekit` file.

***

#### What You Can Do Now

You can start setting up your monetization strategy in the [Onside Developer Console](https://developer.onside.io) right away:

* Set up in-app purchases
* [Configure subscriptions](/sdk/products-and-subscriptions/subscriptions)

#### Related topics

These are the next docs most teams need after the core integration:

* [The Onside Delegate](/sdk/customization/delegate) — theming, host window scene, login routing, and region hints
* [Attribution](/sdk/attribution-and-analytics/attribution) — install-attribution metadata
* [Building funnels with event tracking](/sdk/attribution-and-analytics/building-funnels-with-event-tracking) — track actions with `Onside.track`
* [Purchase Validation](/sdk/purchase-validation/purchase-validation) — server-side verification with the Merchant API
* [OnsideKit Example App](/sdk/reference/example-app) — a working reference project

#### Need More Help?

| Resource              | Description                                        | Link                                                                         |
| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- |
| Monetization Overview | All monetization options available on Onside       | [Go to Monetization Overview](/console/monetization/overview-and-commission) |
| Contact Support       | Questions about the SDK? Our team is here to help. | [Email Us](mailto:support@onside.io)                                         |
| Main Help Section     | Common questions about developing for Onside       | [Visit Help Section](https://docs.onside.io/console/)                        |


# Quick Start

A minimal, end-to-end OnsideKit integration you can copy-paste: initialize the SDK, fetch a product, buy it, and finish the transaction.

This is the shortest path from zero to a completed purchase with OnsideKit. Copy the two files below, replace the placeholders, and you have a working flow. Each step links to a deeper guide when you need more detail.

{% hint style="info" %}
**Before you start**

* OnsideKit is added to your project — see [Installation](/sdk/getting-started/installation).
* Your app is registered with Onside and you have at least one product configured in the [Onside Developer Console](https://developer.onside.io).
* You declared a custom URL scheme in your `Info.plist` for app-to-app login — see [Initializing the SDK](/sdk/getting-started/initialization).
  {% endhint %}

## 1. Initialize the SDK and handle URLs

Do this once at launch, in your `AppDelegate`. `Onside.initialize()` must be called **before** any other OnsideKit API.

```swift
import UIKit
import OnsideKit

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    // Keep a strong reference to your purchase manager for the app's lifetime.
    let store = PurchaseManager()

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // 1. Initialize OnsideKit before calling any other OnsideKit API.
        Onside.initialize()

        // 2. Provide the callback URL scheme you declared in Info.plist.
        Onside.callbackScheme = "com.yourapp.onside"

        // 3. Register a transaction observer as early as possible. The payment
        //    queue stays idle until at least one observer is registered.
        Onside.defaultPaymentQueue().add(observer: store)

        return true
    }

    // 4. Forward incoming URLs so the app-to-app login flow can finish.
    func application(
        _ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
    ) -> Bool {
        Onside.handle(url: url)
    }
}
```

## 2. Create a purchase manager

A single object fetches products, starts purchases, and processes transactions. The payment queue only makes progress while at least one observer is registered (that is why we add it at launch in step 1).

```swift
import OnsideKit

final class PurchaseManager: OnsidePaymentTransactionObserver {

    // Retain in-flight product requests — a dropped request is cancelled.
    private var productsRequest: OnsideProductsRequest?

    // MARK: - Fetch a product

    func loadProduct(identifier: String) {
        let request = Onside.makeProductsRequest(productIdentifiers: [identifier])
        request.delegate = self
        productsRequest = request   // keep it alive for the whole request
        request.start()
    }

    // MARK: - Start a purchase

    func buy(_ product: OnsideProduct) {
        let payment = OnsidePayment(product: product)
        Onside.defaultPaymentQueue().add(payment) { result in
            // The completion only reports the pre-flight outcome, e.g. the user
            // dismissed the login screen. Transaction updates arrive below.
            if case .failure(let error) = result {
                print("Couldn't start the purchase: \(error)")
            }
        }
    }

    // MARK: - Process transactions (OnsidePaymentTransactionObserver)

    func onsidePaymentQueue(
        _ queue: OnsidePaymentQueue,
        updatedTransactions: [OnsidePaymentTransaction]
    ) {
        for transaction in updatedTransactions {
            switch transaction.transactionState {
            case .purchased, .restored:
                unlockContent(for: transaction.payment.product.productIdentifier)
                queue.finishTransaction(transaction)   // required — see the warning below

            case .failed:
                print("Transaction failed: \(String(describing: transaction.error))")
                queue.finishTransaction(transaction)   // required

            case .purchasing:
                break   // still in progress — show a spinner if you like

            @unknown default:
                break
            }
        }
    }

    private func unlockContent(for productIdentifier: String) {
        // Grant access to the purchased content and persist it
        // (e.g. in UserDefaults or the Keychain) so it survives relaunches.
    }
}

// MARK: - Receive the fetched product

extension PurchaseManager: OnsideProductsRequestDelegate {

    func onsideProductsRequest(
        _ request: OnsideProductsRequest,
        didReceive response: OnsideProductsResponse
    ) {
        guard let product = response.products.first else { return }
        // Show `product` in your UI, then call buy(product) when the user taps Buy.
        buy(product)
    }

    func onsideProductsRequest(
        _ request: OnsideProductsRequest,
        didFailWithError error: OnsideProductsRequestError
    ) {
        print("Couldn't load products: \(error)")
    }
}
```

## 3. Run the flow

Kick everything off from your UI — for example, load a product when a screen appears:

```swift
appDelegate.store.loadProduct(identifier: "your.product.identifier")
```

The flow then runs on its own:

1. `loadProduct` fetches the product and `onsideProductsRequest(_:didReceive:)` delivers it.
2. `buy` adds the product to the payment queue. If the user is not logged in, OnsideKit presents the login screen automatically and resumes the purchase afterwards.
3. Each state change is delivered to `onsidePaymentQueue(_:updatedTransactions:)`, where you unlock content and **finish** the transaction.

{% hint style="warning" %}
**You must finish every transaction.** Call `finishTransaction(_:)` for every transaction that reaches `.purchased`, `.restored`, or `.failed`. If you don't, OnsideKit considers it unprocessed and re-delivers it on the next launch. See [The Payment Queue & Transactions](/sdk/core-concepts/payment-queue).
{% endhint %}

{% hint style="info" %}
OnsideKit delivers all delegate and observer callbacks on the **main actor**, and you must retain request objects and observers yourself. See [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention).
{% endhint %}

## Next steps

* [Initializing the SDK](/sdk/getting-started/initialization) — the `Info.plist` URL scheme, `callbackScheme`, and `handle(url:)` in detail
* [Authentication & User Account](/sdk/core-concepts/authentication) — how login works and how to check session state
* [Fetching Products](/sdk/products-and-subscriptions/fetching-products) — reading titles, prices, and subscription details
* [Making a Purchase](/sdk/purchasing/making-a-purchase) — purchase and restore flows, errors, and the storefront safety gate
* [Local Testing with a .storekit File](/sdk/advanced-and-tooling/local-testing) — try the whole flow without a backend


# Installation

Add the OnsideKit framework to your iOS project with Swift Package Manager, CocoaPods, or a manual framework install.

You can integrate OnsideKit using [Swift Package Manager](#swift-package-manager), [CocoaPods](#cocoapods), or by [adding the framework manually](#manual-installation).

## Requirements

* **iOS 16.0** or later as your deployment target
* **Xcode 26** or later

## Choose a product

OnsideKit is distributed as two separate products. Pick the one that fits your app:

<table><thead><tr><th width="200">Product</th><th>When to use it</th></tr></thead><tbody><tr><td><strong>OnsideKit</strong></td><td>The full SDK, including <strong>Apple Pay</strong>. Use this unless you have a specific reason not to.</td></tr><tr><td><strong>OnsideKitLite</strong></td><td>The same SDK <strong>without the PassKit dependency</strong> (no Apple Pay), for apps that cannot include PassKit.</td></tr></tbody></table>

{% hint style="info" %}
Both products are identical apart from Apple Pay support. See [OnsideKit vs OnsideKitLite](/sdk/advanced-and-tooling/onsidekit-lite) for the details. Everywhere below, swap `OnsideKit` for `OnsideKitLite` if you need the PassKit-free build.
{% endhint %}

## Framework installation

{% tabs %}
{% tab title="Swift Package Manager" %}
[Swift Package Manager](https://www.swift.org/documentation/package-manager/) is Apple's official dependency manager, integrated directly into Xcode.

1. In Xcode, select your project in the **Project Navigator**, then open the **Package Dependencies** tab for the project.

<figure><img src="/files/GNtzkMRrXB27hkaCJm4q" alt=""><figcaption></figcaption></figure>

2. Click the **+** button to add a new package.
3. In the search field, paste the SDK's repository URL:

   ```
   https://github.com/onside-io/OnsideKit-iOS
   ```
4. For the **Dependency Rule**, we recommend **"Up to Next Major Version"** to receive updates and bug fixes without breaking changes. Click **Add Package**.

<figure><img src="/files/S1Ft7Jjwz5HAQBchrjDX" alt=""><figcaption></figcaption></figure>

5. Choose the product to add to your app's target — **OnsideKit** (with Apple Pay) or **OnsideKitLite** — and click **Add Package**.

The framework now appears in the Project Navigator and is linked to your target.
{% endtab %}

{% tab title="CocoaPods" %}
[CocoaPods](https://cocoapods.org/) is a popular dependency manager for Swift and Objective-C projects.

1. If you don't have CocoaPods installed, run:

   ```bash
   sudo gem install cocoapods
   ```
2. If your project has no `Podfile`, create one:

   ```bash
   pod init
   ```
3. Add OnsideKit to your app's target in the `Podfile`. Use `OnsideKit` for the full SDK, or `OnsideKitLite` for the PassKit-free build:

   ```ruby
   # Podfile
   platform :ios, '16.0'
   use_frameworks!

   target 'YourAppName' do
     pod 'OnsideKit', :git => 'https://github.com/onside-io/OnsideKit-iOS.git'
     # or, without Apple Pay / PassKit:
     # pod 'OnsideKitLite', :git => 'https://github.com/onside-io/OnsideKit-iOS.git'
   end
   ```
4. Install and open the generated workspace:

   ```bash
   pod install
   ```
5. Open the newly created `.xcworkspace` file and start using the SDK.
   {% endtab %}

{% tab title="Manual" %}
If you prefer not to use a dependency manager, add the framework manually.

1. Download the latest **OnsideKit.xcframework.zip** (or **OnsideKitLite.xcframework.zip**) from the [Releases page](https://github.com/onside-io/OnsideKit-iOS/releases).
2. Unzip it to get the `OnsideKit.xcframework` bundle.
3. In Xcode, select your app's target and open the **General** tab.
4. Find **Frameworks, Libraries, and Embedded Content** and drag the `OnsideKit.xcframework` into it.
5. Set **Embed & Sign** for the framework — this bundles and signs it with your app.

<figure><img src="/files/yrVpYYThq4ANYxVztXZd" alt=""><figcaption></figcaption></figure>
{% endtab %}
{% endtabs %}

You can now `import OnsideKit` (or `import OnsideKitLite`) in your source files.

## Next step

Before calling any OnsideKit API you must initialize the SDK and configure your URL scheme. Continue with [**Initializing the SDK**](/sdk/getting-started/initialization).


# Initializing the SDK

Initialize OnsideKit, register your callback URL scheme, and forward incoming URLs so the app-to-app login flow can complete.

Before you call any OnsideKit API, complete this one-time setup at app launch. It also enables the seamless **app-to-app login** flow, where the user authenticates in the Onside store app and is returned to your app.

The setup has five steps:

1. [Allow your app to detect the Onside app](#id-1-allow-your-app-to-detect-the-onside-app)
2. [Declare your app's URL scheme](#id-2-declare-your-apps-url-scheme)
3. [Initialize the SDK](#id-3-initialize-the-sdk)
4. [Provide your callback scheme to OnsideKit](#id-4-provide-your-callback-scheme-to-onsidekit)
5. [Forward incoming URLs to OnsideKit](#id-5-forward-incoming-urls-to-onsidekit)

## 1. Allow your app to detect the Onside app

To start the app-to-app login, OnsideKit checks whether the Onside store app is installed (via `canOpenURL`). iOS requires you to declare the `onside` scheme you want to query in your `Info.plist`.

Add the `onside` scheme under **Queried URL Schemes** (`LSApplicationQueriesSchemes`):

```xml
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>onside</string>
</array>
```

## 2. Declare your app's URL scheme

Your app needs its own URL scheme so the Onside store app knows where to send the user back. Register a unique scheme under **URL Types** (`CFBundleURLTypes`). Use a reverse-domain style to avoid collisions — for example `com.yourapp.onside`.

```xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>com.yourapp.onside</string>
        </array>
    </dict>
</array>
```

You can also set this in Xcode under **Target → Info → URL Types**.

## 3. Initialize the SDK

Call `Onside.initialize()` once at launch, **before any other OnsideKit API**. Calling an OnsideKit API before this logs a warning and leads to undefined behavior.

```swift
// AppDelegate.swift
import UIKit
import OnsideKit

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        Onside.initialize()

        // Use the exact same scheme you declared in step 2.
        Onside.callbackScheme = "com.yourapp.onside"

        return true
    }
}
```

{% hint style="info" %}
`initialize` is idempotent — a second call is a no-op. It also accepts two optional parameters:

```swift
Onside.initialize(disableSSLPinning: Bool = false, storeKitConfigurationName: String? = nil)
```

* `storeKitConfigurationName` enables an offline testing mode backed by a `.storekit` file — see [Local Testing](/sdk/advanced-and-tooling/local-testing).
* `disableSSLPinning` is a debugging aid only — see [Debugging & installationId](/sdk/advanced-and-tooling/debugging).

For a normal integration, call `Onside.initialize()` with no arguments.
{% endhint %}

## 4. Provide your callback scheme to OnsideKit

Set `Onside.callbackScheme` to the **exact** scheme you declared in step 2 (shown in the snippet above). OnsideKit uses it to recognize the redirect coming back from the Onside store app.

{% hint style="warning" %}
If `callbackScheme` is not set, OnsideKit cannot start the app-to-app login and falls back to its own in-SDK login screen. See [Authentication & User Account](/sdk/core-concepts/authentication).
{% endhint %}

## 5. Forward incoming URLs to OnsideKit

When the Onside store app redirects back to your app, pass the incoming URL to `Onside.handle(url:)`. It returns `true` when the URL was an Onside callback that it consumed.

```swift
@MainActor static func handle(url: URL) -> Bool
```

Implement the forwarding for your app's lifecycle:

{% tabs %}
{% tab title="UIKit (AppDelegate)" %}

```swift
// AppDelegate.swift
import OnsideKit

extension AppDelegate {
    func application(
        _ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey: Any] = [:]
    ) -> Bool {
        if Onside.handle(url: url) {
            return true
        }
        // Handle your own URLs here…
        return false
    }
}
```

{% endtab %}

{% tab title="UIKit (SceneDelegate)" %}

```swift
// SceneDelegate.swift
import OnsideKit

extension SceneDelegate {
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        guard let url = URLContexts.first?.url else { return }
        if !Onside.handle(url: url) {
            // Handle your own URLs here…
        }
    }
}
```

{% endtab %}

{% tab title="SwiftUI" %}

```swift
// YourApp.swift
import SwiftUI
import OnsideKit

@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    if !Onside.handle(url: url) {
                        // Handle your own URLs here…
                    }
                }
        }
    }
}
```

{% endtab %}
{% endtabs %}

## You're set up

OnsideKit is now initialized and ready. Next:

* [Quick Start](/sdk/getting-started/quick-start) — a minimal end-to-end purchase flow
* [Authentication & User Account](/sdk/core-concepts/authentication) — how login works and how to read session state
* [The Onside Delegate](/sdk/customization/delegate) — route SDK screens to a window scene and customize behavior


# Authentication & User Account

How OnsideKit handles authentication: explicit and on-demand login, the two login flows, reading session state, and logging out.

Most OnsideKit features require an authenticated user. A signed-in account securely stores payment methods and purchase history, and determines the correct product availability and pricing for the user's region.

You rarely have to manage login by hand — OnsideKit logs the user in **on demand** when an action needs it.

## How login is triggered

There are two ways the login flow starts.

### Explicit login

Call `Onside.requestLogin(completion:)` — for example, behind a dedicated "Log in" button.

```swift
@MainActor static func requestLogin(
    completion: (@MainActor (Result<Void, OnsideLoginError>) -> Void)? = nil
)
```

```swift
Onside.requestLogin { result in
    switch result {
    case .success:
        // The user is now logged in.
        updateUI()
    case .failure(.loginDiscarded):
        // The user dismissed the login screen.
        break
    }
}
```

`OnsideLoginError` has a single case, `.loginDiscarded`. It is delivered when the user cancels the flow — and also when the login screen could not be presented at all, because no active `UIWindowScene` was available. Either way no user is signed in, so treat it as "login did not happen" rather than strictly "user said no". The `completion` is optional — call `Onside.requestLogin()` to log in without handling the result.

### Implicit (on-demand) login

You usually don't need to call `requestLogin` first. When the user is not authenticated and performs an action that needs an account, OnsideKit presents the login flow automatically and then resumes the original action.

On-demand login is triggered by:

* Starting a purchase — `Onside.defaultPaymentQueue().add(_:completion:)`
* Restoring purchases — `Onside.defaultPaymentQueue().restoreCompletedTransactions(completion:)`
* Presenting the payment methods manager — `Onside.presentPaymentMethodsManager(completion:)`

If the user dismisses the login screen, the corresponding call reports `.loginDiscarded` through its completion handler.

{% hint style="info" %}
Not every account-related call logs the user in. `Onside.makeSignedInAppsHistoryRequest()` does **not** present login — it returns `.failure(.notLoggedIn)` when there is no active session. See [Signed In-App Purchase History](/sdk/purchase-validation/signed-in-apps-history).
{% endhint %}

## The two login flows

Whichever way login starts, OnsideKit uses one of two flows:

* **App-to-app login (preferred).** If the Onside store app is installed **and** you set `Onside.callbackScheme`, OnsideKit hands off to the Onside app for a seamless login and returns the user to your app. This requires the URL-scheme setup and `Onside.handle(url:)` forwarding from [Initializing the SDK](/sdk/getting-started/initialization).
* **In-SDK login (fallback).** Otherwise OnsideKit presents its own phone-number login screen. This is used when the Onside app isn't installed, when `callbackScheme` isn't set, or when you force it via the delegate (see below).

{% hint style="info" %}
You can force the in-SDK flow with `OnsideDelegate.onsideShouldForceLocalLoginMethods()`. Returning `true` always uses OnsideKit's own login screen and skips the app-to-app handoff. It is purely a UI-routing switch — it does not change which credentials are accepted. See [The Onside Delegate](/sdk/customization/delegate).
{% endhint %}

## Reading the session state

The session is represented by the **storefront** on the payment queue. A non-`nil` storefront means there is an authenticated session.

```swift
struct OnsideStorefront {
    var id: String
    var countryCode: String   // the user's region, e.g. "US"
}
```

### Check the current status

```swift
func updateUI() {
    let queue = Onside.defaultPaymentQueue()
    let isLoggedIn = queue.storefront != nil

    loginButton.isHidden = isLoggedIn

    if let region = queue.storefront?.countryCode {
        print("Authenticated. Storefront region: \(region)")
    }
}
```

### Observe changes

To react to login and logout in real time, add an observer conforming to `OnsidePaymentTransactionObserver`. OnsideKit calls `onsidePaymentQueueDidChangeStorefront(_:)` whenever the storefront changes (including `nil` → value on login and value → `nil` on logout).

```swift
import OnsideKit

final class SessionObserver: OnsidePaymentTransactionObserver {

    init() {
        Onside.defaultPaymentQueue().add(observer: self)
    }

    deinit {
        Onside.defaultPaymentQueue().remove(observer: self)
    }

    // Required by the protocol.
    func onsidePaymentQueue(
        _ queue: OnsidePaymentQueue,
        updatedTransactions: [OnsidePaymentTransaction]
    ) {
        // Handle transactions — see "The Payment Queue & Transactions".
    }

    func onsidePaymentQueueDidChangeStorefront(_ queue: OnsidePaymentQueue) {
        print("Storefront changed. Logged in: \(queue.storefront != nil)")
    }
}
```

{% hint style="warning" %}
Observers are held **weakly** — you must keep a strong reference to your observer, and you should `remove(observer:)` it when it goes away. See [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention).
{% endhint %}

## Logging out

`Onside.logout()` ends the current session and clears the user's local authentication state (tokens and cached profile).

```swift
Onside.logout()
```

After logout, `Onside.defaultPaymentQueue().storefront` becomes `nil`, and the next action that requires an account triggers the login flow again.

{% hint style="info" %}
OnsideKit may also log the user out automatically if the server invalidates the session (for example, the cached profile can no longer be refreshed). Observe the storefront, as shown above, to keep your UI in sync.
{% endhint %}

## Authentication, region, and pricing

The signed-in account also determines the user's region, which drives product availability and pricing. You can fetch products before login using a best-guess region, then re-fetch once the storefront is known. See [Regions & Storefronts](/sdk/products-and-subscriptions/regions-and-storefronts).


# The Payment Queue & Transactions

How the OnsidePaymentQueue processes transactions, why a transaction observer is required, the transaction lifecycle, and why finishing transactions is mandatory.

All purchasing in OnsideKit goes through a single, process-wide **payment queue**. It processes purchases and restores, and keeps a list of transactions your app must handle. The model is intentionally close to StoreKit's `SKPaymentQueue`.

## Accessing the queue

```swift
@MainActor static func defaultPaymentQueue() -> OnsidePaymentQueue
```

```swift
let queue = Onside.defaultPaymentQueue()
```

The queue is a shared singleton. You must call [`Onside.initialize()`](/sdk/getting-started/initialization) before accessing it.

{% hint style="info" %}
The accessor is `Onside.defaultPaymentQueue()`. There is no `Onside.paymentQueue()`.
{% endhint %}

`OnsidePaymentQueue` is a protocol — all of its members are `@MainActor`:

```swift
protocol OnsidePaymentQueue: AnyObject {
    var delegate: OnsidePaymentQueueDelegate? { get set }
    var storefront: OnsideStorefront? { get }
    var transactions: [OnsidePaymentTransaction] { get }
    var transactionObservers: [OnsidePaymentTransactionObserver] { get }

    func add(observer: OnsidePaymentTransactionObserver)
    func remove(observer: OnsidePaymentTransactionObserver)

    func add(
        _ payment: OnsidePayment,
        completion: ((Result<Void, OnsidePaymentQueueAddProductError>) -> Void)?
    )
    func restoreCompletedTransactions(
        completion: ((Result<Void, OnsidePaymentQueueRequestRestoreError>) -> Void)?
    )
    func finishTransaction(_ transaction: OnsidePaymentTransaction)
}
```

## A transaction observer is required

The queue makes progress only while at least one **transaction observer** is registered. Until you add one, the queue stays idle — payments and restores are accepted but nothing is processed.

```swift
Onside.defaultPaymentQueue().add(observer: myObserver)
```

Register your observer as early as possible — ideally in your `AppDelegate` — so the queue can immediately process any transactions (including unfinished ones carried over from a previous launch).

{% hint style="info" %}
**Two preconditions for processing.** The queue runs only when (1) at least one observer is registered **and** (2) a storefront exists (the user is logged in). With no observer or no session, transactions sit and wait. See [Authentication & User Account](/sdk/core-concepts/authentication).
{% endhint %}

When you add an observer and the queue already holds transactions, that observer is **immediately called** with the current transactions, so you can resume or finish them.

## The transaction lifecycle

Each transaction is an `OnsidePaymentTransaction` whose `transactionState` moves through these public states:

<table><thead><tr><th width="150">State</th><th>Meaning</th><th>Your action</th></tr></thead><tbody><tr><td><code>.purchasing</code></td><td>In flight — created, awaiting payment, etc.</td><td>Wait (optionally show a spinner).</td></tr><tr><td><code>.purchased</code></td><td>Bought successfully.</td><td>Unlock content, then <code>finishTransaction</code>.</td></tr><tr><td><code>.restored</code></td><td>Returned by <code>restoreCompletedTransactions</code>.</td><td>Unlock content, then <code>finishTransaction</code>.</td></tr><tr><td><code>.failed</code></td><td>Failed. <code>transaction.error</code> is set.</td><td><code>finishTransaction</code> to remove it.</td></tr></tbody></table>

```mermaid
stateDiagram-v2
    [*] --> purchasing: add(payment) / restore
    purchasing --> purchased: success
    purchasing --> restored: restored
    purchasing --> failed: failure
    purchased --> [*]: finishTransaction
    restored --> [*]: finishTransaction
    failed --> [*]: finishTransaction
```

State changes are delivered to your observer's `onsidePaymentQueue(_:updatedTransactions:)`. Inspect each transaction's `transactionState` and react. Because the public enum is resilient, always include an `@unknown default` in your `switch`:

```swift
func onsidePaymentQueue(
    _ queue: OnsidePaymentQueue,
    updatedTransactions: [OnsidePaymentTransaction]
) {
    for transaction in updatedTransactions {
        switch transaction.transactionState {
        case .purchased, .restored:
            unlockContent(for: transaction.payment.product.productIdentifier)
            queue.finishTransaction(transaction)
        case .failed:
            queue.finishTransaction(transaction)
        case .purchasing:
            break
        @unknown default:
            break
        }
    }
}
```

`.purchased` vs `.restored` only tells you how the transaction was surfaced (a fresh purchase vs. a result of `restoreCompletedTransactions`). Both grant the same entitlement and both must be finished. For the full shape of a transaction, see the [Models reference](/sdk/reference/models).

## Finishing transactions is mandatory

{% hint style="danger" %}
**You must call `finishTransaction(_:)` for every transaction that reaches `.purchased`, `.restored`, or `.failed`.**

If you don't, OnsideKit treats the transaction as unprocessed: it stays in the queue and is re-delivered to your observer on the next launch — which can cause you to unlock content repeatedly.
{% endhint %}

```swift
Onside.defaultPaymentQueue().finishTransaction(transaction)
```

Finishing a transaction removes it from the queue (for a purchase, this also performs the necessary server-side completion). When it is removed, observers receive `onsidePaymentQueue(_:removedTransactions:)`.

## Transactions can appear without an explicit purchase

OnsideKit reconciles the account's purchases with the server. As a result, your observer may receive transactions you didn't start in this session — for example a [subscription](/sdk/products-and-subscriptions/subscriptions) renewal, a purchase made on another device, or one interrupted before it finished. Treat every transaction your observer delivers the same way: act on its state and finish it.

## The observer protocol

```swift
protocol OnsidePaymentTransactionObserver: AnyObject { /* ... */ }
```

<table><thead><tr><th width="380">Method</th><th width="110">Required?</th><th>Called when</th></tr></thead><tbody><tr><td><code>onsidePaymentQueue(_:updatedTransactions:)</code></td><td>Yes</td><td>Transactions are added or change state.</td></tr><tr><td><code>onsidePaymentQueue(_:removedTransactions:)</code></td><td>No</td><td>Transactions are removed (after finishing).</td></tr><tr><td><code>onsidePaymentQueueRestoreCompletedTransactionsFinished(_:)</code></td><td>No</td><td>A restore finished successfully.</td></tr><tr><td><code>onsidePaymentQueue(_:restoreCompletedTransactionsFailedWithError:)</code></td><td>No</td><td>A restore failed.</td></tr><tr><td><code>onsidePaymentQueueDidChangeStorefront(_:)</code></td><td>No</td><td>The storefront changed (login/logout/region).</td></tr></tbody></table>

Only `onsidePaymentQueue(_:updatedTransactions:)` is required; the others have default empty implementations. Observers are held **weakly** — keep a strong reference and `remove(observer:)` when done. See [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention).

## When the storefront changes

If the user logs in/out or their region changes, the storefront changes: the queue resets, notifies observers via `onsidePaymentQueueDidChangeStorefront(_:)`, and re-validates pending work. A queued purchase that would now run in a different storefront is gated by the delegate so you can confirm or cancel it. See [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes).

## Next

* [Making a Purchase](/sdk/purchasing/making-a-purchase)
* [Restoring Purchases](/sdk/purchasing/restoring-purchases)
* [Models](/sdk/reference/models) · [Error Reference](/sdk/reference/errors)


# Threading & Object Lifetime

OnsideKit's main-actor threading model and the lifetime rules for request objects, observers, and delegates.

Two rules prevent most integration bugs with OnsideKit: **call it from the main actor**, and **keep a strong reference to the objects you hand it**.

## Threading: everything UI-facing is `@MainActor`

OnsideKit's entry points and callbacks are main-actor isolated:

* The `Onside` facade is `@MainActor`.
* `OnsidePaymentQueue`, `OnsideProductsRequest`, and `OnsideSignedInAppsHistoryRequest` — and all of their methods — are `@MainActor`.
* All delegate and observer methods (`OnsideDelegate`, `OnsidePaymentQueueDelegate`, `OnsidePaymentTransactionObserver`, `OnsideProductsRequestDelegate`, `OnsideSignedInAppsHistoryRequestDelegate`) are `@MainActor`.

In practice: **call OnsideKit from the main actor, and expect every callback and completion handler on the main actor.** You can touch UIKit directly inside them without hopping queues.

The value types you receive are `Sendable` and not actor-isolated, so they are safe to pass to background work:

* Models — `OnsideProduct`, `OnsidePayment`, `OnsidePaymentTransaction`, `OnsideStorefront`, `OnsidePrice`, `OnsidePeriod`, `OnsideProductsResponse`, `OnsideAttributionMetadata`, `OnsideSignedInAppsHistory`.
* Error enums — all `Onside*Error` types are `Sendable` (and `Codable`).

## Object lifetime: what you must retain

OnsideKit holds your callback objects **weakly**. If you don't keep a strong reference, they are deallocated and you stop receiving callbacks.

### Request objects — retain until they finish

`Onside.makeProductsRequest(...)` and `Onside.makeSignedInAppsHistoryRequest()` return request objects that **you** own. The SDK does not keep them alive.

```swift
final class ProductLoader: OnsideProductsRequestDelegate {
    private var request: OnsideProductsRequest?   // strong reference

    func load() {
        let request = Onside.makeProductsRequest(productIdentifiers: ["my.product"])
        request.delegate = self
        self.request = request   // retain for the whole request
        request.start()
    }
}
```

Use `stop()` to cancel an in-flight request; a cancelled request reports `.cancelled` to its failure delegate.

### Observers — retain and remove

The payment queue holds observers weakly. Keep a strong reference to your observer, and balance `add(observer:)` with `remove(observer:)` when the observer goes away.

```swift
final class StoreCoordinator: OnsidePaymentTransactionObserver {
    init()  { Onside.defaultPaymentQueue().add(observer: self) }
    deinit  { Onside.defaultPaymentQueue().remove(observer: self) }

    func onsidePaymentQueue(
        _ queue: OnsidePaymentQueue,
        updatedTransactions: [OnsidePaymentTransaction]
    ) { /* ... */ }
}
```

{% hint style="info" %}
A long-lived owner (an app-level coordinator or your `AppDelegate`) is the natural place to hold the queue observer, so it stays alive for the whole app session and never misses a transaction.
{% endhint %}

### Delegates — retain them too

`Onside.delegate` ([`OnsideDelegate`](/sdk/customization/delegate)) and `Onside.defaultPaymentQueue().delegate` ([`OnsidePaymentQueueDelegate`](/sdk/purchasing/storefront-price-changes)) are weak references. Assign an object you keep alive — typically your `AppDelegate` or an app-level object.

## Switching over OnsideKit enums

OnsideKit ships as a binary framework built for library evolution, so its public enums are **resilient** (non-frozen). When you `switch` over one — such as `OnsidePaymentTransactionState` or `OnsidePeriod` — include an `@unknown default` to stay forward-compatible with future cases:

```swift
switch transaction.transactionState {
case .purchasing: break
case .purchased, .restored: handlePurchase(transaction)
case .failed: handleFailure(transaction)
@unknown default: break
}
```

## Checklist

* ✅ Call OnsideKit from the main actor; handle callbacks on the main actor.
* ✅ Retain request objects until they finish; `stop()` to cancel.
* ✅ Retain observers and delegates; `remove(observer:)` when done.
* ✅ Add an `@unknown default` to every `switch` over an OnsideKit enum.


# Fetching Products

Fetch your product catalog from Onside with a products request, read product details, and handle errors.

Before you can sell anything, fetch the product details from Onside using each product's identifier, which you configure in the [Onside Developer Console](https://developer.onside.io).

{% hint style="info" %}
A product identifier in OnsideKit is the **Onside product identifier (slug)** you configure in the console and pass to the request — it is **not** the App Store / StoreKit SKU. You request and match products by this identifier.
{% endhint %}

The flow is: create a request, assign a delegate, retain it, start it, and handle the result.

## Create and start a request

```swift
@MainActor static func makeProductsRequest(productIdentifiers: Set<String>) -> OnsideProductsRequest
```

```swift
import OnsideKit

final class ProductsViewController: UIViewController {

    private var productsRequest: OnsideProductsRequest?
    private var products: [OnsideProduct] = []

    func fetchProducts() {
        let identifiers: Set<String> = [
            "premium_feature",
            "subscription_monthly",
        ]

        let request = Onside.makeProductsRequest(productIdentifiers: identifiers)
        request.delegate = self
        self.productsRequest = request   // retain it for the whole request
        request.start()
    }
}
```

{% hint style="warning" %}
Keep a strong reference to the `OnsideProductsRequest` until it finishes — see [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention). Use `stop()` to cancel an in-flight request.
{% endhint %}

## Handle the response

Conform to `OnsideProductsRequestDelegate`. All methods are `@MainActor`.

```swift
extension ProductsViewController: OnsideProductsRequestDelegate {

    // Success.
    func onsideProductsRequest(
        _ request: OnsideProductsRequest,
        didReceive response: OnsideProductsResponse
    ) {
        self.products = response.products

        if !response.invalidProductIdentifiers.isEmpty {
            print("Unknown identifiers: \(response.invalidProductIdentifiers)")
        }
        tableView.reloadData()
    }

    // Failure.
    func onsideProductsRequest(
        _ request: OnsideProductsRequest,
        didFailWithError error: OnsideProductsRequestError
    ) {
        print("Failed to fetch products: \(error)")
    }

    // Optional — always called after success or failure. Good for cleanup.
    func onsideProductsRequestDidFinish(_ request: OnsideProductsRequest) {
        self.productsRequest = nil
    }
}
```

Exactly one of `onsideProductsRequest(_:didReceive:)` or `onsideProductsRequest(_:didFailWithError:)` fires per run, always followed by `onsideProductsRequestDidFinish(_:)` (which has a default empty implementation). Calling `start()` while a request is already running is a no-op; the same request object can be re-`start()`ed after it finishes.

### The response object

```swift
struct OnsideProductsResponse {
    var products: [OnsideProduct]
    var invalidProductIdentifiers: [String]
}
```

* `products` — the products that resolved successfully.
* `invalidProductIdentifiers` — an **array** of requested identifiers the backend didn't find. This is a partial-success channel: one response can contain both valid products and unknown identifiers. (This is different from the `.invalidProductIdentifier` error, which means the whole request was rejected — see below.)

## Read a product

`OnsideProduct` exposes everything you need to build your store UI:

<table><thead><tr><th width="280">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>productIdentifier: String</code></td><td>The Onside identifier (slug) you requested.</td></tr><tr><td><code>localizedTitle: String</code></td><td>Display name for the user's locale.</td></tr><tr><td><code>localizedDescription: String</code></td><td>Display description.</td></tr><tr><td><code>iconUrl: URL?</code></td><td>Product icon, if available.</td></tr><tr><td><code>price: OnsidePrice</code></td><td>Price (<code>value: Double</code>, <code>currencyCode: String</code>).</td></tr><tr><td><code>subscriptionPeriod: OnsidePeriod?</code></td><td>Set for subscriptions only.</td></tr><tr><td><code>subscriptionGroupIdentifier: String?</code></td><td>Subscription group, subscriptions only.</td></tr></tbody></table>

```swift
func configure(with product: OnsideProduct) {
    titleLabel.text = product.localizedTitle
    descriptionLabel.text = product.localizedDescription
    priceLabel.text = format(product.price)
}

func format(_ price: OnsidePrice) -> String {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.currencyCode = price.currencyCode
    return formatter.string(from: price.value as NSNumber) ?? "\(price.value) \(price.currencyCode)"
}
```

For subscriptions, the billing period is covered in [Subscriptions](/sdk/products-and-subscriptions/subscriptions).

## Errors

`onsideProductsRequest(_:didFailWithError:)` delivers an `OnsideProductsRequestError`:

<table><thead><tr><th width="240">Case</th><th>Cause</th><th>Suggested handling</th></tr></thead><tbody><tr><td><code>.connectionError</code></td><td>Network failure.</td><td>Offer a retry.</td></tr><tr><td><code>.serviceUnavailable</code></td><td>Server returned 5xx.</td><td>Retry later.</td></tr><tr><td><code>.appNotRegistered</code></td><td>The app/install isn't recognized by Onside (HTTP 404).</td><td>Check your app registration/configuration.</td></tr><tr><td><code>.invalidProductIdentifier</code></td><td>The request was rejected (HTTP 422).</td><td>Check the identifiers you sent.</td></tr><tr><td><code>.cancelled</code></td><td>The request was cancelled (e.g. <code>stop()</code>).</td><td>Usually ignore.</td></tr><tr><td><code>.internalError</code></td><td>Parsing or other unexpected error.</td><td>Report if persistent.</td></tr></tbody></table>

See the full [Error Reference](/sdk/reference/errors).

## Fetching across regions

You can — and should — fetch products before the user logs in, using a best-guess region, then re-fetch once the storefront is known. See [Regions & Storefronts](/sdk/products-and-subscriptions/regions-and-storefronts).


# Subscriptions

Work with auto-renewable subscriptions in OnsideKit: detect them, read the billing period, then purchase, restore, and validate them like any other product.

An auto-renewable **subscription** is an [`OnsideProduct`](/sdk/products-and-subscriptions/fetching-products) like any other — you [fetch](/sdk/products-and-subscriptions/fetching-products) it, [purchase](/sdk/purchasing/making-a-purchase) it, [restore](/sdk/purchasing/restoring-purchases) it, and [validate](/sdk/purchase-validation/purchase-validation) it through the exact same APIs as a consumable or non-consumable. What sets a subscription apart is a small set of subscription-only properties describing its **billing period**.

{% hint style="info" %}
**The OnsideKit subscription API is intentionally small.** Renewals, cancellations, upgrades, billing retries, and the subscription-management UI are handled by the **Onside app** and your backend — not by the SDK. In your app you only *fetch*, *display*, *sell*, *restore*, and *validate* subscriptions; everything on this page covers exactly that.
{% endhint %}

## Detecting a subscription

A product is a subscription when its `subscriptionPeriod` is non-`nil`. That single check distinguishes a subscription from a one-time product:

```swift
if let period = product.subscriptionPeriod {
    // Subscription — billed once every `period`.
} else {
    // One-time product (consumable or non-consumable).
}
```

## Subscription properties

These properties live on [`OnsideProduct`](/sdk/reference/models#onsideproduct). `subscriptionPeriod` and `subscriptionGroupIdentifier` are populated **only** for subscriptions and are `nil` on a one-time product. `price` is non-optional and present on every product; for a subscription it is the recurring per-period charge.

<table><thead><tr><th width="300">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>subscriptionPeriod: OnsidePeriod?</code></td><td>The recurring billing period. Non-<code>nil</code> only for subscriptions — use it to detect one.</td></tr><tr><td><code>subscriptionGroupIdentifier: String?</code></td><td>The subscription group the product belongs to.</td></tr><tr><td><code>price: OnsidePrice</code></td><td>The product price. For a subscription, the amount charged each billing period.</td></tr></tbody></table>

## The pricing types

```swift
struct OnsidePrice {
    var value: Double
    var currencyCode: String   // ISO 4217, e.g. "EUR"
}

enum OnsidePeriod {
    case day(UInt)
    case week(UInt)
    case month(UInt)
    case year(UInt)
}
```

* `OnsidePrice` — a numeric amount plus an ISO-4217 currency code.
* `OnsidePeriod` — a unit with a count, e.g. `.month(1)` (monthly) or `.year(1)` (yearly).

## Displaying a subscription

Combine the recurring price with the period:

```swift
func describe(_ product: OnsideProduct) -> String {
    guard let period = product.subscriptionPeriod else {
        return format(product.price)   // one-time product
    }
    return "\(format(product.price)) / \(describe(period))"
}

func describe(_ period: OnsidePeriod) -> String {
    switch period {
    case .day(let n):   return n == 1 ? "day"   : "\(n) days"
    case .week(let n):  return n == 1 ? "week"  : "\(n) weeks"
    case .month(let n): return n == 1 ? "month" : "\(n) months"
    case .year(let n):  return n == 1 ? "year"  : "\(n) years"
    @unknown default:   return "period"
    }
}

func format(_ price: OnsidePrice) -> String {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.currencyCode = price.currencyCode
    return formatter.string(from: price.value as NSNumber) ?? "\(price.value) \(price.currencyCode)"
}
```

{% hint style="info" %}
`OnsidePeriod` is a resilient (non-frozen) enum — keep an `@unknown default` in every `switch` so your code stays forward-compatible with future cases. See [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention#switching-over-onsidekit-enums).
{% endhint %}

## Purchasing a subscription

Buying a subscription is identical to buying any other product: wrap it in an `OnsidePayment` and add it to the [payment queue](/sdk/core-concepts/payment-queue). The resulting transaction flows through your observer with the same states (`.purchasing`, `.purchased`, `.failed`).

```swift
let payment = OnsidePayment(product: subscription)
Onside.defaultPaymentQueue().add(payment, completion: nil)
```

See [Making a Purchase](/sdk/purchasing/making-a-purchase) for the full flow — processing the transaction, **finishing** it, and the storefront safety gate.

## Restoring a subscription

A subscription is restorable, just like a non-consumable. When the user reinstalls your app or switches devices, `restoreCompletedTransactions(completion:)` re-delivers it as a `.restored` transaction. See [Restoring Purchases](/sdk/purchasing/restoring-purchases).

A renewal can also arrive **unprompted**: the payment queue reconciles it from the server and delivers it to your observer like any other transaction, so process and finish it the same way. See [Transactions can appear without an explicit purchase](/sdk/core-concepts/payment-queue#transactions-can-appear-without-an-explicit-purchase).

## Validating a subscription

The authoritative subscription status — whether it is active, when the current period **expires**, and whether it was cancelled — lives on the server. Verify it from your backend through the [Merchant API](/sdk/purchase-validation/merchant-api): a subscription transaction carries an `expires_at` and a `product_type` of `"SUBSCRIPTION"`.

## Testing subscriptions locally

You can define subscriptions in a `.storekit` file and exercise the whole fetch → purchase → restore flow offline, with no backend. See [Local Testing with a .storekit File](/sdk/advanced-and-tooling/local-testing).


# Regions & Storefronts

How OnsideKit resolves the user's region and storefront before and after login, and how that affects product availability and pricing.

Product availability and pricing vary by region. OnsideKit resolves a region for every products request and exposes the authenticated region as the **storefront**.

## How the region is resolved

### When logged in

The region is the **account's region** — the most accurate source of truth. It is exposed as `Onside.defaultPaymentQueue().storefront?.countryCode`. Once the user is logged in, this always wins; a pre-login hint (below) is ignored.

### When logged out

OnsideKit estimates the region:

1. The `onsideDefaultCountryCodeAssumption()` delegate hint, if you provide one.
2. Otherwise, the device's system region.

```swift
extension AppDelegate: OnsideDelegate {
    func onsideDefaultCountryCodeAssumption() -> String? {
        // An ISO 3166-1 alpha-2 code, e.g. "US", "DE", "GB".
        return userKnownRegion   // or nil to fall back to the device region
    }
}
```

{% hint style="info" %}
`onsideDefaultCountryCodeAssumption()` is the only hook that influences the region, and only **before** login. Provide it when your app already knows the user's likely region (from their profile or settings) so logged-out pricing is accurate. See [The Onside Delegate](/sdk/customization/delegate).
{% endhint %}

## The storefront

```swift
struct OnsideStorefront {
    var id: String
    var countryCode: String
}
```

`Onside.defaultPaymentQueue().storefront` is `nil` until the user logs in, then reflects the account's region. It changes when the user logs in or out, or their region changes.

## Best practice: fetch before login, then re-fetch

You can and should [fetch products](/sdk/products-and-subscriptions/fetching-products) before the user authenticates, so your store UI is ready immediately. Because the true region is only known after login, **re-fetch your products whenever the storefront changes**.

Observe the storefront with a transaction observer:

```swift
func onsidePaymentQueueDidChangeStorefront(_ queue: OnsidePaymentQueue) {
    // The region may have changed — refresh prices/availability.
    reloadProducts()
}
```

See [Authentication & User Account](/sdk/core-concepts/authentication) for the full observer setup.

## Purchase protection on region change

If a user starts a purchase for a product fetched in one region but their account turns out to be in another, OnsideKit does **not** silently charge the new price. The queued purchase is gated so you can confirm the change with the user or cancel it. This is handled by the payment queue delegate — see [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes).


# Making a Purchase

Start a purchase by adding a payment to the queue, process the resulting transaction, and finish it.

A purchase is initiated by adding a payment to the [payment queue](/sdk/core-concepts/payment-queue). The SDK presents whatever UI is needed (login, payment sheet), and the result is delivered to your transaction observer.

{% hint style="info" %}
Make sure you have registered a transaction observer first — the queue does nothing until one is registered. See [The Payment Queue & Transactions](/sdk/core-concepts/payment-queue).
{% endhint %}

## Start the purchase

Create an `OnsidePayment` from an [`OnsideProduct`](/sdk/products-and-subscriptions/fetching-products) and add it to the queue:

```swift
@MainActor func add(
    _ payment: OnsidePayment,
    completion: ((Result<Void, OnsidePaymentQueueAddProductError>) -> Void)?
)
```

```swift
func buy(_ product: OnsideProduct) {
    let payment = OnsidePayment(product: product)
    Onside.defaultPaymentQueue().add(payment) { result in
        if case .failure(let error) = result {
            // Pre-flight failure, e.g. the user dismissed the login screen.
            print("Couldn't start the purchase: \(error)")   // .loginDiscarded
        }
    }
}
```

{% hint style="warning" %}
The `completion` argument is **required** — pass a closure, or `completion: nil` if you don't need the pre-flight result. The completion reports only the synchronous pre-flight outcome (`OnsidePaymentQueueAddProductError.loginDiscarded`). The actual transaction updates arrive through your observer.
{% endhint %}

If the user is not logged in, OnsideKit presents the login flow automatically and resumes the purchase afterwards. See [Authentication & User Account](/sdk/core-concepts/authentication).

### Associating a purchase with your account system

`OnsidePayment.appAccountToken` lets you tie a purchase to your own user/account. Its only initializer is `init(product:)`, so set the token by mutating the value:

```swift
var payment = OnsidePayment(product: product)
payment.appAccountToken = currentUser.id   // your opaque account token
Onside.defaultPaymentQueue().add(payment, completion: nil)
```

The token is carried with the transaction and echoed back on `transaction.payment.appAccountToken`.

## Process the transaction

Updates are delivered to your observer's `onsidePaymentQueue(_:updatedTransactions:)`. Inspect each transaction's state, unlock content, and **finish** it:

```swift
func onsidePaymentQueue(
    _ queue: OnsidePaymentQueue,
    updatedTransactions: [OnsidePaymentTransaction]
) {
    for transaction in updatedTransactions {
        switch transaction.transactionState {
        case .purchased:
            unlockContent(for: transaction.payment.product.productIdentifier)
            queue.finishTransaction(transaction)

        case .restored:
            unlockContent(for: transaction.payment.product.productIdentifier)
            queue.finishTransaction(transaction)

        case .failed:
            // transaction.error is an OnsidePaymentTransactionError (e.g. .cancelled).
            print("Transaction failed: \(String(describing: transaction.error))")
            queue.finishTransaction(transaction)

        case .purchasing:
            break   // in progress — show a spinner if you like

        @unknown default:
            break
        }
    }
}

private func unlockContent(for productIdentifier: String) {
    UserDefaults.standard.set(true, forKey: productIdentifier)
    // Update your UI and grant access.
}
```

{% hint style="danger" %}
**You must finish every transaction** that reaches `.purchased`, `.restored`, or `.failed`. Otherwise OnsideKit re-delivers it on the next launch. See [The Payment Queue & Transactions](/sdk/core-concepts/payment-queue#finishing-transactions-is-mandatory).
{% endhint %}

{% hint style="info" %}
`transaction.error` is an [`OnsidePaymentTransactionError`](/sdk/reference/errors). On a `.failed` transaction, `.cancelled` means the user backed out rather than a hard error; `.presentationFailed` means OnsideKit couldn't present the purchase UI (no active window scene was available) so the purchase never started — retry once the app is in the foreground.
{% endhint %}

## Next

* [Restoring Purchases](/sdk/purchasing/restoring-purchases) — let users get their purchases back
* [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes) — confirm or cancel a purchase when the region/price changes
* [Purchase Validation](/sdk/purchase-validation/purchase-validation) — verify purchases on your backend


# Restoring Purchases

Let users restore their previous non-consumable and subscription purchases on a new device or after reinstalling.

Provide a **Restore Purchases** button (for example in your settings screen) so users who reinstalled your app or switched devices can regain access to their non-consumable and subscription purchases without paying again.

## Trigger a restore

```swift
@MainActor func restoreCompletedTransactions(
    completion: ((Result<Void, OnsidePaymentQueueRequestRestoreError>) -> Void)?
)
```

```swift
func restoreTapped() {
    Onside.defaultPaymentQueue().restoreCompletedTransactions { result in
        if case .failure(let error) = result {
            print("Couldn't start restore: \(error)")   // .loginDiscarded
        }
    }
}
```

{% hint style="warning" %}
The `completion` argument is **required** — pass a closure or `completion: nil`. Like `add(_:completion:)`, the completion only reports the pre-flight outcome (`OnsidePaymentQueueRequestRestoreError.loginDiscarded` if the user dismisses the login screen). Restored transactions are delivered through your observer.
{% endhint %}

## Receive restored transactions

Restored purchases are delivered to the **same** `onsidePaymentQueue(_:updatedTransactions:)` observer method, with a state of `.restored`. Handle them exactly like a purchase — unlock content and finish the transaction:

```swift
case .restored:
    unlockContent(for: transaction.payment.product.productIdentifier)
    queue.finishTransaction(transaction)
```

(See [Making a Purchase](/sdk/purchasing/making-a-purchase) for the full `switch`.)

## Know when the restore finishes

After all restored transactions are delivered, OnsideKit calls one of two optional observer methods. Use them to update your UI (e.g. hide a spinner):

```swift
func onsidePaymentQueueRestoreCompletedTransactionsFinished(_ queue: OnsidePaymentQueue) {
    // Restore finished successfully.
}

func onsidePaymentQueue(
    _ queue: OnsidePaymentQueue,
    restoreCompletedTransactionsFailedWithError error: OnsideTransactionsRestoreError
) {
    // Restore failed — show an error.
}
```

### Restore errors

`OnsideTransactionsRestoreError`:

<table><thead><tr><th width="220">Case</th><th>Cause</th></tr></thead><tbody><tr><td><code>.cancelled</code></td><td>The restore was cancelled.</td></tr><tr><td><code>.connectionError</code></td><td>Network failure.</td></tr><tr><td><code>.serviceUnavailable</code></td><td>Server returned 5xx.</td></tr><tr><td><code>.appNotRegistered</code></td><td>The app/install isn't recognized by Onside (HTTP 404).</td></tr><tr><td><code>.internalError</code></td><td>Parsing or other unexpected error.</td></tr></tbody></table>

See the full [Error Reference](/sdk/reference/errors).


# Handling Storefront & Price Changes

Approve or cancel a queued purchase when the storefront or price changes between enqueue and execution, using OnsidePaymentQueueDelegate.

A purchase can be enqueued in one storefront but execute in another — for example, a logged-out user starts a purchase with their device region, then logs into an account registered in a different country, where the price or availability differs.

OnsideKit does **not** silently charge the new price. Before processing such a purchase, it asks your **payment queue delegate** whether to continue, so you can confirm the change with the user or cancel it.

## The delegate

```swift
protocol OnsidePaymentQueueDelegate: AnyObject {
    @MainActor func onsidePaymentQueue(
        _ queue: OnsidePaymentQueue,
        shouldContinue transaction: OnsidePaymentTransaction,
        in storefront: OnsideStorefront
    ) -> Bool

    @MainActor func onsidePaymentQueue(
        _ queue: OnsidePaymentQueue,
        shouldContinue transaction: OnsidePaymentTransaction,
        in storefront: OnsideStorefront
    ) async -> Bool
}
```

Both a synchronous and an asynchronous variant are available — implement whichever fits. The default implementation returns `true` (always continue), and the async default forwards to the sync one.

{% hint style="info" %}
The gate fires **only** when a queued transaction would execute in a storefront different from the one it was enqueued in. Same-storefront purchases proceed without calling the delegate.
{% endhint %}

* Return `true` — the transaction proceeds in the new storefront.
* Return `false` — the transaction is discarded.

## Implement the gate

Assign a delegate to the queue (keep a strong reference — the delegate is held weakly):

```swift
Onside.defaultPaymentQueue().delegate = self
```

Use the `async` variant to confirm with the user before continuing:

```swift
extension StoreCoordinator: OnsidePaymentQueueDelegate {
    func onsidePaymentQueue(
        _ queue: OnsidePaymentQueue,
        shouldContinue transaction: OnsidePaymentTransaction,
        in storefront: OnsideStorefront
    ) async -> Bool {
        let newPrice = transaction.payment.product.price
        return await confirmPriceChange(
            to: newPrice,
            region: storefront.countryCode
        )
    }

    @MainActor
    private func confirmPriceChange(to price: OnsidePrice, region: String) async -> Bool {
        // Present an alert: "The price is now \(price.value) \(price.currencyCode)
        // for region \(region). Continue?" and return the user's choice.
        return await withCheckedContinuation { continuation in
            // ... present UI, resume with true/false ...
        }
    }
}
```

If you don't set a delegate, OnsideKit continues by default (as if you returned `true`).

## Related

* [Regions & Storefronts](/sdk/products-and-subscriptions/regions-and-storefronts) — how the region is resolved and why it can change
* [The Payment Queue & Transactions](/sdk/core-concepts/payment-queue) — the queue resets and notifies `onsidePaymentQueueDidChangeStorefront` when the storefront changes


# Managing Payment Methods

Let users manage their saved payment methods (bank cards) with OnsideKit's built-in payment methods manager screen.

OnsideKit provides a built-in screen where users can view, add, and remove the **bank cards** saved to their Onside account.

## Present the manager

```swift
@MainActor static func presentPaymentMethodsManager(
    completion: (@MainActor (Result<Void, OnsidePaymentMethodsManagerError>) -> Void)? = nil
)
```

```swift
Onside.presentPaymentMethodsManager { result in
    switch result {
    case .success:
        // The manager was presented and dismissed normally.
        break
    case .failure(.loginDiscarded):
        // The user dismissed the login screen shown before the manager.
        break
    case .failure(.notSupportedInLocalTesting):
        // Not available in local-testing mode.
        break
    }
}
```

`completion` is optional (`= nil`), so you can also call `Onside.presentPaymentMethodsManager()` with no arguments.

## Behavior

* **Implicit login.** If the user is not authenticated, OnsideKit presents the login flow first. If that login does not complete — the user dismissed it, or it could not be presented — the completion fails with `.loginDiscarded`. See [Authentication & User Account](/sdk/core-concepts/authentication).
* **Local testing.** When the SDK is initialized with a `.storekit` configuration, this screen is unavailable and the call fails with `.notSupportedInLocalTesting`. See [Local Testing](/sdk/advanced-and-tooling/local-testing).

## Errors

`OnsidePaymentMethodsManagerError`:

<table><thead><tr><th width="280">Case</th><th>Cause</th></tr></thead><tbody><tr><td><code>.loginDiscarded</code></td><td>The login flow shown before the manager did not complete — dismissed by the user, or not presentable.</td></tr><tr><td><code>.notSupportedInLocalTesting</code></td><td>The SDK is running in local-testing mode (a <code>.storekit</code> configuration was passed to <code>initialize</code>).</td></tr><tr><td><code>.presentationFailed</code></td><td>OnsideKit couldn't present the screen because no active <code>UIWindowScene</code> was available. Nothing was shown. See <a href="/pages/5oGGH6dyxvl0lcEMuSqx">The Onside Delegate</a> to supply the scene explicitly.</td></tr></tbody></table>

## Customizing presentation

The manager is presented in a window owned by the SDK. You can choose which `UIWindowScene` it uses, set that window's [level](/sdk/customization/delegate#window-level), and override its theme via [`OnsideDelegate`](/sdk/customization/delegate), using the `.paymentMethodsManager` screen case.

{% hint style="info" %}
Looking to configure **Apple Pay**? That's a separate setup — see [Apple Pay](/sdk/purchasing/apple-pay).
{% endhint %}


# Apple Pay

Set up Apple Pay in OnsideKit using a Merchant ID, an Apple-generated certificate, and your app's Apple Pay capability.

Apple Pay gives users a fast, secure way to pay with OnsideKit.

{% hint style="warning" %}
Apple Pay is available in the full **OnsideKit** product only. **OnsideKitLite** ships without PassKit, so it has no Apple Pay and the `Onside.applePayMerchantIdentifier` API does not exist there. See [OnsideKit vs OnsideKitLite](/sdk/advanced-and-tooling/onsidekit-lite).
{% endhint %}

Enabling Apple Pay takes four steps:

1. Create a **Merchant ID** in Apple Developer.
2. Generate an **Apple Pay Payment Processing Certificate** for that Merchant ID.
3. Send the certificate to Onside.
4. Configure OnsideKit and your app with the same Merchant ID.

## Step 1: Create a Merchant ID

The Merchant ID identifies your Apple Pay merchant configuration. You'll reuse this exact value in your app and in your Onside configuration.

1. Sign in to your Apple Developer account.
2. Open **Certificates, Identifiers & Profiles → Identifiers**.
3. Click **+**, select **Merchant IDs**, and click **Continue**.
4. Enter a unique **Merchant ID**, click **Continue**, review, and click **Register**.

## Step 2: Generate the Apple Pay Payment Processing Certificate

Apple must issue an **Apple Pay Payment Processing Certificate** for your Merchant ID. This requires a **Certificate Signing Request (CSR)** that Onside provides — do **not** create your own CSR.

#### Request the CSR from Onside

Email <merchant-support@onside.io> and include:

* your app name
* your **Merchant ID** from step 1
* your Onside account or publisher name

Keep the CSR file Onside sends you unchanged.

#### Generate the certificate in Apple Developer

1. Sign in to your Apple Developer account.
2. Open **Certificates, Identifiers & Profiles → Identifiers**.
3. Select the **Merchant ID** you created.
4. Open **Apple Pay Payment Processing Certificate** and start the generation flow.
5. Upload the CSR file provided by Onside.
6. Complete the process and download the generated certificate.

{% hint style="info" %}
Apple Developer can show multiple Apple Pay options. For Onside, generate exactly **Apple Pay Payment Processing Certificate**.
{% endhint %}

{% hint style="warning" %}
You must use the CSR from Onside. If you upload a different CSR, Onside cannot use the generated certificate for your integration.
{% endhint %}

## Step 3: Send the certificate to Onside

Onside uses this certificate to decrypt Apple Pay tokens during transactions. Email the generated certificate file to <merchant-support@onside.io> and include:

* the certificate file you downloaded from Apple
* your app name
* your **Merchant ID**
* your Onside account or publisher name

## Step 4: Configure your app

Once Onside has processed your certificate, configure the same Merchant ID in your app.

#### Set the merchant identifier in OnsideKit

Set `Onside.applePayMerchantIdentifier` to your Merchant ID — **after** `Onside.initialize()`, and before you present any purchase UI:

```swift
import OnsideKit

Onside.initialize()
Onside.applePayMerchantIdentifier = "merchant.your.merchant.id"
```

{% hint style="warning" %}
`applePayMerchantIdentifier` must be set after `Onside.initialize()` (the property requires an initialized SDK) and before the first purchase, so Apple Pay is available when the purchase flow starts.
{% endhint %}

#### Enable the Apple Pay capability in Xcode

Adding the capability generates the merchant entitlement your app needs to present Apple Pay:

1. Open your app target and go to **Signing & Capabilities**.
2. Click **+ Capability** and add **Apple Pay**.
3. In the Apple Pay configuration, select the **Merchant ID** from step 1.

Apple Pay now appears as a payment option in the OnsideKit purchase flow. See [Making a Purchase](/sdk/purchasing/making-a-purchase).


# Overview

Validate purchases server-side: obtain a signed transaction history on the client and verify it against the Onside Merchant API.

To grant entitlements securely, validate purchases on your **backend** rather than trusting the client alone. OnsideKit produces a signed transaction history that your server verifies against the Onside Merchant API.

Validation has two parts:

1. **On the client** — get a signed (JWS) in-app purchase history from OnsideKit and send the relevant order ID (and optionally the JWS) to your backend. See [Signed In-App Purchase History](/sdk/purchase-validation/signed-in-apps-history).
2. **On your backend** — query the Onside Merchant API for the order, verify the signed response, and grant entitlements. See [Backend Validation & Merchant API](/sdk/purchase-validation/merchant-api).

## Flow

```mermaid
sequenceDiagram
    participant App as Mobile App
    participant SDK as OnsideKit
    participant Backend as Publisher Backend
    participant API as Onside Merchant API

    App->>SDK: Request signed history
    SDK->>App: Return signed JWS
    App->>Backend: Send order ID (and JWS)
    Backend->>Backend: Generate Merchant JWT
    Backend->>API: GET /history/{order_id}
    API->>Backend: Return signed transaction details (JWS)
    Backend->>Backend: Verify JWS (Onside public key)
    Backend->>Backend: Grant entitlements
    Backend->>App: Confirm validation
```

## Prerequisites

To call the Merchant API, obtain a **Merchant ID**, a **Merchant Secret**, and a **Secret Key ID (`kid`)** from the Onside Manager. These enable secure server-to-server authentication.

## Next

* [Signed In-App Purchase History](/sdk/purchase-validation/signed-in-apps-history) — the client side
* [Backend Validation & Merchant API](/sdk/purchase-validation/merchant-api) — the server side


# Signed In-App Purchase History

Download a signed (JWS) in-app purchase history on the client using makeSignedInAppsHistoryRequest().

OnsideKit can produce a **signed** in-app purchase history as a compact JWS (JSON Web Signature) for the current user. Send it (or the order IDs it contains) to your backend to validate purchases — see [Backend Validation & Merchant API](/sdk/purchase-validation/merchant-api).

## Create the request

```swift
@MainActor static func makeSignedInAppsHistoryRequest()
    -> Result<OnsideSignedInAppsHistoryRequest, OnsideSignedInAppsHistoryRequestError>
```

The call returns a request object on success, or a pre-flight error:

```swift
switch Onside.makeSignedInAppsHistoryRequest() {
case .success(let request):
    request.delegate = self
    self.historyRequest = request   // retain it
    request.start()

case .failure(let error):
    // .notLoggedIn or .notSupportedInLocalTesting (pre-flight),
    // or a network error.
    print("Couldn't create history request: \(error)")
}
```

{% hint style="info" %}
This API requires an authenticated user — it returns `.notLoggedIn` if there is no session (it does **not** trigger login). In [local-testing](/sdk/advanced-and-tooling/local-testing) mode it returns `.notSupportedInLocalTesting`.
{% endhint %}

## The request object

```swift
protocol OnsideSignedInAppsHistoryRequest: AnyObject {
    @MainActor var delegate: OnsideSignedInAppsHistoryRequestDelegate? { get set }
    @MainActor func start()
    @MainActor func stop()
}
```

Retain the request for the whole operation (a dropped reference is cancelled). Use `stop()` to cancel.

## Handle the result

```swift
extension MyValidator: OnsideSignedInAppsHistoryRequestDelegate {

    func onsideSignedInAppsHistoryRequest(
        _ request: OnsideSignedInAppsHistoryRequest,
        didReceive response: OnsideSignedInAppsHistory
    ) {
        // response.data is the raw JWS blob; response.string is its UTF-8 form.
        sendToBackend(response.data)
    }

    func onsideSignedInAppsHistoryRequest(
        _ request: OnsideSignedInAppsHistoryRequest,
        didFailWithError error: OnsideSignedInAppsHistoryRequestError
    ) {
        print("History request failed: \(error)")
    }

    // Optional — always called after success or failure.
    func onsideSignedInAppsHistoryRequestDidFinish(
        _ request: OnsideSignedInAppsHistoryRequest
    ) {
        self.historyRequest = nil
    }
}
```

### The history value

```swift
struct OnsideSignedInAppsHistory {
    var data: Data        // the raw JWS blob
    var string: String?   // a UTF-8 decoding of `data`, when valid
}
```

`string` is a convenience that UTF-8-decodes `data`; the JWS is normally a printable string, so it is usually non-`nil`.

## Errors

`OnsideSignedInAppsHistoryRequestError`:

<table><thead><tr><th width="280">Case</th><th>Cause</th></tr></thead><tbody><tr><td><code>.notLoggedIn</code></td><td>No authenticated user (this API does not trigger login).</td></tr><tr><td><code>.notSupportedInLocalTesting</code></td><td>Running with a <code>.storekit</code> local-testing configuration.</td></tr><tr><td><code>.connectionError</code></td><td>Network failure.</td></tr><tr><td><code>.serviceUnavailable</code></td><td>Server returned 5xx.</td></tr><tr><td><code>.appNotRegistered</code></td><td>The app/install isn't recognized by Onside (HTTP 404).</td></tr><tr><td><code>.cancelled</code></td><td>The request was cancelled.</td></tr><tr><td><code>.internalError</code></td><td>Parsing or other unexpected error.</td></tr></tbody></table>

Once your backend has the signed history (or the order IDs from it), continue with [Backend Validation & Merchant API](/sdk/purchase-validation/merchant-api).


# Backend Validation & Merchant API

Authenticate to the Onside Merchant API and validate transactions from your own backend.

Your backend validates a purchase by querying the Onside Merchant API for an order, verifying the signed response, and granting entitlements. This is a server-to-server flow — never ship your Merchant Secret in the app.

## Prerequisites

Request these credentials from the Onside team at <merchant-support@onside.io>. Include your company name and app name:

* **Merchant ID**
* **Merchant Secret**
* **Secret Key ID** (`kid`)

## Authentication

Authenticate each request with a JSON Web Token (JWT) in the `Authorization` header, signed with **HS256** using your Merchant Secret.

**Header**

* `alg`: `HS256`
* `kid`: your Secret Key ID

**Claims**

* `aud`: `onside`
* `iss`: `onside`
* `sub`: your Merchant ID
* `exp` and `nbf`: a validity window of **no more than 30 seconds**
* `jti`: a unique token ID

## Request

Fetch the in-app purchase history for an order:

```http
GET /merchant-api/v1/purchases/in-app/history/{order_id}
Authorization: Bearer <merchant JWT>
```

## Response

On success the Merchant API returns a signed **JWS**. After verifying it, the payload is a `TransactionHistory` object listing transaction events.

```json
{
  "transactions": [
    {
      "app_account_token": "user-uuid-1234",
      "bundle_id": "com.example.app",
      "currency": "EUR",
      "expires_at": null,
      "order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_purchase_date": "2025-12-26T10:00:00Z",
      "price": 499,
      "product_id": "remove_ads",
      "product_slug": "remove-ads",
      "product_type": "NON_CONSUMABLE",
      "purchase_date": "2025-12-26T10:00:00Z",
      "quantity": 1,
      "revocation_date": null,
      "revocation_reason": null,
      "fetched_at": "2026-01-26T10:05:00Z",
      "transaction_reason": "PURCHASE",
      "user_country": "DE"
    }
  ]
}
```

## Verify the response

Verify the JWS signature using Onside's public keys, available at:

```
https://onside.io/.well-known/jwks.json
```

Then confirm the decoded payload against the `TransactionHistory` schema before granting entitlements.

## Subscriptions

A subscription order is validated through the same endpoint and the same `TransactionHistory` shape. A subscription transaction differs from a one-time purchase in a few fields:

* `product_type` is `"SUBSCRIPTION"` (rather than `"CONSUMABLE"` / `"NON_CONSUMABLE"`).
* `expires_at` holds the end of the current billing period — it is `null` for non-expiring products.
* `subscription_id` identifies the subscription the transaction belongs to.
* Each renewal is an additional transaction in the `transactions` array, with `transaction_reason` `"RENEWAL"` and the same `original_order_id` as the first purchase.
* A refund sets `revocation_date` and `revocation_reason`.

Treat the subscription as **active** when its most recent transaction has an `expires_at` in the future and a `null` `revocation_date`. The example below shows an initial purchase followed by one renewal:

```json
{
  "transactions": [
    {
      "app_account_token": "user-uuid-1234",
      "bundle_id": "com.example.app",
      "currency": "EUR",
      "expires_at": "2026-02-26T10:00:00Z",
      "order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_purchase_date": "2026-01-26T10:00:00Z",
      "price": 999,
      "product_id": "premium_subscription_monthly",
      "product_slug": "premium-monthly",
      "product_type": "SUBSCRIPTION",
      "purchase_date": "2026-01-26T10:00:00Z",
      "quantity": 1,
      "revocation_date": null,
      "revocation_reason": null,
      "fetched_at": "2026-02-26T10:05:00Z",
      "subscription_id": "sub-group-xyz",
      "transaction_reason": "PURCHASE",
      "user_country": "DE"
    },
    {
      "app_account_token": "user-uuid-1234",
      "bundle_id": "com.example.app",
      "currency": "EUR",
      "expires_at": "2026-03-26T10:00:00Z",
      "order_id": "9b1d2c34-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "original_order_id": "3e46c75a-7e04-4530-979a-e275f3a7c50c",
      "original_purchase_date": "2026-01-26T10:00:00Z",
      "price": 999,
      "product_id": "premium_subscription_monthly",
      "product_slug": "premium-monthly",
      "product_type": "SUBSCRIPTION",
      "purchase_date": "2026-02-26T10:00:00Z",
      "quantity": 1,
      "revocation_date": null,
      "revocation_reason": null,
      "fetched_at": "2026-02-26T10:05:00Z",
      "subscription_id": "sub-group-xyz",
      "transaction_reason": "RENEWAL",
      "user_country": "DE"
    }
  ]
}
```

## API reference

The full request/response schema is documented in the API reference for this section (see the navigation entry under **Backend Validation & Merchant API**).


# Merchant API

## GET /merchant-api/v1/purchases/in-app/history/{order\_id}

> Retrieve transaction details by Order ID

```json
{"openapi":"3.0.3","info":{"title":"Onside Purchase Validation API","version":"1.0.0"},"servers":[{"url":"https://onside.io","description":"Production server"},{"url":"https://onside.dev","description":"Staging server"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}},"paths":{"/merchant-api/v1/purchases/in-app/history/{order_id}":{"get":{"tags":["Merchant API"],"summary":"Retrieve transaction details by Order ID","parameters":[{"name":"order_id","in":"path","description":"The unique identifier of the order.","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Successfully retrieved transaction history (Signed JWS).","content":{"application/jose":{"schema":{"type":"string","description":"Compact JWS string. Payload matches TransactionHistory schema."}}}},"401":{"description":"Unauthorized (Invalid or missing JWT)"},"403":{"description":"Forbidden (Valid JWT but not allowed)"},"404":{"description":"Order not found"}}}}}}
```


# Models

## The TransactionHistory object

```json
{"openapi":"3.0.3","info":{"title":"Onside Purchase Validation API","version":"1.0.0"},"components":{"schemas":{"TransactionHistory":{"type":"object","properties":{"transactions":{"type":"array","items":{"$ref":"#/components/schemas/InAppPurchase"}}},"required":["transactions"]},"InAppPurchase":{"type":"object","properties":{"app_account_token":{"type":"string","nullable":true,"description":"The user account ID."},"bundle_id":{"type":"string","description":"The bundle ID."},"currency":{"type":"string","description":"The currency code (ISO 4217)."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"The subscription expiration date. None if not a subscription."},"order_id":{"type":"string","format":"uuid","description":"The transaction ID."},"original_order_id":{"type":"string","format":"uuid","description":"The original transaction ID."},"original_purchase_date":{"type":"string","format":"date-time","description":"The original purchase date."},"price":{"type":"integer","format":"int64","description":"The price in the smallest currency unit."},"product_id":{"type":"string","description":"The product ID."},"product_slug":{"type":"string","nullable":true,"description":"The product slug."},"product_type":{"$ref":"#/components/schemas/InAppProductType"},"purchase_date":{"type":"string","format":"date-time","description":"The purchase or renewal date."},"quantity":{"type":"integer","minimum":1,"description":"The quantity."},"revocation_date":{"type":"string","format":"date-time","nullable":true,"description":"The revocation date. None if not revoked."},"revocation_reason":{"$ref":"#/components/schemas/RevocationReason","nullable":true},"fetched_at":{"type":"string","format":"date-time","description":"The time of the data fetch."},"subscription_id":{"type":"string","nullable":true,"description":"The subscription group ID. None if not a subscription."},"transaction_reason":{"$ref":"#/components/schemas/TransactionReason"},"user_country":{"type":"string","minLength":2,"maxLength":2,"description":"The user country (ISO 3166-1 alpha-2)."}},"required":["bundle_id","currency","order_id","original_order_id","original_purchase_date","price","product_id","product_type","purchase_date","quantity","fetched_at","transaction_reason","user_country"]},"InAppProductType":{"type":"string","enum":["NON_CONSUMABLE","CONSUMABLE","SUBSCRIPTION"],"description":"The type of in-app product."},"RevocationReason":{"type":"string","enum":["Refund"],"description":"Transaction revocation reason."},"TransactionReason":{"type":"string","enum":["PURCHASE","RENEWAL","REFUND","CHARGEBACK"],"description":"The reason for the transaction."}}}}
```

## The InAppPurchase object

```json
{"openapi":"3.0.3","info":{"title":"Onside Purchase Validation API","version":"1.0.0"},"components":{"schemas":{"InAppPurchase":{"type":"object","properties":{"app_account_token":{"type":"string","nullable":true,"description":"The user account ID."},"bundle_id":{"type":"string","description":"The bundle ID."},"currency":{"type":"string","description":"The currency code (ISO 4217)."},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"The subscription expiration date. None if not a subscription."},"order_id":{"type":"string","format":"uuid","description":"The transaction ID."},"original_order_id":{"type":"string","format":"uuid","description":"The original transaction ID."},"original_purchase_date":{"type":"string","format":"date-time","description":"The original purchase date."},"price":{"type":"integer","format":"int64","description":"The price in the smallest currency unit."},"product_id":{"type":"string","description":"The product ID."},"product_slug":{"type":"string","nullable":true,"description":"The product slug."},"product_type":{"$ref":"#/components/schemas/InAppProductType"},"purchase_date":{"type":"string","format":"date-time","description":"The purchase or renewal date."},"quantity":{"type":"integer","minimum":1,"description":"The quantity."},"revocation_date":{"type":"string","format":"date-time","nullable":true,"description":"The revocation date. None if not revoked."},"revocation_reason":{"$ref":"#/components/schemas/RevocationReason","nullable":true},"fetched_at":{"type":"string","format":"date-time","description":"The time of the data fetch."},"subscription_id":{"type":"string","nullable":true,"description":"The subscription group ID. None if not a subscription."},"transaction_reason":{"$ref":"#/components/schemas/TransactionReason"},"user_country":{"type":"string","minLength":2,"maxLength":2,"description":"The user country (ISO 3166-1 alpha-2)."}},"required":["bundle_id","currency","order_id","original_order_id","original_purchase_date","price","product_id","product_type","purchase_date","quantity","fetched_at","transaction_reason","user_country"]},"InAppProductType":{"type":"string","enum":["NON_CONSUMABLE","CONSUMABLE","SUBSCRIPTION"],"description":"The type of in-app product."},"RevocationReason":{"type":"string","enum":["Refund"],"description":"Transaction revocation reason."},"TransactionReason":{"type":"string","enum":["PURCHASE","RENEWAL","REFUND","CHARGEBACK"],"description":"The reason for the transaction."}}}}
```

## The InAppProductType object

```json
{"openapi":"3.0.3","info":{"title":"Onside Purchase Validation API","version":"1.0.0"},"components":{"schemas":{"InAppProductType":{"type":"string","enum":["NON_CONSUMABLE","CONSUMABLE","SUBSCRIPTION"],"description":"The type of in-app product."}}}}
```

## The TransactionReason object

```json
{"openapi":"3.0.3","info":{"title":"Onside Purchase Validation API","version":"1.0.0"},"components":{"schemas":{"TransactionReason":{"type":"string","enum":["PURCHASE","RENEWAL","REFUND","CHARGEBACK"],"description":"The reason for the transaction."}}}}
```

## The RevocationReason object

```json
{"openapi":"3.0.3","info":{"title":"Onside Purchase Validation API","version":"1.0.0"},"components":{"schemas":{"RevocationReason":{"type":"string","enum":["Refund"],"description":"Transaction revocation reason."}}}}
```


# The Onside Delegate

Customize OnsideKit with OnsideDelegate — host window scene, window level, per-screen theme override, login routing, and a pre-login region hint.

`OnsideDelegate` lets you tune how OnsideKit presents its screens and routes login. Every method has a default implementation, so implement only the ones you need.

```swift
protocol OnsideDelegate: AnyObject {
    @MainActor func onside(hostWindowSceneForScreen screen: OnsideScreen) -> UIWindowScene?
    @MainActor func onside(uiWindowLevelOverrideForScreen screen: OnsideScreen) -> UIWindow.Level?
    @MainActor func onside(uiThemeOverrideForScreen screen: OnsideScreen) -> OnsideUIThemeMode?
    @MainActor func onsideShouldForceLocalLoginMethods() -> Bool
    @MainActor func onsideDefaultCountryCodeAssumption() -> String?
}
```

Assign your delegate (keep a strong reference — it's held weakly), typically at launch:

```swift
@main
final class AppDelegate: UIResponder, UIApplicationDelegate, OnsideDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        Onside.initialize()
        Onside.delegate = self
        return true
    }
}
```

## OnsideScreen

Several delegate methods receive the screen OnsideKit is about to present:

```swift
enum OnsideScreen {
    case login                   // the user authentication screen
    case paymentMethodsManager   // the saved-cards management screen
    case profileInfoFulfillment  // a step that collects/confirms profile info (e.g. region) during a purchase
    case purchase                // the purchase flow
}
```

## Host window scene

```swift
func onside(hostWindowSceneForScreen screen: OnsideScreen) -> UIWindowScene?
```

OnsideKit presents each screen in its own `UIWindow`, created in the scene this method returns — useful in multi-window apps. If you return `nil` (the default), OnsideKit picks a connected scene itself, preferring a `.foregroundActive` one, then `.foregroundInactive`, then any connected window scene.

```swift
func onside(hostWindowSceneForScreen screen: OnsideScreen) -> UIWindowScene? {
    return preferredScene   // or nil to use the default
}
```

{% hint style="warning" %}
If no connected window scene is available, OnsideKit cannot present its UI and the screen is not shown.
{% endhint %}

## Window level

```swift
func onside(uiWindowLevelOverrideForScreen screen: OnsideScreen) -> UIWindow.Level?
```

Set the `windowLevel` of the window OnsideKit presents the screen in. Return `nil` (the default) to let OnsideKit choose it, which is what most apps want:

* If the scene has no visible window above `UIWindow.Level.normal`, the SDK window stays at `.normal` — the behaviour apps have always had.
* Otherwise the SDK window is placed one level above the topmost visible window in the scene, so its UI is not covered by your elevated windows.
* The automatic level never goes above `UIWindow.Level.alert - 1`, leaving system alerts and the keyboard on top. Windows far above the alert level (system-owned ones, such as the keyboard window) are ignored when picking the level.

Return an explicit level to take over — for example to keep the SDK **below** a window of yours that must always stay on top:

```swift
func onside(uiWindowLevelOverrideForScreen screen: OnsideScreen) -> UIWindow.Level? {
    screen == .purchase ? .normal : nil
}
```

An explicit level is applied as-is, without the automatic elevation, so it is also the way to place the SDK window above a window of yours sitting at `UIWindow.Level.alert` or higher.

{% hint style="info" %}
The level is resolved once, when the screen is presented. If your app raises a higher window *while* an OnsideKit screen is on screen, that window will cover it — return an explicit level in that case.
{% endhint %}

When the screen is dismissed, OnsideKit removes its window and restores key-window status to the window that held it before the screen was presented.

## Per-screen theme override

```swift
func onside(uiThemeOverrideForScreen screen: OnsideScreen) -> OnsideUIThemeMode?
```

Force a theme for a specific screen, overriding the [global theme](/sdk/customization/appearance) and the system appearance:

* `.light` / `.dark` — force that theme
* `.auto` — follow the system setting
* `nil` (default) — no override; fall back to the global theme, then the system theme

```swift
func onside(uiThemeOverrideForScreen screen: OnsideScreen) -> OnsideUIThemeMode? {
    screen == .login ? .dark : nil
}
```

## Force in-SDK login

```swift
func onsideShouldForceLocalLoginMethods() -> Bool
```

By default OnsideKit prefers the app-to-app login via the Onside store app. Return `true` to always use OnsideKit's own in-app login screen instead. It is purely a UI-routing switch — it doesn't change which credentials are accepted. Default: `false`. See [Authentication & User Account](/sdk/core-concepts/authentication).

## Pre-login region hint

```swift
func onsideDefaultCountryCodeAssumption() -> String?
```

Provide an ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"DE"`) to use as the region **before** the user logs in, improving logged-out pricing and availability. Return `nil` (default) to use the device region. Once the user logs in, the account's region is used and this hint is ignored. See [Regions & Storefronts](/sdk/products-and-subscriptions/regions-and-storefronts).


# Appearance & Theming

Apply a consistent global theme to all screens presented by OnsideKit using OnsideAppearance.

`OnsideAppearance` sets global styling for every screen OnsideKit presents. Access the shared instance via `Onside.appearance()`.

## Set the global theme

```swift
@MainActor func setThemeMode(_ themeMode: OnsideUIThemeMode?)
```

```swift
Onside.appearance().setThemeMode(.dark)
```

`OnsideUIThemeMode`:

* `.auto` — follow the user's system appearance (Light/Dark)
* `.light` — force light
* `.dark` — force dark
* `nil` — reset the theme setting (does not affect already-presented UI)

The best place to set this is at launch:

```swift
func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    Onside.initialize()
    Onside.appearance().setThemeMode(.dark)   // force dark for all OnsideKit screens
    return true
}
```

## Theme resolution

When OnsideKit presents a screen, the theme is resolved in this order:

1. A per-screen override from [`OnsideDelegate.onside(uiThemeOverrideForScreen:)`](/sdk/customization/delegate#per-screen-theme-override), if provided.
2. The global theme set here via `setThemeMode(_:)`.
3. The system appearance.

{% hint style="info" %}
Theme mode is currently the only globally configurable appearance setting.
{% endhint %}


# Attribution

Retrieve install attribution metadata from OnsideKit, including the referring browser URL tied to a prior web session.

Attribution tells you where a user came from before installing your app. OnsideKit links the install to a prior browser session and returns a `refererUrl` describing the source page.

### Requirements

Call `Onside.initialize()` as early as possible at app launch, before any other OnsideKit API. The earlier you initialize, the sooner the install can be correlated with the originating session. See [Initializing the SDK](/sdk/getting-started/initialization#id-3-initialize-the-sdk).

### Getting attribution metadata

```swift
@MainActor static func getAttributionMetadata(
    completion: @escaping @MainActor (Result<OnsideAttributionMetadata, OnsideAttributionMetadataError>) -> Void
)
```

`OnsideAttributionMetadata` contains a single optional field:

```swift
struct OnsideAttributionMetadata {
    var refererUrl: URL?
}
```

* `refererUrl != nil` — the install was attributed to a browser session. The value is the full URL loaded in the user's browser when the install started (typically your landing page), including any ad-network query parameters such as UTM tags and click IDs.
* `refererUrl == nil` — the install was organic, or attribution wasn't possible.

{% hint style="info" %}
The result is fetched once over the network and then cached on device, and concurrent calls are de-duplicated — so you can call `getAttributionMetadata` whenever convenient without extra cost.
{% endhint %}

#### Example

```swift
import UIKit
import OnsideKit

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Call as early as possible for reliable attribution.
        Onside.initialize()

        Onside.getAttributionMetadata { result in
            switch result {
            case .success(let metadata):
                if let refererUrl = metadata.refererUrl {
                    print("Onside refererUrl: \(refererUrl)")
                    // Send refererUrl to your analytics / backend if needed.
                } else {
                    print("No refererUrl. Organic install or attribution unavailable.")
                }

            case .failure(let error):
                print("Failed to fetch attribution metadata: \(error)")
            }
        }

        return true
    }
}
```

### Errors

`OnsideAttributionMetadataError`:

<table><thead><tr><th width="240">Case</th><th>Cause</th><th>Suggested handling</th></tr></thead><tbody><tr><td><code>.connectionError</code></td><td>Network failure.</td><td>Retry later.</td></tr><tr><td><code>.serviceUnavailable</code></td><td>Server returned 5xx.</td><td>Retry later.</td></tr><tr><td><code>.appNotRegistered</code></td><td>The app/install isn't recognized by Onside (HTTP 404).</td><td>Check your app registration/configuration.</td></tr><tr><td><code>.internalError</code></td><td>Parsing or other unexpected error.</td><td>Report if persistent.</td></tr></tbody></table>

{% hint style="info" %}
Attribution is also available from web content through the [JS ↔ Native Bridge](/sdk/integrations/js-bridge) as `getAttributionMetadata()`.
{% endhint %}

### Next

* [Building funnels with event tracking](/sdk/attribution-and-analytics/building-funnels-with-event-tracking) — track what users do after they arrive


# Building funnels with event tracking

Track user actions with Onside.track and build funnels over these events.

Attribution answers “where did the user come from?”.

See [Attribution](/sdk/attribution-and-analytics/attribution).

Event tracking answers “what did the user do next?”.

Use `Onside.track(_:parameters:)` to log user actions. You can then build funnels over these events.

### API

```swift
Onside.track(
    _ event: OnsideEvent,
    parameters: [OnsideEventParameter: OnsideEventParameterValue]
)
```

Pass `parameters: [:]` if you do not need any metadata.

### Events

`OnsideEvent` ships with a set of common event constants. You can also send any custom event name.

* Use `.levelAchieved`, `.purchaseCompleted`, `.registrationCompleted`, and other predefined cases when they match your action.
* Use `.custom("...")` or a string literal for everything else.

`OnsideEvent` is `ExpressibleByStringLiteral`, so this works:

```swift
Onside.track("my_custom_event", parameters: [:])
```

<details>

<summary>Predefined <code>OnsideEvent</code> cases</summary>

```swift
public enum OnsideEvent: Sendable, ExpressibleByStringLiteral {
    case levelAchieved
    case purchaseCompleted
    case registrationCompleted
    case tutorialCompleted
    case subscriptionStarted
    case subscriptionRenewed
    case subscriptionCancelled
    case profileUpdated
    case contentViewed
    case contentShared
    case searchPerformed
    case addedToCart
    case checkoutStarted
    case paymentInfoEntered
    case custom(String)
}
```

</details>

### Parameters

`OnsideEventParameter` is the key type for event metadata.

It behaves like events:

* It includes a prepared set of common parameter keys.
* It does not limit your API. You can always use a custom key.
* It is `ExpressibleByStringLiteral`.

`OnsideEventParameterValue` supports:

* `String`, `Double`, `Int`, `Bool`
* arrays of these types

<details>

<summary>Full list of predefined <code>OnsideEventParameter</code> keys</summary>

```swift
public enum OnsideEventParameter: Sendable, Hashable, ExpressibleByStringLiteral {
    case achievementId
    case level
    case score
    case success
    case price
    case contentType
    case contentId
    case contentList
    case currency
    case quantity
    case registrationMethod
    case paymentInfoAvailable
    case maxRatingValue
    case ratingValue
    case searchString
    case dateA
    case dateB
    case destinationA
    case destinationB
    case description
    case `class`
    case eventStart
    case eventEnd
    case lat
    case long
    case customerUserId
    case validated
    case revenue
    case projectedRevenue
    case receiptId
    case tutorialId
    case virtualCurrencyName
    case deepLink
    case oldVersion
    case newVersion
    case reviewText
    case couponCode
    case orderId
    case param1
    case param2
    case param3
    case param4
    case param5
    case param6
    case param7
    case param8
    case param9
    case param10
    case departingDepartureDate
    case returningDepartureDate
    case destinationList
    case city
    case region
    case country
    case departingArrivalDate
    case returningArrivalDate
    case suggestedDestinations
    case travelStart
    case travelEnd
    case numAdults
    case numChildren
    case numInfants
    case suggestedHotels
    case userScore
    case hotelScore
    case purchaseCurrency
    case preferredStarRatings
    case preferredPriceRange
    case preferredNeighborhoods
    case preferredNumStops
    case custom(String)
}
```

</details>

### Validation rules

OnsideKit validates events and parameters before sending them.

{% hint style="warning" %}
**Event names and parameter keys**

* Must match `^[a-zA-Z][a-zA-Z0-9_]*$` — start with a letter; letters, digits, and underscores only.
* Are truncated to **40 characters**.
* Must not start with the reserved prefixes `onside_` or `$$`.

A name or key that breaks these rules is **dropped** (a warning is logged to the console): an invalid **event name** drops the whole event; an invalid **parameter key** drops only that parameter.
{% endhint %}

{% hint style="warning" %}
**Parameter values and limits**

* String values are truncated to **100 characters**.
* An event may carry at most **25 effective parameters**; any beyond that are ignored. Array elements each count toward this limit.
* Array values may contain only scalars (`String`, `Double`, `Int`, `Bool`) — no nested arrays.
  {% endhint %}

### Example

```swift
import OnsideKit

// Use predefined event + predefined parameters.
Onside.track(
    .purchaseCompleted,
    parameters: [
        .price: 4.99,
        .currency: "EUR",
        .quantity: 1,
        .validated: true
    ]
)

// Use predefined event + custom parameter key (string literal).
Onside.track(
    .contentViewed,
    parameters: [
        .contentId: "post_123",
        "screen": "home"
    ]
)

// Fully custom event + custom parameters.
Onside.track(
    "my_custom_event",
    parameters: [
        "ab_test_variant": "B",
        "scores": [10, 20, 30]
    ]
)
```


# JS ↔ Native Bridge

Use the Onside JS Bridge to call OnsideKit from JavaScript in a \`WKWebView\`. Covers setup, \`window\.onside\` APIs, callbacks, transactions, and typed errors.

Use the JS <--> Native Bridge to call OnsideKit from JavaScript running inside a `WKWebView`. This integration fits apps built as a web wrapper, where most product logic lives in a website but the app still needs native OnsideKit features such as in-app purchases, attribution, and event tracking.

The bridge is only a transport layer between JavaScript and native code. It does not add new capabilities and does not change the semantics, lifecycle, or error handling of existing OnsideKit APIs. The behavior described in [Installation](/sdk/getting-started/installation), [Authentication & User Account](/sdk/core-concepts/authentication), [Attribution](/sdk/attribution-and-analytics/attribution), [Building funnels with event tracking](/sdk/attribution-and-analytics/building-funnels-with-event-tracking), and [Making a Purchase](/sdk/purchasing/making-a-purchase) still applies. This page focuses on `WKWebView` configuration and the JavaScript API exposed by the bridge.

### Initialize OnsideKit first

Start with the same OnsideKit setup used for a native integration.

Complete the installation, callback URL scheme setup, and SDK initialization exactly as described in [Installation](/sdk/getting-started/installation) and [Initializing the SDK](/sdk/getting-started/initialization).

The only difference is delegate and observer setup.

{% hint style="warning" %}
If you plan to use OnsideKit from JavaScript, do not assign OnsideKit delegates or transaction observers in native code. When the page calls `initializeOnside`, `OnsideJS` installs itself as the delegate and observer layer — and re-installs itself after every page reload, replacing anything native code assigned in the meantime.
{% endhint %}

### Configure WKWebView

After OnsideKit is installed and initialized, configure your `WKWebView` with `OnsideJS.configureOnsideForWebView(_:allowedOrigin:)`.

This is the only required `WKWebView` setup step.

```swift
import WebKit
import OnsideKit

let webView = WKWebView(frame: .zero)

do {
    try OnsideJS.configureOnsideForWebView(
        webView,
        allowedOrigin: URL(string: "https://shop.example.com")
    )
} catch {
    // The bridge is not installed. Do not load content that expects it.
}
```

`allowedOrigin` is the origin your web content is served from. The bridge answers calls only from documents on that origin, and only from the main frame — a third-party `iframe` on the page cannot reach native, and neither can the page after it navigates somewhere else. Only the scheme, host and port are used; any path is ignored.

Passing `nil` disables the origin check and lets **any** document in that web view call the bridge, including `purchase`, `logout` and `getSignedInAppsHistory`. Use it only for local development.

{% hint style="warning" %}
Documents with an opaque origin never receive the bridge. In particular `loadHTMLString(_:baseURL: nil)` will not work — load your content from a real URL, or pass a `baseURL`.
{% endhint %}

`configureOnsideForWebView` throws `OnsideJSSetupError`:

| Case                                 | Meaning                                                                                     |
| ------------------------------------ | ------------------------------------------------------------------------------------------- |
| `notInitialized`                     | `Onside.initialize()` has not been called yet. Initialize the SDK first.                    |
| `invalidAllowedOrigin`               | `allowedOrigin` has no scheme or host, so no origin can be derived from it.                 |
| `alreadyConfiguredForAnotherWebView` | Another `WKWebView` sharing the same `WKWebViewConfiguration` already installed the bridge. |
| `internalError`                      | Unexpected failure inside the SDK.                                                          |

### Load your web content

After configuration, use the `WKWebView` as usual and load your web content in the normal way.

If configuration succeeds, JavaScript running inside the page gets access to `window.onside`.

Before you call any bridge method, initialize the JS API version your code supports.

Call `window.onside.initializeOnside('v1')` during your JS bootstrap. It returns a `Promise` that you must **await** before any other `window.onside` method or callback.

`'v1'` declares that your client code supports JS Bridge API v1. This is currently the only supported version.

```js
await window.onside.initializeOnside('v1');
```

{% hint style="warning" %}
Await `window.onside.initializeOnside('v1')` before calling any other `window.onside` method or assigning any callback. Until it resolves, no other `window.onside.*` method exists — calling one throws a `TypeError` synchronously instead of returning a rejected promise.
{% endhint %}

`initializeOnside` is idempotent, so calling it again is safe. Call it on every page load: after a full page navigation or reload the JavaScript context is new, and your bootstrap has to run again. Native state — the payment queue, the signed-in user — survives the reload, so use `getTransactions()` afterwards to rebuild your UI.

{% hint style="info" %}
Properties named `_pontoon*` on `window.onside` are reserved by the bridge. Do not assign them.
{% endhint %}

After initialization, you can call methods and assign callbacks on `window.onside`.

#### Conventions

* **Every method returns a `Promise`.** There are no synchronous methods — the setters included. A setter resolves with an empty object on success and rejects if you pass it something it cannot decode, so attach a `.catch` even to `setAppearance` and `trackEvent`.
* **Two kinds of rejection.** A *domain error* is the native error enum, serialized as `{ <caseName>: {} }` — discriminate with `'caseName' in error`. A *bridge error* means the call never reached native logic at all (malformed argument, unknown function, a frame the origin policy refuses); it arrives as a real `Error` with `name === 'PontoonBridgeError'` and a stable `code`.

```js
try {
  const { products, invalidProductIdentifiers } = await window.onside.loadProducts({
    productIdentifiers: ['my.product.id'],
  });
} catch (error) {
  if (error instanceof Error) {
    // Bridge error — a bug in the call itself, or a refused document.
    console.error(error.name, error.code);
  } else if ('connectionError' in error) {
    // Domain error — retry, show a message, ...
  }
}
```

Bridge error codes: `invalidArguments`, `functionNotDefined`, `disallowedFrame`, `brokenRequest`, `parametersNotRepresentable`, `cannotSerializeResponse`, `bridgeDetached`.

### API Reference — v1

This reference describes JS Bridge API version `v1`.

#### Configuration

**`initializeOnside('v1')`**

```js
window.onside.initializeOnside('v1');
```

Initialize the JS bridge API. Call this once before any other `window.onside` method or callback.

The argument declares the API version your JS client supports. Currently only `'v1'` is supported.

**`setAppearance(theme)`**

```ts
setAppearance(theme: 'light' | 'dark' | 'system'): Promise<void>;
```

Override the theme used for native UI presented from JS (login, payment methods, etc.). `'system'` follows the OS-level appearance.

**`setShouldForceLocalLoginMethods(force)`**

```ts
setShouldForceLocalLoginMethods(force: boolean): Promise<void>;
```

When `true`, the login screen exposes only local login methods (SMS / e-mail) and hides federated providers. Useful for environments where federated login is not desired.

**`setDefaultCountryCodeAssumption(countryCode)`**

```ts
setDefaultCountryCodeAssumption(countryCode: string): Promise<void>;
```

Default country code assumed when the system region is unknown. Pass an empty string to clear the assumption.

#### Analytics

**`trackEvent(input)`**

```ts
trackEvent(input: {
  name: string;
  parameters?: Record<string, JSEventParameterValue>;
}): Promise<void>;
```

Send a single analytics event. `name` may be a predefined Onside event (e.g. `purchaseCompleted`, `checkoutStarted`) or any custom identifier. Validation is enforced natively:

* name and parameter keys must match `^[a-zA-Z][a-zA-Z0-9_]*$` and be ≤ 40 chars (longer names are truncated; reserved prefixes `onside_` and `$$` are rejected);
* string values longer than 100 chars are truncated;
* at most 25 **effective** parameters per event. Every element of an array parameter consumes one slot, and an empty array still costs one. An array that would exceed the remaining budget is truncated to fit, and once the budget is exhausted the remaining parameters are dropped. Parameters are processed in alphabetical order of their keys, so which ones survive depends on the key names, not on insertion order.

{% hint style="warning" %}
Validation failures are silent: an invalid parameter key drops that parameter, an invalid event **name** drops the whole event, and the promise resolves successfully either way. Verify your event names and keys during development.
{% endhint %}

A parameter value the bridge cannot represent — `null`, `NaN`, an object, or a nested array — is a different matter: it rejects the whole call with a bridge error, and nothing is tracked.

#### Auth

**`requestLogin()`**

```ts
requestLogin(): Promise<void>;
```

Show the login flow. Resolves on successful authentication. Rejects with `OnsideLoginError`.

**`logout()`**

```ts
logout(): Promise<void>;
```

Log out the current user.

#### UI

**`presentPaymentMethodsManager()`**

```ts
presentPaymentMethodsManager(): Promise<void>;
```

Show the payment-methods management screen. Triggers a login flow first if the user is not authenticated. Rejects with `OnsidePaymentMethodsManagerError`.

#### Storefront

**`getCurrentStorefront()`**

```ts
getCurrentStorefront(): Promise<JSStorefront | null>;
```

Currently selected storefront, or `null` if none is selected yet. The selection can change at any time — subscribe to `onStorefrontChanged` for updates.

#### Products

**`loadProducts(input)`**

```ts
loadProducts(input: { productIdentifiers: string[] }): Promise<JSProductsResponse>;
```

Load products by identifiers. The response contains:

* `products` — products that resolved successfully;
* `invalidProductIdentifiers` — identifiers that did not resolve (unknown, malformed, etc.).

Returned products are also cached natively so that `purchase` can look them up by `productIdentifier`. The cache is dropped whenever the storefront changes — including on login and logout — because prices and availability may differ. After `onStorefrontChanged` fires, call `loadProducts` again before the next `purchase`, or it will reject with `unknownProduct`.

Rejects with `OnsideProductsRequestError`.

#### Payments

**`purchase(input)`**

```ts
purchase(input: {
  productIdentifier: string;
  appAccountToken?: string;
}): Promise<void>;
```

Initiate a purchase. The product identifier must refer to a product previously returned by `loadProducts`; otherwise the promise rejects with `{ unknownProduct: {} }`.

The promise itself only reports the **pre-flight** outcome (login outcome, lookup, etc.). Subsequent transaction state changes are delivered via `onTransactionsUpdated`; JS is responsible for calling `finishTransaction` once a transaction reaches a terminal state.

Rejects with `JSPurchaseError`.

**`restoreCompletedTransactions()`**

```ts
restoreCompletedTransactions(): Promise<void>;
```

Restore previously completed transactions. The promise only reports the pre-flight outcome (e.g. `loginDiscarded`). The completion of the restore flow itself is delivered via `onRestoreCompletedTransactionsFinished` or `onRestoreCompletedTransactionsFailedWithError`. Restored transactions arrive through `onTransactionsUpdated`.

Rejects with `OnsidePaymentQueueRequestRestoreError`.

**`getTransactions()`**

```ts
getTransactions(): Promise<JSPaymentTransaction[]>;
```

Snapshot of currently alive transactions in the payment queue. Useful on page reload to rebuild UI state without waiting for the next event.

**`finishTransaction(id)`**

```ts
finishTransaction(id: string): Promise<void>;
```

Finish a transaction by its opaque id (the `id` field of `JSPaymentTransaction`). `onTransactionsRemoved` fires once the transaction leaves the queue.

Removal is immediate only for a transaction that has not been sent for payment yet. For `purchased`, `restored` and `failed` the transaction is marked for completion and removed once that completes, so `onTransactionsRemoved` may arrive noticeably later.

{% hint style="info" %}
A transaction that is `purchasing` because payment is already in flight cannot be finished. The promise still resolves, but nothing happens. Wait for a terminal state before calling `finishTransaction`.
{% endhint %}

Rejects with `JSFinishTransactionError`.

#### Attribution

**`getAttributionMetadata()`**

```ts
getAttributionMetadata(): Promise<JSAttributionMetadata>;
```

Fetch attribution metadata for the current install/user (e.g. the referer URL that brought the user to the install).

Rejects with `OnsideAttributionMetadataError`.

#### History

**`getSignedInAppsHistory()`**

```ts
getSignedInAppsHistory(): Promise<JSSignedInAppsHistory>;
```

Fetch the signed in-apps history JWT for the current user. The response carries both the raw bytes (base64-encoded) and a UTF-8 representation, when applicable.

Rejects with `OnsideSignedInAppsHistoryRequestError`.

### Native → JS Callbacks

Assign a function to the corresponding property on `window.onside` to start receiving the event. All callbacks are optional and a missing handler is a no-op — except for the two gates below, where native falls back to approving the transaction.

The origin policy applies to this direction too: callbacks are delivered only while the web view is showing a document on the allowed origin. If the page navigates away — to a payment provider, or to an external link opened in place — native stops delivering them, so a transaction that changes state meanwhile produces no callback at all. Call `getTransactions()` once you are back to reconcile.

{% hint style="warning" %}
Native does not serialize callbacks. Each event is dispatched as soon as it happens, so a handler that returns a `Promise` may still be running when the next callback fires. Handlers are invoked in the order the events occurred, but they can **finish** in any order — if yours mutate shared state, guard against overlap yourself.
{% endhint %}

**`onTransactionsUpdated(transactions)`**

```ts
onTransactionsUpdated(transactions: JSPaymentTransaction[]): void | Promise<void>;
```

Transactions added to the queue or transitioned to a new state. Inspect `transactionState` and react accordingly. Once the state reaches `purchased`, `restored` or `failed`, JS must call `finishTransaction` to remove it from the queue.

**`onTransactionsRemoved(transactions)`**

```ts
onTransactionsRemoved(transactions: JSPaymentTransaction[]): void | Promise<void>;
```

Transactions that have been removed from the queue (typically after `finishTransaction`).

**`onStorefrontChanged(storefront)`**

```ts
onStorefrontChanged(storefront: JSStorefront | null): void | Promise<void>;
```

Storefront selection changed; `null` means the storefront is no longer available.

**`onRestoreCompletedTransactionsFinished()`**

```ts
onRestoreCompletedTransactionsFinished(): void | Promise<void>;
```

The `restoreCompletedTransactions` flow finished successfully.

**`onRestoreCompletedTransactionsFailedWithError(error)`**

```ts
onRestoreCompletedTransactionsFailedWithError(
  error: OnsideTransactionsRestoreError
): void | Promise<void>;
```

The `restoreCompletedTransactions` flow failed.

**`shouldContinueTransaction(input)`**

```ts
shouldContinueTransaction(input: {
  transaction: JSPaymentTransaction;
  storefront: JSStorefront;
}): boolean | Promise<boolean>;
```

Gate the queue invokes when a queued transaction is about to execute in a storefront **different** from the one it was enqueued in — typically because the user logged into an account registered in another country, where the price or availability differs. Same-storefront purchases never call it. This is the JavaScript counterpart of the native delegate described in [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes).

Return `false` to discard the transaction. It is then removed from the queue and reported through `onTransactionsRemoved` — it does **not** reach `failed`, and the `purchase` promise that created it has already resolved by then.

**`shouldContinueTransactionAfterLogin(input)`**

```ts
shouldContinueTransactionAfterLogin?(input: {
  transaction: JSPaymentTransaction;
}): boolean | Promise<boolean>;
```

Gate the queue invokes when `purchase` had to log the user in first. It fires after the login succeeds and before the transaction is enqueued, so the transaction passed here is not in the queue yet.

Return `false` to drop the purchase — `purchase` then rejects with `rejectedAfterLogin`, and no transaction callback is delivered for that attempt.

{% hint style="warning" %}
Both gates default to `true` in three cases: no handler registered, the handler threw, and the handler returned anything that is not a boolean. A handler that forgets to `return` therefore approves the purchase.
{% endhint %}

### Domain Types

**`JSPrice`**

```ts
{ value: number; currencyCode: string }
```

Numeric monetary value with an ISO-4217 currency code (e.g. `"EUR"`).

**`JSPeriod`**

```ts
{ unit: 'day' | 'week' | 'month' | 'year'; numberOfUnits: number }
```

A length of time — analogous to StoreKit's `SKProductSubscriptionPeriod`. Used for the subscription period and for the duration of introductory and discounted prices.

**`JSPricePeriod`**

```ts
{ price: JSPrice; period: JSPeriod }
```

A price that applies for the given period — an introductory or discounted price.

**`JSProduct`**

```ts
{
  productIdentifier: string;
  localizedTitle: string;
  localizedDescription: string;
  iconUrl?: string;
  subscriptionGroupIdentifier?: string;
  subscriptionPeriod?: JSPeriod;
  price: JSPrice;
  introductoryPrice?: JSPricePeriod;
  discountedPrice?: JSPricePeriod;
}
```

A product as returned by `loadProducts`. `subscriptionGroupIdentifier`, `subscriptionPeriod`, `introductoryPrice` and `discountedPrice` are present only for subscriptions.

{% hint style="info" %}
Optional fields are **omitted** from the payload rather than sent as `null`, so `'iconUrl' in product` is `false` when a product has no icon.
{% endhint %}

**`JSStorefront`**

```ts
{ id: string; countryCode: string }
```

The selected storefront — opaque storefront `id` plus its ISO-3166 `countryCode`.

{% hint style="warning" %}
`id` is stable only for the lifetime of the app process; it is regenerated on every launch. Never persist it or compare it across sessions — use `countryCode` for anything durable.
{% endhint %}

**`JSPayment`**

```ts
{ product: JSProduct; appAccountToken?: string }
```

The payment that produced a transaction, embedded inside `JSPaymentTransaction.payment`. `appAccountToken` is the optional opaque token passed to `purchase`.

**`JSPaymentTransactionState`**

```ts
'purchasing' | 'purchased' | 'restored' | 'failed'
```

Lifecycle states of a transaction:

| State        | Meaning                                                                      |
| ------------ | ---------------------------------------------------------------------------- |
| `purchasing` | Still in flight: created, awaiting payment, etc.                             |
| `purchased`  | Completed successfully. Call `finishTransaction(id)`.                        |
| `restored`   | Returned by `restoreCompletedTransactions`. Call `finishTransaction(id)`.    |
| `failed`     | Final, failed. `error` is populated. Call `finishTransaction(id)` to remove. |

**`JSPaymentTransaction`**

```ts
{
  id: string;
  transactionIdentifier?: string;
  originalTransactionIdentifier?: string;
  payment: JSPayment;
  transactionState: JSPaymentTransactionState;
  storefront: JSStorefront;
  error?: OnsidePaymentTransactionError;
}
```

* `id` — opaque UUID string. The only stable handle for this transaction; pass it to `finishTransaction`.
* `transactionIdentifier` — server-side order id. Absent until payment has been sent.
* `originalTransactionIdentifier` — currently always equal to `transactionIdentifier`. Reserved for a future renewal chain; do not group renewals by it yet.
* `error` — populated only for `failed` transactions.

**`JSAttributionMetadata`**

```ts
{ refererUrl?: string }
```

Currently exposes only the referer URL. May be extended in the future.

**`JSSignedInAppsHistory`**

```ts
{ dataBase64: string; string?: string }
```

* `dataBase64` — raw payload bytes, base64-encoded.
* `string` — UTF-8 decoded payload, when applicable (the payload is typically a JWT and decodes to a printable string).

**`JSProductsResponse`**

```ts
{ products: JSProduct[]; invalidProductIdentifiers: string[] }
```

Result of `loadProducts`.

**`JSEventParameterValue`**

```ts
string | number | boolean | JSEventArrayParameterValue[]
```

Value type for `trackEvent` parameters: a primitive or an array of primitives. Array elements (`JSEventArrayParameterValue`) follow the same primitive set without further nesting.

### Errors

All native `enum: Error` cases without associated values arrive on the JS side as `{ <caseName>: {} }`. The bridge also defines a few JS-specific error types listed at the end.

Rejections that never reached native logic are not in this list — they arrive as an `Error` with `name === 'PontoonBridgeError'` and a `code`, as described under [Conventions](#conventions).

**`OnsideLoginError`**

| Case             | Meaning                                                                                |
| ---------------- | -------------------------------------------------------------------------------------- |
| `loginDiscarded` | The login flow did not complete — the user cancelled it, or it could not be presented. |

**`OnsideProductsRequestError`**

| Case                       | Meaning                                        |
| -------------------------- | ---------------------------------------------- |
| `cancelled`                | The request was cancelled.                     |
| `connectionError`          | Network connectivity error.                    |
| `appNotRegistered`         | The current app is not registered with Onside. |
| `invalidProductIdentifier` | Server rejected the product identifier(s).     |
| `serviceUnavailable`       | Server returned a 5xx.                         |
| `internalError`            | Other unexpected error.                        |

**`OnsideSignedInAppsHistoryRequestError`**

| Case                         | Meaning                                                     |
| ---------------------------- | ----------------------------------------------------------- |
| `notLoggedIn`                | Operation requires an authenticated user.                   |
| `notSupportedInLocalTesting` | Not supported when running with a local-testing storefront. |
| `cancelled`                  | The request was cancelled.                                  |
| `connectionError`            | Network connectivity error.                                 |
| `appNotRegistered`           | The current app is not registered with Onside.              |
| `serviceUnavailable`         | Server returned a 5xx.                                      |
| `internalError`              | Other unexpected error.                                     |

**`OnsidePaymentMethodsManagerError`**

| Case                         | Meaning                                                                            |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| `loginDiscarded`             | The implicit login did not complete — dismissed, or not presentable.               |
| `notSupportedInLocalTesting` | Not supported when running with a local-testing storefront.                        |
| `presentationFailed`         | OnsideKit couldn't present the screen (no active window scene); nothing was shown. |

**`OnsidePaymentQueueRequestRestoreError`**

| Case             | Meaning                                                              |
| ---------------- | -------------------------------------------------------------------- |
| `loginDiscarded` | The implicit login did not complete — dismissed, or not presentable. |

**`OnsideTransactionsRestoreError`**

| Case                 | Meaning                                        |
| -------------------- | ---------------------------------------------- |
| `cancelled`          | The restore was cancelled.                     |
| `connectionError`    | Network connectivity error.                    |
| `appNotRegistered`   | The current app is not registered with Onside. |
| `serviceUnavailable` | Server returned a 5xx.                         |
| `internalError`      | Other unexpected error.                        |

**`OnsidePaymentTransactionError`**

Populated on `JSPaymentTransaction.error` for `failed` transactions.

| Case                 | Meaning                                                                                |
| -------------------- | -------------------------------------------------------------------------------------- |
| `cancelled`          | The transaction was cancelled.                                                         |
| `presentationFailed` | OnsideKit couldn't present the purchase UI (no active window scene); it never started. |

**`OnsideAttributionMetadataError`**

| Case                 | Meaning                                        |
| -------------------- | ---------------------------------------------- |
| `connectionError`    | Network connectivity error.                    |
| `appNotRegistered`   | The current app is not registered with Onside. |
| `serviceUnavailable` | Server returned a 5xx.                         |
| `internalError`      | Other unexpected error.                        |

**`JSPurchaseError`**

JS-bridge specific error for `purchase`.

| Case                 | Meaning                                                                       |
| -------------------- | ----------------------------------------------------------------------------- |
| `unknownProduct`     | The `productIdentifier` is not in the local cache. Call `loadProducts` first. |
| `loginDiscarded`     | The implicit login did not complete — dismissed, or not presentable.          |
| `rejectedAfterLogin` | `shouldContinueTransactionAfterLogin` returned `false`.                       |

**`JSFinishTransactionError`**

JS-bridge specific error for `finishTransaction`.

| Case                 | Meaning                                                 |
| -------------------- | ------------------------------------------------------- |
| `unknownTransaction` | No transaction with the given `id` exists in the queue. |

### Reference

The full machine-readable contract is available in [`onside.d.ts`](https://github.com/onside-io/OnsideKit-iOS/blob/main/onside.d.ts).


# Unity

Integrate OnsideKit into a Unity game targeting iOS with the OnsideKit Unity package — a C# wrapper over the native SDK.

The **OnsideKit Unity** package lets you use OnsideKit from C# in a Unity game that ships to iOS. It is a thin wrapper over the native OnsideKit iOS SDK: your C# calls are forwarded to the native SDK, and native events are delivered back to C#. The same concepts apply — products, the payment queue, transactions, login, attribution, and event tracking — so the [Core Concepts](/sdk/core-concepts/payment-queue) and [Error Reference](/sdk/reference/errors) pages are good companion reading.

* **Package:** `io.onside.onsidekit-unity`
* **Namespace:** `OnsideKit`
* **Requires:** Unity 2022.1+, an iOS build target

{% hint style="info" %}
The native bridge exists only in an **iOS device/simulator build**. In the Unity Editor (and on non-iOS platforms) the API calls are no-ops, so you test the actual purchase flow by building and running on iOS.
{% endhint %}

## 1. Install the package

Download `OnsideKit-Unity-<version>.unitypackage` from the [Releases of the OnsideKit-iOS repository](https://github.com/onside-io/OnsideKit-iOS/releases). In Unity, choose **Assets → Import Package → Custom Package**, select the file, and click **Import All**.

The External Dependency Manager (EDM4U) is bundled in the package, and the native OnsideKit SDK is pulled in automatically when you build for iOS — you don't need to add any scoped registries or extra dependencies.

## 2. Create the settings asset

In Unity: **Assets → Create → OnsideKit → Settings**.

This creates `Assets/OnsideKit/Resources/OnsideKitSettings.asset`. Set:

* **Callback Scheme** *(required)* — your app's unique URL scheme (e.g. `myapp-onside`). It must match the scheme you pass to `Onside.Initialize(...)`, and the **iOS build fails if it's empty**. The Onside Store app uses it to return to your game after login/payment.
* **StoreKit Configuration Path** *(optional)* — path to a `.storekit` file for offline [local testing](/sdk/advanced-and-tooling/local-testing). The file is copied into the generated Xcode project.

## 3. Write your integration

```csharp
using System.Collections.Generic;
using OnsideKit;
using UnityEngine;

public class StoreManager : MonoBehaviour, IOnsidePaymentTransactionObserver
{
    void Start()
    {
        // 1. Initialize once. Use the same scheme as in OnsideKitSettings.
        Onside.Initialize("myapp-onside");

        // 2. Subscribe to product events and register as a transaction observer.
        var queue = Onside.DefaultPaymentQueue;
        queue.ProductsReceived += OnProductsReceived;
        queue.ProductsRequestFailed += error => Debug.LogError($"Products failed: {error}");
        // The only signal that an Add(payment) was rejected before a transaction
        // was created (e.g. the user dismissed login).
        queue.AddPaymentFailed += (productId, error) =>
            Debug.LogWarning($"Purchase rejected: {productId} — {error}");
        queue.Add(this);

        // 3. Fetch products.
        queue.RequestProducts(new List<string> { "premium_feature", "remove_ads" });
    }

    void OnProductsReceived(IList<OnsideProduct> products, IList<string> invalidIdentifiers)
    {
        foreach (var p in products)
            Debug.Log($"{p.localizedTitle} — {p.price.value} {p.price.currencyCode}");
    }

    // Start a purchase (e.g. from a Buy button).
    public void Buy(string productIdentifier)
    {
        Onside.DefaultPaymentQueue.Add(new OnsidePayment(productIdentifier));
    }

    // --- IOnsidePaymentTransactionObserver ---

    public void OnsidePaymentQueueUpdatedTransactions(
        OnsidePaymentQueue queue, IList<OnsidePaymentTransaction> transactions)
    {
        foreach (var t in transactions)
        {
            switch (t.State)
            {
                case OnsidePaymentTransactionState.Purchased:
                case OnsidePaymentTransactionState.Restored:
                    UnlockContent(t.productIdentifier);
                    queue.FinishTransaction(t);   // required
                    break;
                case OnsidePaymentTransactionState.Failed:
                    Debug.LogWarning($"Purchase failed: {t.Error}");
                    queue.FinishTransaction(t);   // required
                    break;
                case OnsidePaymentTransactionState.Purchasing:
                    break;   // in progress — wait
            }
        }
    }

    void UnlockContent(string productIdentifier) { /* grant + persist */ }

    public void OnsidePaymentQueueRemovedTransactions(
        OnsidePaymentQueue queue, IList<OnsidePaymentTransaction> transactions) { }
    public void OnsidePaymentQueueRestoreCompletedTransactionsFinished(
        OnsidePaymentQueue queue) { }
    public void OnsidePaymentQueueRestoreCompletedTransactionsFailed(
        OnsidePaymentQueue queue, OnsideTransactionsRestoreError error) { }
    public void OnsidePaymentQueueDidChangeStorefront(
        OnsidePaymentQueue queue) { }
}
```

### Key rules

{% hint style="warning" %}

* **Callbacks run on Unity's main thread** — you can safely touch Unity APIs inside them.
* **Finish every transaction** — call `FinishTransaction` for each `Purchased`, `Restored`, or `Failed` transaction, or it is re-delivered on the next launch.
* **`AddPaymentFailed` is the only signal** that an `Add(payment)` was rejected before a transaction was created — the login did not complete, `ShouldContinueAfterLogin` returned `false`, or the product is not loaded. The transaction observer is **not** called in that case.
* **Request products again after `StorefrontChanged`** — loaded products are cached natively so a purchase can find them by identifier, and that cache is dropped whenever the storefront changes, including on login and logout, because prices and availability may differ. Until you call `RequestProducts` again, `AddPayment` fails with `ProductNotLoaded`.
* **The Editor is a no-op** — methods and callbacks run only in an on-device iOS build (outside it you get a warning and callbacks don't fire). Test on a device/simulator.
* **`ShouldContinueHandler` must return quickly** — the native purchase flow blocks waiting for its answer.
  {% endhint %}

## 4. Build for iOS

1. **File → Build Settings → iOS → Switch Platform**, then **Build**.
2. A post-process build step runs automatically and configures the generated Xcode project: it embeds `OnsideKit.framework`, adds `onside` to `LSApplicationQueriesSchemes`, registers your callback scheme under `CFBundleURLTypes`, copies your `.storekit` file (if set), and applies the required Swift/build settings.
3. Open the generated `.xcodeproj` and run on a device (or the simulator).

You don't need to edit the Xcode project or `Info.plist` by hand, and you don't implement any URL handling yourself — incoming URLs are caught automatically by the bundled app controller.

## How it works

Your C# calls are marshaled to the native SDK; native events are delivered back to a persistent `OnsideKit` GameObject (created by `Initialize`) via `UnitySendMessage` and surfaced as C# events/observer callbacks.

```
Onside.Initialize("myapp-onside")
  → creates the persistent "OnsideKit" GameObject
  → native [Onside initialize] + callbackScheme + delegate/observer wiring

DefaultPaymentQueue.RequestProducts(ids)
  → native OnsideProductsRequest → products serialized to JSON
  → C# OnsidePaymentQueue.ProductsReceived fires

DefaultPaymentQueue.Add(payment)
  → native purchase flow (may open the Onside Store app)
  → Store app returns via myapp-onside:// → native handleURL
  → transaction updates → IOnsidePaymentTransactionObserver
```

***

## API Reference

### `Onside` (static)

```csharp
static void Initialize(string callbackScheme, string storeKitConfigurationName = null)
```

Initializes the SDK and creates the persistent `OnsideKit` GameObject. Call once at startup, before any other call. Idempotent — a second call is ignored. `storeKitConfigurationName` enables offline [local testing](/sdk/advanced-and-tooling/local-testing).

<table><thead><tr><th width="430">Member</th><th>Description</th></tr></thead><tbody><tr><td><code>OnsidePaymentQueue DefaultPaymentQueue { get; }</code></td><td>The shared payment queue.</td></tr><tr><td><code>void RequestLogin(Action onSuccess = null, Action&#x3C;OnsideLoginError> onFailed = null)</code></td><td>Present the login flow. Login is also triggered on demand by a purchase or by presenting the payment methods manager.</td></tr><tr><td><code>void Logout()</code></td><td>End the current session.</td></tr><tr><td><code>void SetThemeMode(OnsideUIThemeMode mode)</code></td><td>Global theme: <code>Auto</code>, <code>Light</code>, or <code>Dark</code>.</td></tr><tr><td><code>void SetDelegateOptions(string countryCodeHint = null, bool forceLocalLoginMethods = false)</code></td><td>Pre-login region hint and force the in-app login screen. See <a href="/pages/5oGGH6dyxvl0lcEMuSqx">The Onside Delegate</a>.</td></tr><tr><td><code>void SetApplePayMerchantIdentifier(string merchantIdentifier)</code></td><td>Configure Apple Pay. No-op if the SDK build has no Apple Pay. See <a href="/pages/d68KNbPlPOh4ZwOy2bDc">Apple Pay</a>.</td></tr><tr><td><code>void PresentPaymentMethodsManager(Action onSuccess = null, Action&#x3C;OnsidePaymentMethodsManagerError> onFailed = null)</code></td><td>Show the saved-cards manager.</td></tr><tr><td><code>void GetAttributionMetadata(Action&#x3C;OnsideAttributionMetadata> onSuccess = null, Action&#x3C;OnsideAttributionMetadataError> onFailed = null)</code></td><td>Fetch <a href="/pages/CTU1ZKPvzowOnIDPO1rq">attribution</a> metadata.</td></tr><tr><td><code>void Track(string eventName, Dictionary&#x3C;string, object> parameters = null)</code></td><td>Send an analytics event. See <a href="/pages/hwXMpbVJlmfOrR46RUTt">Event tracking</a>.</td></tr><tr><td><code>void ResetLocalTestingState()</code></td><td>Reset local-testing purchase history.</td></tr></tbody></table>

**Signed in-app purchase history** — subscribe to the static events, then request:

```csharp
static event Action<OnsideSignedInAppsHistory> SignedInAppsHistoryReceived;
static event Action<OnsideSignedInAppsHistoryRequestError> SignedInAppsHistoryFailed;
static event Action SignedInAppsHistoryFinished;

static void RequestSignedInAppsHistory();
static void StopSignedInAppsHistoryRequest();
```

See [Signed In-App Purchase History](/sdk/purchase-validation/signed-in-apps-history).

**Installation id** — for support/diagnostics (see [Debugging](/sdk/advanced-and-tooling/debugging)):

```csharp
static string InstallationId { get; }
static event Action<string> InstallationIdChanged;   // may deliver null while loading
static void SubscribeInstallationId();               // call once to start receiving
```

### `OnsidePaymentQueue`

Accessed via `Onside.DefaultPaymentQueue`.

```csharp
OnsideStorefront Storefront { get; }                  // null until logged in
IList<OnsidePaymentTransaction> Transactions { get; } // snapshot of current transactions

event Action<IList<OnsideProduct>, IList<string>> ProductsReceived;  // (products, invalidIdentifiers)
event Action<OnsideProductsRequestError> ProductsRequestFailed;
event Action ProductsRequestFinished;
event Action<string, OnsidePaymentQueueAddProductError> AddPaymentFailed;  // (productIdentifier, error)

// Optional storefront/price-change gate. Return false to cancel a transaction
// whose storefront changed. Keep it fast; if null, transactions always continue.
Func<OnsidePaymentTransaction, OnsideStorefront, bool> ShouldContinueHandler;

void RequestProducts(IList<string> productIdentifiers);
void Add(OnsidePayment payment);
void FinishTransaction(OnsidePaymentTransaction transaction);
void RestoreCompletedTransactions(Action<bool> completion = null);

void Add(IOnsidePaymentTransactionObserver observer);     // replays current transactions
void Remove(IOnsidePaymentTransactionObserver observer);
```

The `ShouldContinueHandler` is the Unity equivalent of the native storefront safety gate — see [Handling Storefront & Price Changes](/sdk/purchasing/storefront-price-changes).

### `IOnsidePaymentTransactionObserver`

<table><thead><tr><th width="430">Method</th><th>When</th></tr></thead><tbody><tr><td><code>OnsidePaymentQueueUpdatedTransactions(queue, transactions)</code></td><td>Transactions added or changed state. Finish each completed one.</td></tr><tr><td><code>OnsidePaymentQueueRemovedTransactions(queue, transactions)</code></td><td>Transactions removed (after finishing).</td></tr><tr><td><code>OnsidePaymentQueueRestoreCompletedTransactionsFinished(queue)</code></td><td>A restore finished successfully.</td></tr><tr><td><code>OnsidePaymentQueueRestoreCompletedTransactionsFailed(queue, error)</code></td><td>A restore failed.</td></tr><tr><td><code>OnsidePaymentQueueDidChangeStorefront(queue)</code></td><td>The storefront changed (login/logout/region).</td></tr></tbody></table>

### Types

```csharp
class OnsidePayment {
    string productIdentifier { get; }
    string appAccountToken { get; }   // optional, ties the purchase to your account
    OnsidePayment(string productIdentifier, string appAccountToken = null);
}

enum OnsidePaymentTransactionState { Unknown = -1, Purchasing = 0, Purchased = 1, Restored = 2, Failed = 3 }

class OnsidePaymentTransaction {
    string id;
    string transactionIdentifier;
    string originalTransactionIdentifier;
    string productIdentifier;
    string appAccountToken;
    OnsideStorefront storefront;
    OnsidePaymentTransactionState State { get; }
    OnsidePaymentTransactionError? Error { get; }   // set only when Failed
}

class OnsideProduct {
    string productIdentifier;
    string localizedTitle;
    string localizedDescription;
    OnsidePrice price;          // value (double) + currencyCode (string)
    string iconUrl;
    string subscriptionGroupIdentifier;   // subscriptions only; null otherwise
    OnsidePeriod subscriptionPeriod;      // subscriptions only; null for one-time products
}

class OnsidePeriod { string unit; long value; }   // unit: "day" | "week" | "month" | "year"

class OnsideStorefront { string id; string countryCode; }

class OnsideAttributionMetadata { string refererUrl; }

class OnsideSignedInAppsHistory {
    string Text { get; }    // decoded UTF-8 payload
    byte[] Bytes { get; }   // decoded raw bytes
}

enum OnsideUIThemeMode { Auto = 0, Light = 1, Dark = 2 }
```

The subscription fields on `OnsideProduct` (the billing period and group) carry the same meaning as in the native SDK — see [Subscriptions](/sdk/products-and-subscriptions/subscriptions).

### Errors

All error enums include an `Unknown` fallback. Notable cases:

<table><thead><tr><th width="360">Enum</th><th>Cases</th></tr></thead><tbody><tr><td><code>OnsideProductsRequestError</code></td><td><code>Cancelled</code>, <code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>InvalidProductIdentifier</code>, <code>ServiceUnavailable</code>, <code>InternalError</code></td></tr><tr><td><code>OnsidePaymentQueueAddProductError</code></td><td><code>LoginDiscarded</code>, <code>RejectedAfterLogin</code>, <code>ProductNotLoaded</code></td></tr><tr><td><code>OnsideTransactionsRestoreError</code></td><td><code>Cancelled</code>, <code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>ServiceUnavailable</code>, <code>InternalError</code>, <code>LoginDiscarded</code></td></tr><tr><td><code>OnsidePaymentTransactionError</code></td><td><code>Cancelled</code>, <code>PresentationFailed</code></td></tr><tr><td><code>OnsideLoginError</code></td><td><code>LoginDiscarded</code>, <code>RequestAlreadyInProgress</code></td></tr><tr><td><code>OnsidePaymentMethodsManagerError</code></td><td><code>LoginDiscarded</code>, <code>NotSupportedInLocalTesting</code>, <code>PresentationFailed</code>, <code>RequestAlreadyInProgress</code></td></tr><tr><td><code>OnsideAttributionMetadataError</code></td><td><code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>ServiceUnavailable</code>, <code>InternalError</code>, <code>RequestAlreadyInProgress</code></td></tr><tr><td><code>OnsideSignedInAppsHistoryRequestError</code></td><td><code>NotLoggedIn</code>, <code>NotSupportedInLocalTesting</code>, <code>Cancelled</code>, <code>ConnectionError</code>, <code>AppNotRegistered</code>, <code>ServiceUnavailable</code>, <code>InternalError</code></td></tr></tbody></table>

The meaning of each case matches the native SDK — see the [Error Reference](/sdk/reference/errors). The `RequestAlreadyInProgress` cases are Unity-specific: they're returned when you start a one-shot request (login, payment methods, attribution) while a previous one is still pending.

## Common issues

<table><thead><tr><th width="320">Issue</th><th>Fix</th></tr></thead><tbody><tr><td>Callback scheme doesn't work</td><td>Ensure the <strong>Callback Scheme</strong> in <code>OnsideKitSettings</code> matches the string passed to <code>Onside.Initialize()</code>.</td></tr><tr><td>Products request returns nothing</td><td>Check that your product identifiers match what's configured in the Onside Developer Console.</td></tr><tr><td>Nothing happens in the Editor</td><td>Expected — the native bridge only runs in an iOS build. Test on a device/simulator.</td></tr></tbody></table>


# Local Testing with a .storekit File

Develop and QA the purchase and restore flows offline with a bundled StoreKit configuration file, without a backend or a real account.

Local testing runs OnsideKit fully offline against a bundled **`.storekit`** configuration file. Products come from the file, login is automatic and fake, and purchases and restores run against a local simulation — no backend, no registered app, and no real Onside account.

## Enable it

1. Add a StoreKit configuration file to your app target — for example `LocalProducts.storekit`. (Xcode can create one via **File → New → File → StoreKit Configuration File**.)
2. Pass its **base name** (no extension) to `initialize`:

```swift
Onside.initialize(storeKitConfigurationName: "LocalProducts")
```

When `storeKitConfigurationName` is `nil` (the default), the SDK runs normally against the live backend. The choice is fixed at the first `initialize` call.

On success the SDK logs:

```
[Onside]: ✅ Initialized for local testing with N product(s) from LocalProducts.storekit
```

## How it behaves

* **Products** come from the `.storekit` file. [`makeProductsRequest`](/sdk/products-and-subscriptions/fetching-products) returns the configured products, matched by identifier.
* **Login is automatic and offline** — the SDK commits a synthetic session with no phone verification or store-app handoff. `requestLogin` "succeeds" instantly, and the storefront country is the one you set in the file (see `_storefront` below). `logout()` still clears it.
* **Purchases** present a fake Apple-Pay-style sheet (no real charge) and complete the transaction through the normal [payment queue](/sdk/core-concepts/payment-queue). Restorable products (non-consumables and subscriptions) are remembered locally; re-buying one shows an "already purchased" alert.
* **Restore** replays the locally remembered purchases.

{% hint style="info" %}
The standard OnsideKit APIs are unchanged — the same queue, observers, and transaction flow. This is the recommended way to exercise the purchase UI, including the [storefront/price-change gate](/sdk/purchasing/storefront-price-changes), without a backend.
{% endhint %}

### Not supported in local testing

Two APIs require a real account and return a dedicated error:

* `Onside.presentPaymentMethodsManager(completion:)` → `.notSupportedInLocalTesting`
* `Onside.makeSignedInAppsHistoryRequest()` → `.notSupportedInLocalTesting`

## Reset between runs

```swift
Onside.resetLocalTestingState()
```

This clears the locally remembered purchase history so you can simulate a fresh install — restorable products are forgotten and re-buying one shows the full payment sheet again. It does not clear the fake login or the parsed products.

## The .storekit file

OnsideKit reads a subset of Apple's StoreKit configuration format.

```json
{
  "products": [
    {
      "productID": "remove_ads",
      "referenceName": "Remove Ads",
      "type": "NonConsumable",
      "displayPrice": "4.99",
      "localizations": [
        { "locale": "en_US", "displayName": "Remove Ads", "description": "Removes all ads." }
      ]
    }
  ],
  "subscriptionGroups": [
    {
      "id": "21F5C0E3",
      "name": "Premium",
      "localizations": [
        { "locale": "en_US", "displayName": "Premium", "description": "" }
      ],
      "subscriptions": [
        {
          "productID": "subscription_monthly",
          "referenceName": "Premium Monthly",
          "type": "RecurringSubscription",
          "displayPrice": "9.99",
          "recurringSubscriptionPeriod": "P1M",
          "localizations": [
            { "locale": "en_US", "displayName": "Premium Monthly", "description": "Monthly premium." }
          ]
        }
      ]
    }
  ],
  "settings": {
    "_locale": "en_US",
    "_storefront": "US"
  }
}
```

**Supported fields**

* `products[]`: `productID`, `referenceName`, `type` (`Consumable` → consumable; anything else → non-consumable), `displayPrice`, `localizations[]`.
* `subscriptionGroups[]`: `id`, `name`, `localizations[]`, `subscriptions[]`.
* `subscriptions[]`: `productID`, `referenceName`, `type`, `displayPrice`, `recurringSubscriptionPeriod` (ISO-8601 duration, e.g. `P1M`), `localizations[]`.
* `localizations[]`: `locale`, `displayName`, `description`.
* `settings`: `_locale` (preferred localization, default `en_US`) and `_storefront` (the testing storefront/country code — set this explicitly).

{% hint style="warning" %}
In local testing, all prices render in **EUR** — only the numeric value comes from `displayPrice`. Set `_storefront` to the ISO country code you want to test; the storefront country drives region-dependent behavior.
{% endhint %}


# Testing Installation & Purchases

Test an OnsideKit integration during development before your app is notarized and live: simulate the purchase flow with a local StoreKit file, and simulate the alternative-marketplace install source i

Testing an OnsideKit integration during development comes down to two independent pieces, and neither one needs a production backend or a live install:

1. **The purchase flow** — fetching products, buying, and restoring.
2. **The install source** — how your app reports the marketplace it was installed from, when you distribute through an alternative app marketplace.

This page shows how to simulate both in Xcode so you can exercise your integration end to end before going live.

## 1. Testing the purchase flow

OnsideKit can run fully offline against a bundled **`.storekit`** configuration file. Products, login, purchases, and restores are all simulated locally — no backend, no registered app, and no real Onside account. This is the recommended way to develop and QA the purchase UI.

Pass the `.storekit` file's base name to `initialize`:

```swift
Onside.initialize(storeKitConfigurationName: "LocalProducts")
```

The standard OnsideKit APIs are unchanged — the same payment queue, observers, and transaction flow — so what you test here matches what ships.

{% hint style="info" %}
Local testing is documented in full — setup, behavior, the supported `.storekit` fields, the APIs that aren't available offline, and resetting state between runs — in [**Local Testing with a .storekit File**](/sdk/advanced-and-tooling/local-testing).
{% endhint %}

## 2. Testing the install source

When you distribute your app through an alternative app marketplace, your app — and some Apple frameworks — can branch on **how the app was installed**. MarketplaceKit exposes this through [`AppDistributor`](https://developer.apple.com/documentation/marketplacekit/appdistributor) (iOS 17.4+):

```swift
import MarketplaceKit

switch AppDistributor.current {
case .appStore:                  break
case .testFlight:                break
case .marketplace(let bundleID): break // installed from an alternative marketplace
case .web:                       break
case .other:                     break
@unknown default:                break
}
```

`AppDistributor.current` reports the source the app installed from:

| `AppDistributor.current` | Install source                                                            |
| ------------------------ | ------------------------------------------------------------------------- |
| `appStore`               | The App Store.                                                            |
| `testFlight`             | TestFlight.                                                               |
| `marketplace(_:)`        | An alternative app marketplace; the associated `String` is its bundle ID. |
| `web`                    | The developer's website.                                                  |
| `other`                  | Enterprise or education developer programs.                               |

{% hint style="info" %}
This is a property of **your app**, not of OnsideKit. OnsideKit's product, purchase, and restore APIs behave identically regardless of `AppDistributor.current`. The technique below is for testing the code in your app that reacts to the install source.
{% endhint %}

### Simulate a marketplace install during development

Your app can install from an alternative marketplace only after it passes **Notarization**, so before that you can't get a real marketplace install on device. To test any code that branches on `AppDistributor.current` beforehand, override the install source for development builds.

{% hint style="info" %}
This section summarizes Apple's [Distributing your app on an alternative marketplace → Test your app during development](https://developer.apple.com/documentation/marketplacekit/distributing-your-app-on-an-alternative-marketplace#Test-your-app-during-development). Refer to Apple's documentation for the authoritative, up-to-date steps.
{% endhint %}

**1. Declare the marketplaces in your build settings.**

Set your target's **Alternative Distribution - Marketplaces** build setting — identifier `MARKETPLACES` — to the list of marketplace bundle IDs your app can install from. The **Onside marketplace** bundle ID is `com.onside.marketplace-app`. Open the project in **Xcode 15.3 or later** and add the build setting manually if one by that title isn't already present.

```
MARKETPLACES = com.onside.marketplace-app
```

This build setting overrides `AppDistributor.current` for development builds, so you can test any custom branching — a different image, different menu items, and so on — without a notarized build.

**2. Choose the marketplace in your run scheme.**

In **Product → Scheme → Edit Scheme… → Run → Options** tab, use the **Distribution** menu to pick that marketplace's bundle ID. Runs on device through Xcode then simulate an install from that marketplace.

With `com.onside.marketplace-app` selected, `AppDistributor.current` returns:

```swift
.marketplace("com.onside.marketplace-app")
```

**3. Override it in a test plan (optional).**

To exercise the same branching from automated tests, choose a marketplace bundle ID in the **Distribution** menu of your **test plan's configuration**. This overrides the distributor for that test plan.

{% hint style="warning" %}
These settings only affect development builds. They simulate the install source so you can test locally — they don't change how a released build is distributed, and a production install reports its real source.
{% endhint %}

## See also

* [Local Testing with a .storekit File](/sdk/advanced-and-tooling/local-testing) — the full reference for offline purchase testing.
* [OnsideKit vs OnsideKitLite](/sdk/advanced-and-tooling/onsidekit-lite) — the two SDK builds, and which one to test against.
* [Debugging & installationId](/sdk/advanced-and-tooling/debugging) — logs and identifiers for diagnosing an integration.
* Apple — [`AppDistributor`](https://developer.apple.com/documentation/marketplacekit/appdistributor) and [Distributing your app on an alternative marketplace](https://developer.apple.com/documentation/marketplacekit/distributing-your-app-on-an-alternative-marketplace).


# Debugging & installationId

Diagnostics for OnsideKit: the installationId publisher, SDK console logging, and the SSL-pinning debug switch.

## installationId

`Onside.installationId` publishes the SDK's server-issued, anonymous install identifier. It's the value to include in support tickets and to correlate a device with server-side records.

```swift
static let installationId: AnyPublisher<String?, Never>
```

It emits `nil` while the id is loading (or on error) and a non-`nil` `String` once resolved. Subscribe with Combine:

```swift
import Combine
import OnsideKit

var cancellables = Set<AnyCancellable>()

Onside.installationId
    .compactMap { $0 }
    .sink { id in
        print("Onside installation id: \(id)")
    }
    .store(in: &cancellables)
```

OnsideKit attaches this id to its requests automatically — you don't need to send it yourself.

## Console logging

OnsideKit prints diagnostic messages prefixed with `[Onside]:`. Watch for them during development:

* **Used before initialization** — a warning that an API was called before `Onside.initialize()`.
* **Event validation** — warnings when an event name or parameter is truncated or dropped. See [Building funnels with event tracking](/sdk/attribution-and-analytics/building-funnels-with-event-tracking#validation-rules).
* **Local testing** — confirmation that a `.storekit` file was loaded, or an error if it couldn't be parsed. See [Local Testing](/sdk/advanced-and-tooling/local-testing).

## Disabling SSL pinning (debug only)

To inspect SDK network traffic through a TLS-intercepting proxy (e.g. Charles or Proxyman), disable certificate pinning at initialization:

```swift
Onside.initialize(disableSSLPinning: true)
```

{% hint style="danger" %}
`disableSSLPinning` is a debugging aid only. The setting is persisted across launches, and disabling pinning weakens transport security — **never ship it enabled**. Leave it `false` (the default) in production builds.
{% endhint %}


# OnsideKit vs OnsideKitLite

OnsideKitLite is the SDK without the PassKit dependency (no Apple Pay), for apps that cannot include PassKit.

OnsideKit ships as two products, built from the same source:

<table><thead><tr><th width="200">Product</th><th>Apple Pay / PassKit</th><th>Use it when</th></tr></thead><tbody><tr><td><strong>OnsideKit</strong></td><td>Included</td><td>The default. Use it unless you have a reason not to.</td></tr><tr><td><strong>OnsideKitLite</strong></td><td>Excluded</td><td>Your app cannot include the PassKit framework.</td></tr></tbody></table>

The two are **identical apart from Apple Pay**. Bank-card payments, login, products, the payment queue, attribution, event tracking, and the JS bridge are all present in both.

## What changes in Lite

* **No Apple Pay.** The purchase and payment-methods UI offer bank cards only — the Apple Pay option never appears.
* **No `applePayMerchantIdentifier`.** This API exists only in the full build. Referencing `Onside.applePayMerchantIdentifier` in an app that links OnsideKitLite is a **compile error**.

Everything else — the `Onside` API, models, delegates, and observers — is the same.

## Choosing and importing

The capability is fixed by which product you add as a dependency; there is no runtime or compile flag in your app. Add **OnsideKit** or **OnsideKitLite** as described in [Installation](/sdk/getting-started/installation), then import the matching module:

```swift
import OnsideKit        // full build
// or
import OnsideKitLite    // no PassKit / no Apple Pay
```

In both cases you use the same `Onside` API (minus `applePayMerchantIdentifier` in Lite).

{% hint style="info" %}
If you need Apple Pay later, switch the dependency to the full **OnsideKit** product and configure it as described in [Apple Pay](/sdk/purchasing/apple-pay).
{% endhint %}


# Migrating from StoreKit

Map StoreKit types and patterns to their OnsideKit equivalents.

OnsideKit is modeled on StoreKit's original (`SKPaymentQueue`-based) API, so most concepts map directly. This page lists the equivalents and the differences worth knowing.

## Type mapping

<table><thead><tr><th width="340">StoreKit</th><th>OnsideKit</th></tr></thead><tbody><tr><td><code>SKPaymentQueue.default()</code></td><td><code>Onside.defaultPaymentQueue()</code></td></tr><tr><td><code>SKPayment</code></td><td><code>OnsidePayment</code></td></tr><tr><td><code>SKPaymentTransaction</code></td><td><code>OnsidePaymentTransaction</code></td></tr><tr><td><code>SKPaymentTransactionState</code></td><td><code>OnsidePaymentTransactionState</code></td></tr><tr><td><code>SKPaymentTransactionObserver</code></td><td><code>OnsidePaymentTransactionObserver</code></td></tr><tr><td><code>SKProductsRequest</code></td><td><code>OnsideProductsRequest</code> (via <code>Onside.makeProductsRequest(productIdentifiers:)</code>)</td></tr><tr><td><code>SKProductsRequestDelegate</code></td><td><code>OnsideProductsRequestDelegate</code></td></tr><tr><td><code>SKProduct</code></td><td><code>OnsideProduct</code></td></tr><tr><td><code>SKProductSubscriptionPeriod</code></td><td><code>OnsidePeriod</code></td></tr><tr><td><code>finishTransaction(_:)</code></td><td><code>finishTransaction(_:)</code></td></tr><tr><td><code>restoreCompletedTransactions()</code></td><td><code>restoreCompletedTransactions(completion:)</code></td></tr></tbody></table>

## Key differences

* **Queue accessor.** Use `Onside.defaultPaymentQueue()` — not `SKPaymentQueue.default()`, and not `Onside.paymentQueue()`.
* **Completion arguments are required.** `add(_:completion:)` and `restoreCompletedTransactions(completion:)` take a non-defaulted `completion` (pass a closure or `nil`). The completion reports only the pre-flight outcome (e.g. `.loginDiscarded`); transaction updates still arrive via the observer.
* **Login is built in.** OnsideKit presents login on demand when a purchase/restore needs it — there's no separate StoreKit-style account check. See [Authentication & User Account](/sdk/core-concepts/authentication).
* **Product identifiers are Onside slugs.** `OnsideProduct.productIdentifier` is the identifier you configure in the Onside console and request with — not an App Store SKU.
* **`appAccountToken` via mutation.** `OnsidePayment`'s only initializer is `init(product:)`; set `appAccountToken` by mutating the value (`var payment = OnsidePayment(product:); payment.appAccountToken = ...`).
* **Resilient enums.** OnsideKit enums are non-frozen; include `@unknown default` in `switch` statements. See [Threading & Object Lifetime](/sdk/core-concepts/threading-and-retention).
* **Storefront safety gate.** When the storefront/price changes for a queued purchase, OnsideKit asks your [`OnsidePaymentQueueDelegate`](/sdk/purchasing/storefront-price-changes) before continuing.


# Models

Reference for OnsideKit's public value types: transactions, payments, products, prices and periods.

OnsideKit's public models are value types (`Sendable`, safe to pass across actors).

## OnsidePaymentTransaction

A snapshot of a transaction in the [payment queue](/sdk/core-concepts/payment-queue).

<table><thead><tr><th width="320">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>id: UUID</code></td><td>Stable local identifier for this transaction.</td></tr><tr><td><code>transactionIdentifier: String?</code></td><td>Server-side transaction id (may be absent while <code>.purchasing</code>).</td></tr><tr><td><code>originalTransactionIdentifier: String?</code></td><td>Id of the original transaction (for restored/renewed cases).</td></tr><tr><td><code>payment: OnsidePayment</code></td><td>The payment that produced this transaction.</td></tr><tr><td><code>transactionState: OnsidePaymentTransactionState</code></td><td>Current state.</td></tr><tr><td><code>storefront: OnsideStorefront</code></td><td>The storefront the transaction belongs to.</td></tr><tr><td><code>error: OnsidePaymentTransactionError?</code></td><td>Set only for <code>.failed</code> transactions.</td></tr></tbody></table>

`func isSame(as another: OnsidePaymentTransaction) -> Bool` — compares by `id`. Use it to correlate updates for the same transaction across state changes.

## OnsidePaymentTransactionState

```swift
enum OnsidePaymentTransactionState {
    case purchasing   // in flight
    case purchased    // bought successfully
    case restored     // surfaced by restoreCompletedTransactions
    case failed       // failed; see transaction.error
}
```

## OnsidePayment

```swift
struct OnsidePayment {
    var product: OnsideProduct
    var appAccountToken: String?

    init(product: OnsideProduct)
}
```

`appAccountToken` correlates the purchase with your account system. The only initializer is `init(product:)`; set the token by mutating the value.

## OnsideProduct

<table><thead><tr><th width="320">Property</th><th>Description</th></tr></thead><tbody><tr><td><code>productIdentifier: String</code></td><td>The Onside identifier (slug) you requested.</td></tr><tr><td><code>localizedTitle: String</code></td><td>Display title.</td></tr><tr><td><code>localizedDescription: String</code></td><td>Display description.</td></tr><tr><td><code>iconUrl: URL?</code></td><td>Icon URL, if available.</td></tr><tr><td><code>price: OnsidePrice</code></td><td>Price.</td></tr><tr><td><code>subscriptionPeriod: OnsidePeriod?</code></td><td>Recurring period (subscriptions only).</td></tr><tr><td><code>subscriptionGroupIdentifier: String?</code></td><td>Subscription group (subscriptions only).</td></tr></tbody></table>

See [Subscriptions](/sdk/products-and-subscriptions/subscriptions) for the subscription fields.

## Pricing types

```swift
struct OnsidePrice {
    var value: Double
    var currencyCode: String   // ISO 4217, e.g. "EUR"
}

enum OnsidePeriod {
    case day(UInt)
    case week(UInt)
    case month(UInt)
    case year(UInt)
}
```

## OnsideStorefront

```swift
struct OnsideStorefront {
    var id: String
    var countryCode: String   // the user's region, e.g. "US"
}
```

## OnsideProductsResponse

```swift
struct OnsideProductsResponse {
    var products: [OnsideProduct]
    var invalidProductIdentifiers: [String]   // requested identifiers not found
}
```

## OnsideSignedInAppsHistory

```swift
struct OnsideSignedInAppsHistory {
    var data: Data        // raw JWS blob
    var string: String?   // UTF-8 decoding of `data`, when valid
}
```

## OnsideAttributionMetadata

```swift
struct OnsideAttributionMetadata {
    var refererUrl: URL?
}
```


# Error Reference

Every OnsideKit error type, its cases, what causes them, and how to handle them.

OnsideKit reports failures with small typed enums. Switch over the cases — they are not `LocalizedError`, so `localizedDescription` is generic.

## Common causes

Many cases share a meaning across error types:

<table><thead><tr><th width="260">Case</th><th>Meaning</th></tr></thead><tbody><tr><td><code>loginDiscarded</code></td><td>The user dismissed a required login screen.</td></tr><tr><td><code>cancelled</code></td><td>The operation was cancelled (by the user or <code>stop()</code>).</td></tr><tr><td><code>presentationFailed</code></td><td>OnsideKit couldn't present the purchase UI — no active window scene was available (e.g. the app was backgrounded). The purchase never started; retry once the app is in the foreground.</td></tr><tr><td><code>connectionError</code></td><td>Network failure — usually retryable.</td></tr><tr><td><code>serviceUnavailable</code></td><td>Server returned 5xx — retry later.</td></tr><tr><td><code>appNotRegistered</code></td><td>The app/install isn't recognized by Onside (HTTP 404) — check your app registration/configuration.</td></tr><tr><td><code>invalidProductIdentifier</code></td><td>The products request was rejected (HTTP 422).</td></tr><tr><td><code>internalError</code></td><td>Parsing or other unexpected error.</td></tr><tr><td><code>notLoggedIn</code></td><td>The operation requires an authenticated user.</td></tr><tr><td><code>notSupportedInLocalTesting</code></td><td>Unavailable while running with a <code>.storekit</code> configuration.</td></tr></tbody></table>

## Errors by type

<table><thead><tr><th width="360">Error type</th><th>Cases</th></tr></thead><tbody><tr><td><code>OnsideLoginError</code></td><td><code>loginDiscarded</code></td></tr><tr><td><code>OnsideProductsRequestError</code></td><td><code>cancelled</code>, <code>connectionError</code>, <code>appNotRegistered</code>, <code>invalidProductIdentifier</code>, <code>serviceUnavailable</code>, <code>internalError</code></td></tr><tr><td><code>OnsideSignedInAppsHistoryRequestError</code></td><td><code>notLoggedIn</code>, <code>notSupportedInLocalTesting</code>, <code>cancelled</code>, <code>connectionError</code>, <code>appNotRegistered</code>, <code>serviceUnavailable</code>, <code>internalError</code></td></tr><tr><td><code>OnsidePaymentMethodsManagerError</code></td><td><code>loginDiscarded</code>, <code>notSupportedInLocalTesting</code>, <code>presentationFailed</code></td></tr><tr><td><code>OnsidePaymentQueueAddProductError</code></td><td><code>loginDiscarded</code></td></tr><tr><td><code>OnsidePaymentQueueRequestRestoreError</code></td><td><code>loginDiscarded</code></td></tr><tr><td><code>OnsidePaymentTransactionError</code></td><td><code>cancelled</code>, <code>presentationFailed</code></td></tr><tr><td><code>OnsideTransactionsRestoreError</code></td><td><code>cancelled</code>, <code>connectionError</code>, <code>appNotRegistered</code>, <code>serviceUnavailable</code>, <code>internalError</code></td></tr><tr><td><code>OnsideAttributionMetadataError</code></td><td><code>connectionError</code>, <code>appNotRegistered</code>, <code>serviceUnavailable</code>, <code>internalError</code></td></tr></tbody></table>

## Where each is delivered

* `OnsideLoginError` — `Onside.requestLogin(completion:)`
* `OnsideProductsRequestError` — `OnsideProductsRequestDelegate.onsideProductsRequest(_:didFailWithError:)`
* `OnsideSignedInAppsHistoryRequestError` — `Onside.makeSignedInAppsHistoryRequest()` result and its request delegate
* `OnsidePaymentMethodsManagerError` — `Onside.presentPaymentMethodsManager(completion:)`
* `OnsidePaymentQueueAddProductError` — `OnsidePaymentQueue.add(_:completion:)`
* `OnsidePaymentQueueRequestRestoreError` — `OnsidePaymentQueue.restoreCompletedTransactions(completion:)` (pre-flight)
* `OnsidePaymentTransactionError` — `OnsidePaymentTransaction.error` on a `.failed` transaction
* `OnsideTransactionsRestoreError` — `onsidePaymentQueue(_:restoreCompletedTransactionsFailedWithError:)`
* `OnsideAttributionMetadataError` — `Onside.getAttributionMetadata(completion:)`


# Example App

A sample app showcasing OnsideKit features: login, fetching products, making purchases, and restoring purchases — as a reference implementation.

The sample app is a small, focused project that demonstrates the core OnsideKit flows end to end. It's a good starting point for seeing how the pieces fit together in a real project.

{% hint style="info" %}
[**View the Example App on GitHub**](https://github.com/onside-io/OnsideKit-iOS/tree/main/Example)
{% endhint %}

## What it demonstrates

<table><thead><tr><th width="260">Feature</th><th>API</th></tr></thead><tbody><tr><td>User authentication</td><td><a href="/pages/IDqAGYlWbxSmIjHh88J7"><code>Onside.requestLogin</code></a> / on-demand login</td></tr><tr><td>Fetching products</td><td><a href="/pages/c1zXVakY1NYA1etwIIJj"><code>Onside.makeProductsRequest(productIdentifiers:)</code></a></td></tr><tr><td>Making a purchase</td><td><a href="/pages/HhnXQpRNBjCAhuOmnFSN"><code>Onside.defaultPaymentQueue().add(_:completion:)</code></a></td></tr><tr><td>Restoring purchases</td><td><a href="/pages/ywjXlambhT5vjV5RFgHO"><code>restoreCompletedTransactions(completion:)</code></a></td></tr></tbody></table>

## Required setup

The app won't run correctly until you complete two steps:

1. **Bundle Identifier** — the app's Bundle ID in Xcode must match the one you registered in the [Onside Developer Console](https://developer.onside.io).
2. **Product identifiers** — add your product identifiers in [`ProductsRepository.swift`](https://github.com/onside-io/OnsideKit-iOS/blob/main/Example/OnsideKitExample/ProductsRepository.swift), so the app can fetch your products.

The app also calls `Onside.initialize()` at launch, as every integration must — see [Initializing the SDK](/sdk/getting-started/initialization). It integrates the full **OnsideKit** product; see [OnsideKit vs OnsideKitLite](/sdk/advanced-and-tooling/onsidekit-lite) if you need the PassKit-free build.

Full instructions are in the [Example README](https://github.com/onside-io/OnsideKit-iOS/blob/main/Example/README.md).


# Welcome to the Onside API

An introduction to the Onside API, covering authentication, key concepts, and where to find detailed endpoint references.

Welcome, developer! The Onside API provides programmatic access to manage your apps, automate workflows, and integrate Onside Console features directly into your own tools.

Whether you want to automate new app submissions, sync analytics data with your internal dashboards, or manage your app listings from your CI/CD pipeline, our REST API is here to help.

***

### Exploring the API

Our API is organized into logical sections based on functionality.

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Transactions Reporting API</strong></td><td>Use your payment gateway and tell us about your transactions.</td><td><a href="/pages/PdE7XFbB7aSu8QoS0b2q">/pages/PdE7XFbB7aSu8QoS0b2q</a></td></tr><tr><td><strong>App Management API</strong></td><td>Coming Soon!</td><td></td></tr><tr><td><strong>Analytics API</strong></td><td>Coming Soon!</td><td></td></tr></tbody></table>

***


# Transactions Reporting API

Transactions Reporting API offers our merchant clients increased flexibility in managing their in-app payments.

Use the Transactions Reporting API if you process in-app payments outside the Onside Payment SDK.

You must report settled transactions from apps distributed through the Onside Store. Onside uses these reports to calculate the applicable platform fees.

{% hint style="info" %}
If you use the Onside Payment SDK, you do not need this API.
{% endhint %}

### How it works

1. Process the payment in your own payment stack.
2. Wait until the payment is settled or captured.
3. Upload a transaction report and receive a report ID.
4. Check the report processing status with that report ID.
5. Repeat on a fixed schedule based on your transaction volume.

Use batch uploads. Most merchants report hourly, daily, or weekly.

### Reporting flow

The reporting flow has two steps:

1. Upload the report.
2. Poll or query the processing status with the returned report ID.

Use the report ID as your reference for:

* Delivery tracking
* Retry handling
* Reconciliation

### Duplicate handling

The API tolerates duplicate report submissions. If you submit the same report more than once, Onside returns the same report ID. Duplicate transactions inside submitted reports are ignored. This lets you retry safely when you are unsure whether a previous upload completed successfully.

### OpenAPI reference

Use these references for the exact API contract:

* [Merchant reports transaction](https://app.gitbook.com/o/nvk3UljiUI9NN7xz4cj9/s/vu6GnA9QIMP7iX7RdyRJ/~/edit/~/changes/33/transactions-reporting-api/merchant-reports-transaction) — primary operation for submitting transaction data
* [Transaction report schema](https://store.onside.io/schema/transactions-report.json) — JSON schema for the request payload
* [Transaction report result schema](https://store.onside.io/schema/transactions-report-result.json) — JSON schema for the response payload

### Authentication

Onside provides an API key for this API.

Send it as a Bearer token in the `Authorization` header on every request.

```http
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
```

If you do not have an API key yet, contact your Onside account contact.

### What to report

Report only transactions that meet all of these conditions:

* The app is distributed through the Onside Store
* The payment was processed outside the Onside Payment SDK
* The payment is settled or captured

Do not report:

* Authorized but uncaptured payments
* Refunds
* Chargebacks

If the OpenAPI schema marks a field as required, always send it. Follow the field names, formats, and enum values exactly as defined in the operation reference.

Validate each request body against the [transaction report schema](https://store.onside.io/schema/transactions-report.json) before sending it.

### Reporting schedule and limits

Send transactions in batches.

Choose a reporting cadence that matches your volume:

* Hourly for high-volume apps
* Daily for most apps
* Weekly for low-volume apps

A single report payload must not exceed `5 MB`.

If your batch would exceed that limit, split it into multiple requests.

### Implementation guidance

When you integrate this API:

* Keep your internal transaction ID for reconciliation
* Log every report attempt and response
* Retry failed submissions safely
* Validate your payload against the request schema before sending
* Validate successful responses against the result schema if you persist them

### Best practices

* Report transactions soon after settlement
* Keep a delivery log for audit and support cases
* Monitor for validation errors and fix them at the source
* Store each returned report ID in your internal logs
* Retry with the same report payload when delivery is uncertain

### Need help?

If you are unsure whether a payment flow must be reported, check the operation reference first. If the flow is still unclear, contact Onside before sending production data.


# Merchant::reports::transaction

## get processing result

> Retrieves the processing result of a previously submitted Merchant Transactions Report.\
> \
> The response will be in JSON format, conforming to \[the schema]\(<https://store.onside.io/schema/transactions-report-result.json>).

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"servers":[{"url":"https://store.onside.io/","description":"Production server"},{"url":"https://store.onside.dev/","description":"Development server"}],"paths":{"/merchant-api/v1/reports/transactions":{"get":{"tags":["merchant::reports::transaction"],"summary":"get processing result","description":"Retrieves the processing result of a previously submitted Merchant Transactions Report.\n\nThe response will be in JSON format, conforming to [the schema](https://store.onside.io/schema/transactions-report-result.json).","operationId":"merchant_transactions_report_result","parameters":[{"name":"Authorization","in":"header","description":"Bearer token for authentication","required":true,"schema":{"type":"string"}},{"name":"report_id","in":"query","description":"Unique identifier of Merchant Transactions report.","required":true,"schema":{"type":"integer","format":"int64","minimum":0}}],"responses":{"200":{"description":"Transactions report result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultOfMerchantTransactionsReportProcessing"}}}},"400":{"description":"Bad request"},"401":{"description":"Not authorized, missing or invalid token"},"404":{"description":"Transaction report not found"}}}}},"components":{"schemas":{"ResultOfMerchantTransactionsReportProcessing":{"type":"object","description":"Result of processing of transactions report\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"$id\": \"https://store.onside.io/schema/transactions-report-result.json\",\n  \"title\": \"Result of Merchant Transactions Report processing\",\n  \"description\": \"Result of processing of transactions report\",\n  \"type\": \"object\",\n  \"required\": [\n    \"accepted_at\",\n    \"report_id\",\n    \"transactions\"\n  ],\n  \"properties\": {\n    \"accepted_at\": {\n      \"description\": \"Date and time when the report was accepted\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"completed_at\": {\n      \"description\": \"Date and time when the report was completed\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"processing_at\": {\n      \"description\": \"Date and time when the report processing has started\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"report_id\": {\n      \"description\": \"Unique report identifier\",\n      \"type\": \"string\"\n    },\n    \"transactions\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"invalid\": {\n          \"description\": \"List of invalid transactions\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\n              \"id\",\n              \"reason\"\n            ],\n            \"properties\": {\n              \"id\": {\n                \"description\": \"Unique transaction identifier\",\n                \"type\": \"string\"\n              },\n              \"reason\": {\n                \"description\": \"Reason why the transaction is invalid\",\n                \"type\": \"string\"\n              }\n            }\n          }\n        },\n        \"valid\": {\n          \"description\": \"List of identifiers of valid transactions\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    }\n  }\n}\n ```\n </details>","required":["accepted_at","report_id","transactions"],"properties":{"accepted_at":{"type":"string","format":"date-time","description":"Date and time when the report was accepted"},"completed_at":{"type":["string","null"],"format":"date-time","description":"Date and time when the report was completed"},"processing_at":{"type":["string","null"],"format":"date-time","description":"Date and time when the report processing has started"},"report_id":{"type":"string","description":"Unique report identifier"},"transactions":{"$ref":"#/components/schemas/ResultOfMerchantTransactionsReportProcessingTransactions"}}},"ResultOfMerchantTransactionsReportProcessingTransactions":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactions`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"invalid\": {\n      \"description\": \"List of invalid transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"reason\"\n        ],\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique transaction identifier\",\n            \"type\": \"string\"\n          },\n          \"reason\": {\n            \"description\": \"Reason why the transaction is invalid\",\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"valid\": {\n      \"description\": \"List of identifiers of valid transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    }\n  }\n}\n ```\n </details>","properties":{"invalid":{"type":"array","items":{"$ref":"#/components/schemas/ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem"},"description":"List of invalid transactions"},"valid":{"type":"array","items":{"type":"string"},"description":"List of identifiers of valid transactions"}}},"ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"reason\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"reason\": {\n      \"description\": \"Reason why the transaction is invalid\",\n      \"type\": \"string\"\n    }\n  }\n}\n ```\n </details>","required":["id","reason"],"properties":{"id":{"type":"string","description":"Unique transaction identifier"},"reason":{"type":"string","description":"Reason why the transaction is invalid"}}}}}}
````

## upload a report

> Uploads a Merchant Transaction report.\
> \
> The report must conform to \[the schema]\(<https://store.onside.io/schema/transactions-report.json)\\>
> and the payload size must not exceed 5 Megabytes.\
> \
> Report processing is asynchronous. To check the status of a submitted report,\
> make a GET request to this same endpoint using the \`report\_id\` returned in the\
> response of this POST request.\
> \
> This endpoint is idempotent; submitting the same report content multiple times\
> will result in the same outcome without reprocessing.

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"servers":[{"url":"https://store.onside.io/","description":"Production server"},{"url":"https://store.onside.dev/","description":"Development server"}],"paths":{"/merchant-api/v1/reports/transactions":{"post":{"tags":["merchant::reports::transaction"],"summary":"upload a report","description":"Uploads a Merchant Transaction report.\n\nThe report must conform to [the schema](https://store.onside.io/schema/transactions-report.json)\nand the payload size must not exceed 5 Megabytes.\n\nReport processing is asynchronous. To check the status of a submitted report,\nmake a GET request to this same endpoint using the `report_id` returned in the\nresponse of this POST request.\n\nThis endpoint is idempotent; submitting the same report content multiple times\nwill result in the same outcome without reprocessing.","operationId":"upload_merchant_transactions_report","parameters":[{"name":"Authorization","in":"header","description":"Bearer token for authentication","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MerchantTransactionsReport"}}},"required":true},"responses":{"202":{"description":"Transactions report accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportIdResponse"}}}},"400":{"description":"Missing token"},"401":{"description":"Not authorized, invalid token"},"413":{"description":"Payload size exceeds the limit"},"422":{"description":"Invalid payload format"}}}}},"components":{"schemas":{"MerchantTransactionsReport":{"type":"object","description":"Report of merchant transactions\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"$id\": \"https://store.onside.io/schema/transactions-report.json\",\n  \"title\": \"Merchant Transactions Report\",\n  \"description\": \"Report of merchant transactions\",\n  \"type\": \"object\",\n  \"required\": [\n    \"created_at\",\n    \"transactions\"\n  ],\n  \"properties\": {\n    \"created_at\": {\n      \"description\": \"Date and time when the report was generated in RFC 3339 format\",\n      \"examples\": [\n        \"2025-06-30T16:01:25Z\",\n        \"2025-06-30T03:01:01+02:00\"\n      ],\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"transactions\": {\n      \"description\": \"List of transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\n          \"currency\",\n          \"full_amount\",\n          \"id\",\n          \"payer_country\",\n          \"settled_at\",\n          \"tax_amount\"\n        ],\n        \"properties\": {\n          \"currency\": {\n            \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n            \"examples\": [\n              \"EUR\",\n              \"USD\"\n            ],\n            \"type\": \"string\",\n            \"pattern\": \"^[A-Z]{3}$\"\n          },\n          \"full_amount\": {\n            \"description\": \"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n            \"examples\": [\n              \"12357\",\n              \"99\"\n            ],\n            \"type\": \"integer\"\n          },\n          \"id\": {\n            \"description\": \"Unique transaction identifier\",\n            \"type\": \"string\"\n          },\n          \"payer_country\": {\n            \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n            \"examples\": [\n              \"FRA\",\n              \"ESP\"\n            ],\n            \"type\": \"string\",\n            \"pattern\": \"^[A-Z]{3}$\"\n          },\n          \"settled_at\": {\n            \"description\": \"Date and time when the transaction was settled in RFC 3339 format\",\n            \"examples\": [\n              \"2025-06-12T12:14:25Z\",\n              \"2025-06-01T03:01:01+02:00\"\n            ],\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"tax_amount\": {\n            \"description\": \"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n            \"examples\": [\n              \"12357\",\n              \"99\"\n            ],\n            \"type\": \"integer\"\n          }\n        }\n      },\n      \"minItems\": 1\n    }\n  }\n}\n ```\n </details>","required":["created_at","transactions"],"properties":{"created_at":{"type":"string","format":"date-time","description":"Date and time when the report was generated in RFC 3339 format"},"transactions":{"type":"array","items":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItem"},"description":"List of transactions"}}},"MerchantTransactionsReportTransactionsItem":{"type":"object","description":"`MerchantTransactionsReportTransactionsItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"currency\",\n    \"full_amount\",\n    \"id\",\n    \"payer_country\",\n    \"settled_at\",\n    \"tax_amount\"\n  ],\n  \"properties\": {\n    \"currency\": {\n      \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n      \"examples\": [\n        \"EUR\",\n        \"USD\"\n      ],\n      \"type\": \"string\",\n      \"pattern\": \"^[A-Z]{3}$\"\n    },\n    \"full_amount\": {\n      \"description\": \"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n      \"examples\": [\n        \"12357\",\n        \"99\"\n      ],\n      \"type\": \"integer\"\n    },\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"payer_country\": {\n      \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n      \"examples\": [\n        \"FRA\",\n        \"ESP\"\n      ],\n      \"type\": \"string\",\n      \"pattern\": \"^[A-Z]{3}$\"\n    },\n    \"settled_at\": {\n      \"description\": \"Date and time when the transaction was settled in RFC 3339 format\",\n      \"examples\": [\n        \"2025-06-12T12:14:25Z\",\n        \"2025-06-01T03:01:01+02:00\"\n      ],\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"tax_amount\": {\n      \"description\": \"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n      \"examples\": [\n        \"12357\",\n        \"99\"\n      ],\n      \"type\": \"integer\"\n    }\n  }\n}\n ```\n </details>","required":["currency","full_amount","id","payer_country","settled_at","tax_amount"],"properties":{"currency":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItemCurrency","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction"},"full_amount":{"type":"integer","format":"int64","description":"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units."},"id":{"type":"string","description":"Unique transaction identifier"},"payer_country":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItemPayerCountry","description":"Alpha-3 code of the country (ISO 3166) of the payer"},"settled_at":{"type":"string","format":"date-time","description":"Date and time when the transaction was settled in RFC 3339 format"},"tax_amount":{"type":"integer","format":"int64","description":"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units."}}},"MerchantTransactionsReportTransactionsItemCurrency":{"type":"string","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n  \"examples\": [\n    \"EUR\",\n    \"USD\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"},"MerchantTransactionsReportTransactionsItemPayerCountry":{"type":"string","description":"Alpha-3 code of the country (ISO 3166) of the payer\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n  \"examples\": [\n    \"FRA\",\n    \"ESP\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"},"ReportIdResponse":{"type":"object","required":["reportId"],"properties":{"reportId":{"type":"integer","format":"int64","description":"Unique identifier of Merchant Transactions report.","minimum":1}}}}}}
````


# Models

## The MerchantTransactionsReport object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"MerchantTransactionsReport":{"type":"object","description":"Report of merchant transactions\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"$id\": \"https://store.onside.io/schema/transactions-report.json\",\n  \"title\": \"Merchant Transactions Report\",\n  \"description\": \"Report of merchant transactions\",\n  \"type\": \"object\",\n  \"required\": [\n    \"created_at\",\n    \"transactions\"\n  ],\n  \"properties\": {\n    \"created_at\": {\n      \"description\": \"Date and time when the report was generated in RFC 3339 format\",\n      \"examples\": [\n        \"2025-06-30T16:01:25Z\",\n        \"2025-06-30T03:01:01+02:00\"\n      ],\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"transactions\": {\n      \"description\": \"List of transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\n          \"currency\",\n          \"full_amount\",\n          \"id\",\n          \"payer_country\",\n          \"settled_at\",\n          \"tax_amount\"\n        ],\n        \"properties\": {\n          \"currency\": {\n            \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n            \"examples\": [\n              \"EUR\",\n              \"USD\"\n            ],\n            \"type\": \"string\",\n            \"pattern\": \"^[A-Z]{3}$\"\n          },\n          \"full_amount\": {\n            \"description\": \"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n            \"examples\": [\n              \"12357\",\n              \"99\"\n            ],\n            \"type\": \"integer\"\n          },\n          \"id\": {\n            \"description\": \"Unique transaction identifier\",\n            \"type\": \"string\"\n          },\n          \"payer_country\": {\n            \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n            \"examples\": [\n              \"FRA\",\n              \"ESP\"\n            ],\n            \"type\": \"string\",\n            \"pattern\": \"^[A-Z]{3}$\"\n          },\n          \"settled_at\": {\n            \"description\": \"Date and time when the transaction was settled in RFC 3339 format\",\n            \"examples\": [\n              \"2025-06-12T12:14:25Z\",\n              \"2025-06-01T03:01:01+02:00\"\n            ],\n            \"type\": \"string\",\n            \"format\": \"date-time\"\n          },\n          \"tax_amount\": {\n            \"description\": \"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n            \"examples\": [\n              \"12357\",\n              \"99\"\n            ],\n            \"type\": \"integer\"\n          }\n        }\n      },\n      \"minItems\": 1\n    }\n  }\n}\n ```\n </details>","required":["created_at","transactions"],"properties":{"created_at":{"type":"string","format":"date-time","description":"Date and time when the report was generated in RFC 3339 format"},"transactions":{"type":"array","items":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItem"},"description":"List of transactions"}}},"MerchantTransactionsReportTransactionsItem":{"type":"object","description":"`MerchantTransactionsReportTransactionsItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"currency\",\n    \"full_amount\",\n    \"id\",\n    \"payer_country\",\n    \"settled_at\",\n    \"tax_amount\"\n  ],\n  \"properties\": {\n    \"currency\": {\n      \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n      \"examples\": [\n        \"EUR\",\n        \"USD\"\n      ],\n      \"type\": \"string\",\n      \"pattern\": \"^[A-Z]{3}$\"\n    },\n    \"full_amount\": {\n      \"description\": \"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n      \"examples\": [\n        \"12357\",\n        \"99\"\n      ],\n      \"type\": \"integer\"\n    },\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"payer_country\": {\n      \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n      \"examples\": [\n        \"FRA\",\n        \"ESP\"\n      ],\n      \"type\": \"string\",\n      \"pattern\": \"^[A-Z]{3}$\"\n    },\n    \"settled_at\": {\n      \"description\": \"Date and time when the transaction was settled in RFC 3339 format\",\n      \"examples\": [\n        \"2025-06-12T12:14:25Z\",\n        \"2025-06-01T03:01:01+02:00\"\n      ],\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"tax_amount\": {\n      \"description\": \"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n      \"examples\": [\n        \"12357\",\n        \"99\"\n      ],\n      \"type\": \"integer\"\n    }\n  }\n}\n ```\n </details>","required":["currency","full_amount","id","payer_country","settled_at","tax_amount"],"properties":{"currency":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItemCurrency","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction"},"full_amount":{"type":"integer","format":"int64","description":"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units."},"id":{"type":"string","description":"Unique transaction identifier"},"payer_country":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItemPayerCountry","description":"Alpha-3 code of the country (ISO 3166) of the payer"},"settled_at":{"type":"string","format":"date-time","description":"Date and time when the transaction was settled in RFC 3339 format"},"tax_amount":{"type":"integer","format":"int64","description":"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units."}}},"MerchantTransactionsReportTransactionsItemCurrency":{"type":"string","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n  \"examples\": [\n    \"EUR\",\n    \"USD\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"},"MerchantTransactionsReportTransactionsItemPayerCountry":{"type":"string","description":"Alpha-3 code of the country (ISO 3166) of the payer\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n  \"examples\": [\n    \"FRA\",\n    \"ESP\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"}}}}
````

## The MerchantTransactionsReportTransactionsItem object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"MerchantTransactionsReportTransactionsItem":{"type":"object","description":"`MerchantTransactionsReportTransactionsItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"currency\",\n    \"full_amount\",\n    \"id\",\n    \"payer_country\",\n    \"settled_at\",\n    \"tax_amount\"\n  ],\n  \"properties\": {\n    \"currency\": {\n      \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n      \"examples\": [\n        \"EUR\",\n        \"USD\"\n      ],\n      \"type\": \"string\",\n      \"pattern\": \"^[A-Z]{3}$\"\n    },\n    \"full_amount\": {\n      \"description\": \"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n      \"examples\": [\n        \"12357\",\n        \"99\"\n      ],\n      \"type\": \"integer\"\n    },\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"payer_country\": {\n      \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n      \"examples\": [\n        \"FRA\",\n        \"ESP\"\n      ],\n      \"type\": \"string\",\n      \"pattern\": \"^[A-Z]{3}$\"\n    },\n    \"settled_at\": {\n      \"description\": \"Date and time when the transaction was settled in RFC 3339 format\",\n      \"examples\": [\n        \"2025-06-12T12:14:25Z\",\n        \"2025-06-01T03:01:01+02:00\"\n      ],\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"tax_amount\": {\n      \"description\": \"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units.\",\n      \"examples\": [\n        \"12357\",\n        \"99\"\n      ],\n      \"type\": \"integer\"\n    }\n  }\n}\n ```\n </details>","required":["currency","full_amount","id","payer_country","settled_at","tax_amount"],"properties":{"currency":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItemCurrency","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction"},"full_amount":{"type":"integer","format":"int64","description":"Amount of the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units."},"id":{"type":"string","description":"Unique transaction identifier"},"payer_country":{"$ref":"#/components/schemas/MerchantTransactionsReportTransactionsItemPayerCountry","description":"Alpha-3 code of the country (ISO 3166) of the payer"},"settled_at":{"type":"string","format":"date-time","description":"Date and time when the transaction was settled in RFC 3339 format"},"tax_amount":{"type":"integer","format":"int64","description":"Amount of the taxes included in the transaction in the original currency in minor units: the smallest unit of a currency, depending on the number of decimals. For example, $12.34 (12 USD and 34 cents) is 1234 in minor units."}}},"MerchantTransactionsReportTransactionsItemCurrency":{"type":"string","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n  \"examples\": [\n    \"EUR\",\n    \"USD\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"},"MerchantTransactionsReportTransactionsItemPayerCountry":{"type":"string","description":"Alpha-3 code of the country (ISO 3166) of the payer\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n  \"examples\": [\n    \"FRA\",\n    \"ESP\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"}}}}
````

## The MerchantTransactionsReportTransactionsItemCurrency object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"MerchantTransactionsReportTransactionsItemCurrency":{"type":"string","description":"Alpha-3 code of the original currency (ISO 4027) of the transaction\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the original currency (ISO 4027) of the transaction\",\n  \"examples\": [\n    \"EUR\",\n    \"USD\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"}}}}
````

## The MerchantTransactionsReportTransactionsItemPayerCountry object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"MerchantTransactionsReportTransactionsItemPayerCountry":{"type":"string","description":"Alpha-3 code of the country (ISO 3166) of the payer\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"description\": \"Alpha-3 code of the country (ISO 3166) of the payer\",\n  \"examples\": [\n    \"FRA\",\n    \"ESP\"\n  ],\n  \"type\": \"string\",\n  \"pattern\": \"^[A-Z]{3}$\"\n}\n ```\n </details>"}}}}
````

## The ReportIdResponse object

```json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"ReportIdResponse":{"type":"object","required":["reportId"],"properties":{"reportId":{"type":"integer","format":"int64","description":"Unique identifier of Merchant Transactions report.","minimum":1}}}}}}
```

## The ResultOfMerchantTransactionsReportProcessing object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"ResultOfMerchantTransactionsReportProcessing":{"type":"object","description":"Result of processing of transactions report\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"$id\": \"https://store.onside.io/schema/transactions-report-result.json\",\n  \"title\": \"Result of Merchant Transactions Report processing\",\n  \"description\": \"Result of processing of transactions report\",\n  \"type\": \"object\",\n  \"required\": [\n    \"accepted_at\",\n    \"report_id\",\n    \"transactions\"\n  ],\n  \"properties\": {\n    \"accepted_at\": {\n      \"description\": \"Date and time when the report was accepted\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"completed_at\": {\n      \"description\": \"Date and time when the report was completed\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"processing_at\": {\n      \"description\": \"Date and time when the report processing has started\",\n      \"type\": \"string\",\n      \"format\": \"date-time\"\n    },\n    \"report_id\": {\n      \"description\": \"Unique report identifier\",\n      \"type\": \"string\"\n    },\n    \"transactions\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"invalid\": {\n          \"description\": \"List of invalid transactions\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"required\": [\n              \"id\",\n              \"reason\"\n            ],\n            \"properties\": {\n              \"id\": {\n                \"description\": \"Unique transaction identifier\",\n                \"type\": \"string\"\n              },\n              \"reason\": {\n                \"description\": \"Reason why the transaction is invalid\",\n                \"type\": \"string\"\n              }\n            }\n          }\n        },\n        \"valid\": {\n          \"description\": \"List of identifiers of valid transactions\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    }\n  }\n}\n ```\n </details>","required":["accepted_at","report_id","transactions"],"properties":{"accepted_at":{"type":"string","format":"date-time","description":"Date and time when the report was accepted"},"completed_at":{"type":["string","null"],"format":"date-time","description":"Date and time when the report was completed"},"processing_at":{"type":["string","null"],"format":"date-time","description":"Date and time when the report processing has started"},"report_id":{"type":"string","description":"Unique report identifier"},"transactions":{"$ref":"#/components/schemas/ResultOfMerchantTransactionsReportProcessingTransactions"}}},"ResultOfMerchantTransactionsReportProcessingTransactions":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactions`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"invalid\": {\n      \"description\": \"List of invalid transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"reason\"\n        ],\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique transaction identifier\",\n            \"type\": \"string\"\n          },\n          \"reason\": {\n            \"description\": \"Reason why the transaction is invalid\",\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"valid\": {\n      \"description\": \"List of identifiers of valid transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    }\n  }\n}\n ```\n </details>","properties":{"invalid":{"type":"array","items":{"$ref":"#/components/schemas/ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem"},"description":"List of invalid transactions"},"valid":{"type":"array","items":{"type":"string"},"description":"List of identifiers of valid transactions"}}},"ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"reason\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"reason\": {\n      \"description\": \"Reason why the transaction is invalid\",\n      \"type\": \"string\"\n    }\n  }\n}\n ```\n </details>","required":["id","reason"],"properties":{"id":{"type":"string","description":"Unique transaction identifier"},"reason":{"type":"string","description":"Reason why the transaction is invalid"}}}}}}
````

## The ResultOfMerchantTransactionsReportProcessingTransactions object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"ResultOfMerchantTransactionsReportProcessingTransactions":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactions`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"properties\": {\n    \"invalid\": {\n      \"description\": \"List of invalid transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"object\",\n        \"required\": [\n          \"id\",\n          \"reason\"\n        ],\n        \"properties\": {\n          \"id\": {\n            \"description\": \"Unique transaction identifier\",\n            \"type\": \"string\"\n          },\n          \"reason\": {\n            \"description\": \"Reason why the transaction is invalid\",\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"valid\": {\n      \"description\": \"List of identifiers of valid transactions\",\n      \"type\": \"array\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    }\n  }\n}\n ```\n </details>","properties":{"invalid":{"type":"array","items":{"$ref":"#/components/schemas/ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem"},"description":"List of invalid transactions"},"valid":{"type":"array","items":{"type":"string"},"description":"List of identifiers of valid transactions"}}},"ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"reason\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"reason\": {\n      \"description\": \"Reason why the transaction is invalid\",\n      \"type\": \"string\"\n    }\n  }\n}\n ```\n </details>","required":["id","reason"],"properties":{"id":{"type":"string","description":"Unique transaction identifier"},"reason":{"type":"string","description":"Reason why the transaction is invalid"}}}}}}
````

## The ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem object

````json
{"openapi":"3.1.0","info":{"title":"Merchant API","version":"1.0.0"},"components":{"schemas":{"ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem":{"type":"object","description":"`ResultOfMerchantTransactionsReportProcessingTransactionsInvalidItem`\n\n <details><summary>JSON schema</summary>\n\n ```json\n{\n  \"type\": \"object\",\n  \"required\": [\n    \"id\",\n    \"reason\"\n  ],\n  \"properties\": {\n    \"id\": {\n      \"description\": \"Unique transaction identifier\",\n      \"type\": \"string\"\n    },\n    \"reason\": {\n      \"description\": \"Reason why the transaction is invalid\",\n      \"type\": \"string\"\n    }\n  }\n}\n ```\n </details>","required":["id","reason"],"properties":{"id":{"type":"string","description":"Unique transaction identifier"},"reason":{"type":"string","description":"Reason why the transaction is invalid"}}}}}}
````


# Store Attribution Webhooks

This page describes how attribution events are tracked and sent from the Onside store. These events are primarily used to measure conversions for ad campaigns and connect them with user actions inside

## Process Description

The attribution process is divided into five main stages:

1. **Initial Visit & Web Attribution**: \
   A user visits our landing page, triggering the `landing_page_visited` event. We log this visit, including their IP address and referrer information. When they click the "Install" button, the `install_button_tapped` event is fired, and we initiate the installation process.
2. **Marketplace Installation:**\
   Installing the marketplace on a device triggers the `store_app_installed` event.
3. **First Launch & Mobile Attribution**: \
   On first launch, the marketplace app registers itself with our backend. The backend attempts to match the mobile installation to the initial web visit using the user's IP address.
4. **User Verification & Final Attribution**: \
   When the user signs in and verifies their account, we create a user record and link it to the mobile installation and the original web visit. This completes the attribution chain.
5. **App Installation**: \
   The mobile app is installed on the user's device, which triggers the `app_installed` event.

## Delivery method

Onside sends attribution events as webhooks using an HTTPS POST request.

* Each event is delivered as a JSON payload following the schema below.
* You can get these events set up for your account by providing your webhook endpoint URL to your Onside account manager.

## Event structure

All events conform to the [schema](https://onside.io/schema/ads-conversions.json).

| Field             | Description                                                                                          | Required                                                      |
| ----------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| onside\_event\_id | unique identifier for deduplication (uuid)                                                           | +                                                             |
| conversion\_label | type of event ([see below](https://docs.onside.io/api/store-attribution-webhooks#conversion-labels)) | +                                                             |
| conversion\_time  | RFC 3339 timestamp of when the event occurred                                                        | +                                                             |
| app\_id           | ApplicationApple Id (uint64)                                                                         | + for `conversion_label` = `app_installed` and `app_launched` |
| gclid             | Google Ads click identifier (from web campaigns)                                                     | -                                                             |
| gbraid            | Identifier for click-based attribution on iOS (Google Ads)                                           | -                                                             |
| wbraid            | Identifier for view-through attribution on iOS (Google Ads)                                          | -                                                             |

<details>

<summary>Event example</summary>

```json
[
  {
    "onside_event_id": "0197ac9c-ef28-7ee3-a5b4-533c6157d4d8",
    "conversion_label": "store_app_installed",
    "conversion_time": "2025-01-01T00:00:00Z",
    "app_id": "1234567890",
    "gclid": "EAIaIQobChMInbuctZO6jgMVEWWkBB2KrCZJEAEYASAAEgLVvfD_BwE",
    "gbraid": "0AAAAAqGACjk7bU-GTrGxRYpdqSonuTpAV",
    "wbraid": "0AAAAAqGACjmOugzfZ2SiPGY7nz370F-s-"
  }
]
```

</details>

### Conversion labels

The following conversion events are supported:

| Label                   | Description                                           | Comment                                                                               |
| ----------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------- |
| landing\_page\_visited  | User opened the landing page from an ad.              | This event is not available by default. Please ask your account manager to enable it. |
| install\_button\_tapped | User tapped the “install” button on the landing page. | This event is not available by default. Please ask your account manager to enable it. |
| store\_app\_installed   | User installed the Onside store app.                  |                                                                                       |
| app\_installed          | User installed the app via the Onside store.          |                                                                                       |
| app\_launched           | User opened the app.                                  | This event is only available when the Onside SDK is integrated into the app.          |

### Usage

* Events are sent via POST webhooks directly to the endpoint you provide.
* These events are exported to Google Ads for conversion tracking and optimization.
* Each event should include the `gclid`, `gbraid`, or `wbraid` parameter if available to ensure correct attribution.
* Events can be deduplicated using the `onside_event_id` field.


# Onside Install Attribution Flow

Onside uses a privacy-friendly token system to track user journeys from landing page to app install. It details events available for marketing analytics and performance.

## 🔍 Overview

Onside uses a lightweight client-server tracking system to attribute app installs to user actions on landing pages. This allows us to:&#x20;

* Understand where installs are coming from
* Optimize user experience across devices
* Integrate with platforms like Google Ads for conversion tracking

Attribution is anonymous and relies on two browser-side tokens and server-side event reporting.

***

## 🔑 The Two-Token System

1. `onside_token` – Persistent Visitor Identifier&#x20;
   * Generated on the user's first visit
   * Stored in a browser cookie for up to 6 months&#x20;
   * Extended on each return visit&#x20;
   * Does not contain any personal data

<details>

<summary>Example</summary>

```json
{
  "token": "550e8400-e29b-41d4-a716-446655440000",
  "createdAt": 1704067200000
}
```

</details>

2. `attribution_token` – One-Time Install Attempt Token&#x20;
   * Generated by the server when the page is loaded
   * Returned to the client along with the install link&#x20;
   * Short-lived and used to track a single install session

### How it works:&#x20;

* On page load, the browser requests the install link&#x20;
* The request includes the `onside_token`&#x20;
* The server responds with:&#x20;
  * Install URL (e.g. onside-app\://install?token=abc123)&#x20;
  * `attribution_token`&#x20;
  * Expiry timestamp

***

## 🎬 Attribution in Action

User Flow Example (iPhone, iOS 18.6+):

1. Visit <https://onside.io/tango> to set the `onside_token`.
2. Page loads, triggering a request for the install link and issuing an `attribution_token`.
3. User clicks Install, sending an `install_button_click` event.
4. App opens automatically.
5. User confirms installation, sending a `check_install.click` event.

{% hint style="info" %}
Other flows (older iOS, Android, Chrome) trigger different modals and events (e.g. get\_approved.opened, go\_to\_safari.click).
{% endhint %}

***

## 📦 What Gets Tracked

Each install-related event includes:&#x20;

* Timestamp
* Page URL and element ID
* `onside_token`
* `attribution_token` (if applicable)
* Device type and browser info
* Timezone and language

### Common Events:

| Event Description      | Details                                       |
| ---------------------- | --------------------------------------------- |
| install\_button\_click | User clicked the install button               |
| get\_approved.opened   | Approval modal shown                          |
| check\_install.click   | User confirmed they installed the app         |
| go\_to\_safari.click   | Prompted to open Safari (non-default browser) |
| is\_android.opened     | Android-specific fallback displayed           |

***

## 🌍 Attribution Events for Analytics

We send webhook events that match key user actions in the attribution flow. These events can be integrated into your analytics pipeline or exported to ad platforms.

## 🔔 Webhook Events

Each event is a POST request with a JSON payload. Webhooks include:&#x20;

* Unique event ID (onside\_event\_id)
* Conversion label (e.g. store\_app\_installed)
* Timestamp (conversion\_time)
* App ID (if applicable)
* Google Ads attribution parameters (optional: gclid, gbraid, wbraid)

All events conform to the [schema](https://onside.io/schema/ads-conversions.json).

<details>

<summary>Event example</summary>

```json
[
  {
    "onside_event_id": "0197ac9c-ef28-7ee3-a5b4-533c6157d4d8",
    "conversion_label": "store_app_installed",
    "conversion_time": "2025-01-01T00:00:00Z",
    "app_id": "1234567890",
    "gclid": "EAIaIQobChMInbuctZO6jgMVEWWkBB2KrCZJEAEYASAAEgLVvfD_BwE",
    "gbraid": "0AAAAAqGACjk7bU-GTrGxRYpdqSonuTpAV",
    "wbraid": "0AAAAAqGACjmOugzfZ2SiPGY7nz370F-s-"
  }
]
```

</details>

## 📋 Supported Conversion Labels

| Label                   | Description                                           | Comment                                                                               |
| ----------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------- |
| landing\_page\_visited  | User opened the landing page from an ad.              | This event is not available by default. Please ask your account manager to enable it. |
| install\_button\_tapped | User tapped the “install” button on the landing page. | This event is not available by default. Please ask your account manager to enable it. |
| store\_app\_installed   | User installed the Onside store app.                  |                                                                                       |
| app\_installed          | User installed the end app via the Onside store.      |                                                                                       |
| app\_launched           | User opened the app.                                  | This event is only available when the Onside SDK is integrated into the app.          |

***

## 🔐 Privacy First

We designed the attribution system to be privacy-conscious:&#x20;

* No personal data is collected (no names, emails, or user IDs)
* Cookies are limited to random tokens
* Events are anonymous and compliant with GDPR principles

***

## 🤝 Integration Requirements

To receive webhook data, you must:&#x20;

* Provide a webhook endpoint URL to your Onside account manager.
* Ensure your endpoint accepts `application/json` POST requests.
* Handle deduplication using the `onside_event_id`.

***

## Token Lifecycle Summary

| Token              | Scope           | Storage                       | Expiry                   |
| ------------------ | --------------- | ----------------------------- | ------------------------ |
| onside\_token      | Visitor session | Browser cookie                | 6 months (auto-extended) |
| attribution\_token | Install session | Server-generated, client-used | A few minutes            |


# Onside Attribution

How Onside tracks and delivers landing-page, install, purchase, and custom conversion events.

Onside tracks the conversion journey from the application landing page to Store installation, target-app installation, purchase, and custom application events.

Landing-page events are configured through Google Tag Manager (GTM). Deeper conversion events can be delivered either to the publisher through webhooks or directly to supported advertising platforms through Onside-managed connectors.

### Client-side attribution

Onside uses an anonymous `onside_token` stored in the browser to associate landing-page activity and install-link requests with the same visitor. The token does not contain personal data and is not an advertising-platform click ID.

### Conversion events

| Stage | Event                  | Description                                                                     | Source              |
| ----- | ---------------------- | ------------------------------------------------------------------------------- | ------------------- |
| 1     | `page_view`            | Any visit to the application landing page                                       | Client-side via GTM |
| 2     | `eligible_page_view`   | A visit from iOS 18.6+ using a supported browser                                | Client-side via GTM |
| 3     | `install_button_click` | An eligible visitor clicks the install button                                   | Client-side via GTM |
| 4     | `store_app_installed`  | The visitor installs the Onside Store                                           | Server-side         |
| 5     | `app_installed`        | The visitor installs the target application                                     | Server-side         |
| 6     | `purchase_completed`   | A purchase from the target application or Store reported through the Onside SDK | Server-side         |
| 7     | Custom SDK/S2S event   | A custom event is reported through the Onside SDK or S2S API                    | Server-side         |

### Landing-page tracking: events 1–3

Onside manages the GTM configuration on the application landing page and can add JavaScript tracking tags for advertising platforms or analytics systems.

Send Onside the tracking snippets generated by the platforms you use and specify the required event mapping. Onside will add the tags to the relevant landing page and configure events 1–3.

Examples include:

* Google Ads, Meta Ads, and TikTok Ads;
* Google Analytics;
* Amplitude.

#### Default Meta event mapping

| Onside event           | Meta standard event    |
| ---------------------- | ---------------------- |
| `eligible_page_view`   | `PageView`             |
| `install_button_click` | `SubmitApplication`    |
| `store_app_installed`  | `Lead`                 |
| `app_installed`        | `CompleteRegistration` |
| `purchase_completed`   | `Purchase`             |

Other mappings can be agreed during setup.

### Server-side delivery: events 4–7

Choose one delivery model for each advertising platform.

#### Publisher-managed delivery

Onside sends the agreed conversion events and attribution data to the publisher's webhook endpoint. The publisher is responsible for forwarding the events to the advertising platforms.

Onside provides the webhook contract as part of the integration setup. The payload and attribution fields are agreed for the selected platforms and events.

#### Onside-managed delivery

Onside sends conversion events directly to supported advertising platforms. Provide the list of required platforms, and Onside will confirm connector availability and the account access, identifiers, or configuration required for each integration. Additional connectors can be added upon request from the publisher.

| Platform   | Integration       | Status         |
| ---------- | ----------------- | -------------- |
| Google Ads | Data Manager      | Available      |
| Meta Ads   | Conversions API   | Available      |
| TikTok Ads | Managed connector | In development |
| ExoClick   | Managed connector | In development |

Use only one delivery path for the same event and advertising platform to avoid duplicate conversions.

### What we need from you

* JavaScript tracking snippets for any advertising platforms or analytics systems you want to use;
* the required event mapping and any platform-specific configuration for each tracker;
* the landing pages and applications to which each tracker applies;
* the selected server-side delivery model for each advertising platform;
* for publisher-managed delivery: a webhook URL and authentication requirements;
* for Onside-managed delivery: the list of required advertising platforms;
* definitions for any custom SDK or S2S events.

### Attribution identifiers and deduplication

Advertising-platform click IDs and browser identifiers are handled as part of the configured landing-page tag and server-side integration where applicable. The exact fields depend on the selected platform and delivery model and are confirmed during setup.

Webhook events include a stable `onside_event_id` that can be used for deduplication.


# Installation and Attribution

This document describes the Onside API endpoint for retrieving app installation and attribution data. It is intended for use by apps and SDKs upon their first launch.

### Matching approach

#### **Client-provided data**

The client sends the token (e.g., appsflyer\_id) along with device information. See the request format.

#### Existing installation check

If an installation record with the same token already exists, we return the previously issued installation ID.

#### New installation logic

If no installation with that token is found, we attempt to resolve the installation as follows:

1. **IP-based matching**\
   We compare the request’s **IP address** with user IPs from recent Onside Store sessions (last 24 hours).
2. If we find a match, we generate a new installation ID tied to that specific store user.
3. **Device properties matching**\
   If multiple users share the same IP, we use **device information** to resolve the collision. If we can’t resolve the collision, we generate a completely new installation token.&#x20;
4. If no store sessions match the request IP, we generate a completely new installation token.
5. **Attribution resolution**\
   If the matched store user has attribution data, we include it in the response as `attribution_data`.\
   Otherwise, we return no `attribution data`, indicating an organic user.

An example of matched installation without marketing data (Organic install):

```
# The JWS response
eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpY19rZXlfMSJ9.eyJpc3Mi...
# Decoded response
{
  "iss": "https://onside.io",
  "aud": "com.example.my-awesome-app",
  "iat": 1678886400,
  "exp": 1678890000,
  "jti": "4d3f2c1a-6b7c-4d3f-8c1a-6b7c4d3f8c1a",
  "request_token": "a1b2c3d4-client-random-token-5e6f",
  "installation_token": "a1b2c3d4-onside-install-token-5e6f"
 }
```

An example of matched installation with marketing data (Non-Organic install):

```
# The JWS response
eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpY19rZXlfMSJ9.eyJpc3Mi...
# Decoded response
{
  "iss": "https://onside.io",
  "aud": "com.example.my-awesome-app",
  "iat": 1678886400,
  "exp": 1678890000,
  "jti": "4d3f2c1a-6b7c-4d3f-8c1a-6b7c4d3f8c1a",
  "request_token": "a1b2c3d4-client-random-token-5e6f",
  "installation_token": "a1b2c3d4-onside-install-token-5e6f",
  "attribution_data": {
    "marketing_campaign": "summer_sale_2024",
    "source": "facebook_ads",
    "tags": ["social", "video_ad"],
    "click_id": "clk_123456789"
  }
 }
```

## Get iOS Installation Attribution

> Attempts to match a new app installation with attribution data.

```json
{"openapi":"3.0.1","info":{"title":"Installation Attribution API","version":"1.0.0"},"servers":[{"url":"https://onside.io/","description":"Production Server"},{"url":"https://onside.dev/","description":"Testing Server"}],"paths":{"/merchant-api/v1/attribution/install":{"post":{"summary":"Get iOS Installation Attribution","description":"Attempts to match a new app installation with attribution data.","operationId":"getInstallationAttribution","tags":["Attribution"],"requestBody":{"description":"Device and app information for matching.","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InstallationRequest"}}}},"responses":{"200":{"description":"**Match Found.**\n\nReturns a signed JSON Web Token (JWT) as a plain text string.\nThe JWT payload contains the installation token and attribution data.\n\n**You must validate the JWT signature** using our public key, \npublished at: `https://onside.io/.well-known/jwks.json`.\n\nCheck `AttributionResponse` for the token schema.\n","content":{"application/jose":{"schema":{"type":"string","format":"jwt"}}}}}}}},"components":{"schemas":{"InstallationRequest":{"type":"object","description":"Information provided by the client device for attribution matching.","required":["token","bundle_id","device_model","os_version","language"],"properties":{"token":{"type":"string","description":"A unique, client-generated random string to bind the installation."},"bundle_id":{"type":"string","description":"The app's bundle identifier."},"device_model":{"type":"string","description":"The device model identifier (e.g., iPhone14,5)."},"os_version":{"type":"string","description":"The version of the device's operating system."},"language":{"type":"string","description":"The device's BCP 47 language code."},"availableCapacity":{"type":"integer","description":"The available disk capacity on the device in bytes"}}}}}}
```

## The InstallationRequest object

```json
{"openapi":"3.0.1","info":{"title":"Installation Attribution API","version":"1.0.0"},"components":{"schemas":{"InstallationRequest":{"type":"object","description":"Information provided by the client device for attribution matching.","required":["token","bundle_id","device_model","os_version","language"],"properties":{"token":{"type":"string","description":"A unique, client-generated random string to bind the installation."},"bundle_id":{"type":"string","description":"The app's bundle identifier."},"device_model":{"type":"string","description":"The device model identifier (e.g., iPhone14,5)."},"os_version":{"type":"string","description":"The version of the device's operating system."},"language":{"type":"string","description":"The device's BCP 47 language code."},"availableCapacity":{"type":"integer","description":"The available disk capacity on the device in bytes"}}}}}}
```

## The AttributionResponse object

```json
{"openapi":"3.0.1","info":{"title":"Installation Attribution API","version":"1.0.0"},"components":{"schemas":{"AttributionResponse":{"type":"object","description":"The decoded payload (claims) of the JWT returned on success.","required":["iss","aud","iat","exp","jti","request_token","installation_token"],"properties":{"iss":{"type":"string","description":"Issuer"},"aud":{"type":"string","description":"Audience (will match the request bundle_id)."},"iat":{"type":"integer","format":"int64","description":"Issued At timestamp (Unix epoch)."},"exp":{"type":"integer","format":"int64","description":"Expiration timestamp (Unix epoch)."},"jti":{"type":"string","description":"JWT ID."},"request_token":{"type":"string","description":"The 'token' you provided in the request, confirming the binding."},"installation_token":{"type":"string","description":"The new, unique installation identifier generated by our system."},"attribution_data":{"$ref":"#/components/schemas/AttributionData"}}},"AttributionData":{"type":"object","description":"Attribution data, if available.","properties":{"marketing_campaign":{"type":"string"},"source":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"click_id":{"type":"string"}}}}}}
```

## The AttributionData object

```json
{"openapi":"3.0.1","info":{"title":"Installation Attribution API","version":"1.0.0"},"components":{"schemas":{"AttributionData":{"type":"object","description":"Attribution data, if available.","properties":{"marketing_campaign":{"type":"string"},"source":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"click_id":{"type":"string"}}}}}}
```


# Server-to-server attribution events

This page describes the server-to-server API for submitting attribution events.

{% hint style="info" %}
Use this API to report attribution events from a trusted backend service. This gives you a secure way to submit events such as purchases, where the mobile app should not be treated as the source of truth.
{% endhint %}

Attribution events can be sent in batches to:

```
POST https://onside.io/attribution-api/v1/events
```

Use Basic authentication. Contact your Onside account contact to get credentials.

Example request:

```bash
curl -X POST \
  -H 'Content-Type: application/json' \
  -u <basic auth> \
  --data '{
    "events": [
      {
        "event_name": "purchase_completed",
        "payload": {
          "currency": "USD",
          "amount": "9.99"
        },
        "event_datetime": "2026-05-05T12:00:00Z",
        "event_id": "550e8400-e29b-41d4-a716-446655440000",
        "installation_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    ]
  }' \
  https://onside.io/attribution-api/v1/events
```

### Request body

Example payload:

```json
{
  "events": [
    {
      "event_name": "purchase_completed",
      "payload": {
        "currency": "USD",
        "amount": "9.99"
      },
      "event_datetime": "2026-05-05T12:00:00Z",
      "event_id": "550e8400-e29b-41d4-a716-446655440000",
      "installation_id": "550e8400-e29b-41d4-a716-446655440000"
    }
  ]
}
```

`events` is an array of attribution events. It can contain up to 1,000 items.

Each event includes:

* `event_name` — The event name. Case-insensitive. **Required**
* `payload` — A JSON object with event properties. **Required**
* `event_datetime` — The time the event occurred on your server, in ISO 8601 format. **Required**
* `event_id` — A unique UUIDv4 used for idempotency. **Required**
* `installation_id` — The installation identifier returned by the Onside SDK in your app. **Required**

### Limits

* Send up to 1,000 events per request.
* Keep the total request payload under 1 MB.

### HTTP response codes

The endpoint can return the following HTTP status codes:

* `204 No Content` — The system accepted and queued the batch for processing.
* `400 Bad Request` — A validation error occurred. This includes malformed JSON and invalid UUIDs.
* `413 Payload Too Large` — The payload exceeds 1 MB or the batch contains more than 1,000 events.
* `429 Too Many Requests` — You have sent too many requests. Wait before you try again.


