> For the complete documentation index, see [llms.txt](https://docs.onside.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.onside.io/sdk/products-and-subscriptions/fetching-products.md).

# Fetching Products

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.md). 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.md).

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

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


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.onside.io/sdk/products-and-subscriptions/fetching-products.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
