---
title: Product management
description: Discover product catalogs and offerings, understand pricing and billing cycles, and use offerings in orders, addons, and subscription changes
---

Everything that a customer can buy through the API is a **product offering**: a mobile plan, a
travel eSIM, an addon, and a license. This guide shows you how to find what is available and
how to read a price. It then resolves the exact set of offerings that one customer can buy.
Last, it puts an offering ID into an order and into a subscription change.

## Prerequisites

You need all of these before you start:

- **API credentials**: A valid access token and API key for the API
- **Customer context**: Whether you are selling to `CONSUMER` or `BUSINESS` customers
- **Order basics**: Familiarity with [placing an order](/developer-guide/use-cases/place-an-order.md) helps for the later steps

## Overview

A typical catalog integration follows this flow:

**1. Explore catalogs**

List product catalogs to understand how offerings are segmented.

**2. List offerings**

Fetch product offerings, filtered by type, category, or catalog.

**3. Interpret pricing**

Read prices, billing cycles, and promotional discounts correctly.

**4. Resolve per-customer catalogs**

Fetch the exact offerings and groups available to one customer.

**5. Sell and change**

Use offering IDs in orders, addons, and subscription changes.

## The object model

Four concepts make up the catalog, from the technical core outward:

- **Product** — The technical definition of a service: its type, category, network provider, and included features (data, calls, SMS, coverage). Products are reusable — several offerings can wrap the same product at different prices.
- **Product offering** — A product combined with a price. This is the unit customers actually buy, and its `productOfferingId` is what you pass to orders, addons, and change endpoints.
- **Product offering group** — Organizes related offerings of the same category — for example all mobile plans. Groups are the natural unit for rendering plan pickers and upgrade ladders.
- **Product catalog** — A curated set of offerings for a context such as a customer segment, region, or sales channel. A catalog can extend the default catalog, inheriting all of its offerings.

Every offering carries its product inline, so one list call gives you the whole picture. The
`product` field says what the service is. The `price` field says what it costs. The `group`,
`name`, `description`, and `imageUrl` fields say how to present it.

### Offering types and categories

The `product.type` field determines what buying the offering creates:

| Type                 | Creates                                                    | Examples                                |
| -------------------- | ---------------------------------------------------------- | --------------------------------------- |
| `SUBSCRIPTION`       | A standalone subscription with its own lifecycle           | Mobile plan, broadband, travel eSIM     |
| `SUBSCRIPTION_ADDON` | A feature or resource attached to an existing subscription | Extra data package, travel eSIM package |
| `LICENSE`            | A license for business/PBX features                        | Enterprise telephony seat               |
| `EXTERNAL_PRODUCT`   | A purchasable item outside the core telecom platform       | Hardware, accessories                   |

The `product.category` field is a sub-type within each type, such as `PRODUCT_CATEGORY_SUBSCRIPTION_CELL`, `PRODUCT_CATEGORY_SUBSCRIPTION_BROADBAND`, `PRODUCT_CATEGORY_TRAVEL_ESIM`, or `PRODUCT_CATEGORY_TRAVEL_ESIM_PACKAGE`. Offerings of the same type and category are generally interchangeable — that is what makes upgrades and downgrades within a group possible.

> **Note**
>
> Addon offerings additionally carry `addonCategories`: the subscription categories the addon can be
> attached to. For example, a `PRODUCT_CATEGORY_TRAVEL_ESIM_PACKAGE` addon that applies to
> `PRODUCT_CATEGORY_TRAVEL_ESIM` subscriptions.

## Step-by-step implementation

### Step 1: List product catalogs

Start by listing the catalogs configured for your tenant. Catalogs segment offerings by market or channel, and their IDs can be used to filter offering lists:

```bash
# List product catalogs, optionally filtered by name
curl -X GET "{BASE_URL}/product-catalogs?filter=US&limit=100" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

A catalog listing looks like this:
```json
{
  "items": [
    {
      "productCatalogId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "US Consumer Catalog",
      "description": "Consumer plans sold through the US web store",
      "extendsDefault": true
    },
    {
      "productCatalogId": "8d3e5f70-12ab-4cd6-9e8f-a01b23c45d67",
      "name": "US Business Catalog",
      "description": "Business plans with pooled data and licenses",
      "extendsDefault": false
    }
  ],
  "pagination": {
    "nextCursor": null
  }
}
```

`extendsDefault` tells you how a catalog is composed. When it is `true`, the catalog inherits
every offering of the default catalog and adds its own. When it is `false`, the catalog stands
alone, and it carries only the offerings assigned to it.

### Step 2: List product offerings

Get the offerings themselves. The `customerType` parameter is required. Every other parameter
narrows the result:

- `types` — filter by offering type (`SUBSCRIPTION`, `SUBSCRIPTION_ADDON`, `LICENSE`, `EXTERNAL_PRODUCT`)
- `categories` — filter by product category
- `productCatalogId` — only offerings belonging to a specific catalog
- `includeArchived` — include `ARCHIVED` offerings (default `false`)
- `countries` / `regions` — coverage filters for travel eSIM offerings (see below)

```bash
# List consumer subscription offerings in a specific catalog
curl -X GET "{BASE_URL}/product-offerings?customerType=CONSUMER&types=SUBSCRIPTION&productCatalogId=f47ac10b-58cc-4372-a567-0e02b2c3d479&limit=100" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"

# Fetch the next page using the cursor from the previous response
curl -X GET "{BASE_URL}/product-offerings?customerType=CONSUMER&types=SUBSCRIPTION&limit=100&cursor=NEXT_CURSOR_VALUE" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

Each item is a full `ProductOffering` with its product embedded:
```json
{
  "items": [
    {
      "productOfferingId": "0b54a9c2-7f13-4e8a-b2d6-91c37f5a04e8",
      "status": "AVAILABLE",
      "name": "Seamless 10GB",
      "description": "10GB of high-speed data on nationwide 5G",
      "customerType": "CONSUMER",
      "product": {
        "productId": "4c6a1e83-b25f-4d90-87ce-3f19a0d6b524",
        "internalName": "seamless_cell_10gb_us",
        "type": "SUBSCRIPTION",
        "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL",
        "networkProviderId": "tmobile-us",
        "features": {
          "dataMb": 10240,
          "includedCallSeconds": 60000,
          "includedSms": 1000
        }
      },
      "price": {
        "netPriceMinor": 2999,
        "currency": "USD",
        "priceType": "RECURRING",
        "billingCycle": {
          "period": "MONTHLY",
          "interval": 1
        },
        "standardDiscount": { "amountMinor": 500 },
        "bindingContract": {
          "duration": {
            "unit": "MONTHS",
            "value": 12
          },
          "discount": {
            "amountMinor": 200
          }
        },
        "customUpfrontPayment": {
          "billingCycles": 3,
          "discount": {
            "amountMinor": 300,
            "duration": { "unit": "MONTHS", "value": 3 }
          }
        },
        "currencyOptionsMinor": {
          "USD": 2999,
          "SEK": 29900
        }
      },
      "group": {
        "productOfferingGroupId": "mobile-plans",
        "name": "Mobile Plans",
        "description": "Cell subscriptions with data, calls, and SMS included",
        "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
      },
      "imageUrl": "https://cdn.example.com/images/seamless-10gb.png"
    }
  ],
  "pagination": {
    "nextCursor": null
  }
}
```

The `product.features` object tells you what the service includes. A cellular plan carries
`dataMb`, `includedCallSeconds`, and `includedSms`. A travel eSIM package carries
`validityDays`, `countries`, `regions`, and `activationType`.

> **Warning**
>
> When you show the details of an existing subscription, pass `includeArchived=true`, or get the
> offering by its ID. Nobody can order an `ARCHIVED` offering any more, but an existing subscription
> can still point at one, and the lookup then comes back empty.

To fetch a single offering — for example to render a detail page or re-validate before checkout — use its ID:

```bash
curl -X GET "{BASE_URL}/product-offerings/0b54a9c2-7f13-4e8a-b2d6-91c37f5a04e8" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

### Step 3: Understand pricing and billing cycles

Every offering carries one `price` object. Read it with these fields:

| Field                  | Meaning                                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| `netPriceMinor`        | The offering's configured price for one billing period, with no discount deducted                               |
| `currency`             | The currency code, such as `USD`                                                                                |
| `priceType`            | `ONE_TIME` for a single charge, `RECURRING` for repeated billing                                                |
| `billingCycle`         | For recurring prices: the billing `period` (`MONTHLY`) and `interval` (1 = every month, 3 = every three months) |
| `standardDiscount`     | An unconditional discount: `amountMinor` per billing period, and an optional `duration`                         |
| `bindingContract`      | A commitment to keep the subscription for a fixed `duration`, and the `discount` granted in exchange            |
| `customUpfrontPayment` | Billing cycles the customer pays for in advance at checkout, and the `discount` granted for doing so            |
| `currencyOptionsMinor` | Per-currency price overrides keyed by ISO currency code, for offerings sold in multiple currencies              |

> **Warning**
>
> **A price is the catalog entry, not a quote.** Do not charge a customer from it. Each field
> reports the offering exactly as it is configured. `netPriceMinor` has no discount deducted, not
> even the discounts on the same object. The price also knows nothing about the customer that reads
> it, so it carries no promotion and no negotiated price list.

The order is the one place that resolves a discount, a promotion, a price list, and the tax. Add
the offering to an order, then read
[the order's `pricing`](/api-reference/orders.md#tag/orders/GET/orders/{orderId}). That answer is
what the customer pays.

Every monetary amount is an integer in the **minor units** of its currency. A minor unit is one
hundredth of the major unit, for every currency that the platform bills in. As a result, `2999`
is $29.99 in `USD`, and 299.00 kr in `SEK`. Divide by 100 to display an amount.

The decimal `netPrice`, `currencyOptions`, `discount` and `discountMinor` fields are gone from
the response. An older revision still receives the first two. See [Versioning](/api-reference/versioning.md).

Only `currency` and `priceType` are always present. Every example in this
documentation shows the same offering with each optional field filled in, so that you see the
whole shape in one place. A real offering carries only the discounts and the currency options
that it is configured with. Read all of them as optional.

The two price types match two selling motions:

- **`RECURRING`** — A subscription, a license, and a recurring addon. The `billingCycle` gives
  the cadence. `{ "period": "MONTHLY", "interval": 1 }` bills every month.
- **`ONE_TIME`** — One charge, such as a travel eSIM package or an external product. There is no
  `billingCycle`.

> **Warning**
>
> **A recurring price is quoted for one billing period, not for one charge.** Bill and total
> `netPriceMinor × billingCycle.interval`, and display the per-period figure. An `interval` of more
> than 1 collects that many periods at once. A `netPriceMinor` of `2999` with a `MONTHLY` period and
> an `interval` of 3 charges `8997` every three months, not `2999`.

#### Discounts

An offering can carry up to three discounts, each with its own condition. This is the Seamless 10GB price used throughout this documentation:
```json
{
  "price": {
    "netPriceMinor": 2999,
    "currency": "USD",
    "priceType": "RECURRING",
    "billingCycle": { "period": "MONTHLY", "interval": 1 },
    "standardDiscount": { "amountMinor": 500 },
    "bindingContract": {
      "duration": { "unit": "MONTHS", "value": 12 },
      "discount": { "amountMinor": 200 }
    },
    "customUpfrontPayment": {
      "billingCycles": 3,
      "discount": { "amountMinor": 300, "duration": { "unit": "MONTHS", "value": 3 } }
    },
    "currencyOptionsMinor": { "USD": 2999, "SEK": 29900 }
  }
}
```

Read that price this way. The plan lists at $29.99 a month. A customer that takes it as sold
pays $22.99. The $5.00 standard discount and the $2.00 binding-contract discount come off when
the order is priced. A customer that also prepays three billing cycles pays $19.99 per period,
which is $59.97 at checkout. From the fourth month the upfront discount stops and the price
returns to $22.99.

**None of that comes off `netPriceMinor`, which stays 2999.** Each discount has its own
condition, and the order decides which ones apply:

- **`standardDiscount`** — unconditional. It applies to every purchase of the offering.
- **`bindingContract.discount`** — applies when the subscription is bound for `duration` months.
- **`customUpfrontPayment.discount`** — applies when the customer pays `billingCycles` cycles in advance at checkout. Its `duration` covers those cycles, and the price then returns to the full amount.

A discount `amountMinor` is **per billing period**, like the price itself. It is never a total.
`{ "amountMinor": 300 }` takes $3.00 off every period, not $3.00 once. On a quarterly price it
comes off all three periods of each invoice.

You can subtract the discounts yourself to show an indicative price before an order exists. That
is what the fields are for. But the order price is the number that you charge.

#### Discounts that expire

A discount can carry a `duration`, which makes it an introductory offer rather than the standing price:
```json
{
  "standardDiscount": {
    "amountMinor": 500,
    "duration": { "unit": "MONTHS", "value": 3 }
  }
}
```

That takes $5.00 off each of the first three months, $15.00 in all. After that the customer pays
the full price. A discount with no `duration` never stops.

Read `duration` to find out whether a saving that you advertise has an end date. When it does,
say so: "$24.99/mo for 3 months, then $29.99".

`currencyOptionsMinor` is the price of the same offering in the other currencies of the catalog.
This plan is $29.99 in the US and 299.00 kr in Sweden. The key that matches `currency` repeats
`netPriceMinor`.

#### Promotional pricing

Promo codes belong to the order, not to the catalog. Set `promoCode` when you create or update the order, then read the order back:

```bash
# Apply the promo code to the order
curl -X PUT "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "promoCode": "SPRING25" }'

# Read the order back to see what the customer pays
curl "{BASE_URL}/orders/296af5b6-f3b3-4128-b307-5ddc9190502f" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

To examine a code before an order exists, call [Get
promotion](/api-reference/product-discounts.md#tag/product-discounts/GET/discounts/promotions/promo-code/{promoCode}).
It tells you whether the code is valid, and what discount it carries.

### Step 4: Fetch a customer's product catalog

To find out what one customer can buy, ask the API. Do not filter the global list yourself. The
customer catalog endpoint merges the default catalog with every catalog assigned to that
customer, and it returns a result that you can render directly:

```bash
# By internal customer UUID
curl -X GET "{BASE_URL}/customers/5b8f3c72-94d1-4a06-8e2b-c1d7f0a63e94/product-catalog" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"

# By external reference identifier (rid_ prefix)
curl -X GET "{BASE_URL}/customers/rid_crm-customer-12345/product-catalog" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

The response contains the groups and the offerings side by side:
```json
{
  "productOfferingGroups": [
    {
      "productOfferingGroupId": "mobile-plans",
      "name": "Mobile Plans",
      "description": "Cell subscriptions with data, calls, and SMS included",
      "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
    }
  ],
  "productOfferings": [
    {
      "productOfferingId": "0b54a9c2-7f13-4e8a-b2d6-91c37f5a04e8",
      "status": "AVAILABLE",
      "name": "Seamless 10GB",
      "customerType": "CONSUMER",
      "product": {
        "productId": "4c6a1e83-b25f-4d90-87ce-3f19a0d6b524",
        "internalName": "seamless_cell_10gb_us",
        "type": "SUBSCRIPTION",
        "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
      },
      "price": {
        "netPriceMinor": 2999,
        "currency": "USD",
        "priceType": "RECURRING",
        "billingCycle": {
          "period": "MONTHLY",
          "interval": 1
        },
        "standardDiscount": { "amountMinor": 500 },
        "bindingContract": {
          "duration": {
            "unit": "MONTHS",
            "value": 12
          },
          "discount": {
            "amountMinor": 200
          }
        },
        "customUpfrontPayment": {
          "billingCycles": 3,
          "discount": {
            "amountMinor": 300,
            "duration": { "unit": "MONTHS", "value": 3 }
          }
        },
        "currencyOptionsMinor": {
          "USD": 2999,
          "SEK": 29900
        }
      },
      "group": {
        "productOfferingGroupId": "mobile-plans",
        "name": "Mobile Plans",
        "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
      }
    },
    {
      "productOfferingId": "6d2f8e11-3c49-4b7a-a5e0-84b9d1c72f35",
      "status": "AVAILABLE",
      "name": "Seamless 25GB",
      "customerType": "CONSUMER",
      "product": {
        "productId": "4c6a1e83-b25f-4d90-87ce-3f19a0d6b524",
        "internalName": "seamless_cell_25gb_us",
        "type": "SUBSCRIPTION",
        "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
      },
      "price": {
        "netPriceMinor": 3999,
        "currency": "USD",
        "priceType": "RECURRING",
        "billingCycle": {
          "period": "MONTHLY",
          "interval": 1
        },
        "standardDiscount": { "amountMinor": 500 },
        "bindingContract": {
          "duration": {
            "unit": "MONTHS",
            "value": 12
          },
          "discount": {
            "amountMinor": 300
          }
        },
        "customUpfrontPayment": {
          "billingCycles": 3,
          "discount": {
            "amountMinor": 400,
            "duration": { "unit": "MONTHS", "value": 3 }
          }
        },
        "currencyOptionsMinor": {
          "USD": 3999,
          "SEK": 39900
        }
      },
      "group": {
        "productOfferingGroupId": "mobile-plans",
        "name": "Mobile Plans",
        "category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
      }
    }
  ]
}
```

> **Info**
>
> Customer identifiers can be internal UUIDs or your own reference identifiers. Reference
> identifiers must be prefixed with `rid_` (for example `rid_crm-customer-12345`) so the API can
> distinguish them from UUIDs.

### Step 5: Use offerings in orders and addons

The `productOfferingId` is the currency of the rest of the platform. In an order, each line item names the offering it purchases:

```bash
# Create an order with a subscription line item for a chosen offering
curl -X POST "{BASE_URL}/orders" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customerType": "CONSUMER",
    "lineItems": [
      {
        "type": "SUBSCRIPTION",
        "lineItemId": "line-item-1",
        "productOfferingId": "0b54a9c2-7f13-4e8a-b2d6-91c37f5a04e8",
        "sim": { "esim": true }
      }
    ]
  }'
```

Addon offerings (`type: SUBSCRIPTION_ADDON`) attach to an existing subscription instead. Pick an addon whose `addonCategories` includes the subscription's category, then add it:

```bash
# Add a travel eSIM package to an existing travel eSIM subscription
curl -X POST "{BASE_URL}/subscriptions/e7a12b90-45cd-4f38-9a61-08b3d5c2ef47/addons" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-Idempotency-Key: addon-e7a12b90-2a91cf64" \
  -H "Content-Type: application/json" \
  -d '{
    "productOfferingId": "2a91cf64-8e05-47d3-b18c-f60a24d9e573"
  }'
```

### Step 6: Discover and apply subscription changes

For upgrades and downgrades, never guess which offerings a subscription can move to. The change-options endpoint returns exactly what the subscription can become and **when** each change can take effect:

```bash
curl -X GET "{BASE_URL}/subscriptions/e7a12b90-45cd-4f38-9a61-08b3d5c2ef47/product-offering-options" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

Each option pairs an offering with a change schedule:
```json
{
  "items": [
    {
      "productOffering": {
        "productOfferingId": "6d2f8e11-3c49-4b7a-a5e0-84b9d1c72f35",
        "name": "Seamless 25GB",
        "price": {
          "netPriceMinor": 3999,
          "currency": "USD",
          "priceType": "RECURRING",
          "billingCycle": {
            "period": "MONTHLY",
            "interval": 1
          },
          "standardDiscount": { "amountMinor": 500 },
          "bindingContract": {
            "duration": {
              "unit": "MONTHS",
              "value": 12
            },
            "discount": {
              "amountMinor": 300
            }
          },
          "customUpfrontPayment": {
            "billingCycles": 3,
            "discount": {
              "amountMinor": 400,
              "duration": { "unit": "MONTHS", "value": 3 }
            }
          },
          "currencyOptionsMinor": {
            "USD": 3999,
            "SEK": 39900
          }
        }
      },
      "changeSchedule": "INSTANT",
      "changeScheduleDate": "2026-07-12"
    },
    {
      "productOffering": {
        "productOfferingId": "9f3b6d84-2c71-4a5e-b90d-57e1f8a3c266",
        "name": "Seamless 5GB",
        "price": {
          "netPriceMinor": 1999,
          "currency": "USD",
          "priceType": "RECURRING",
          "billingCycle": {
            "period": "MONTHLY",
            "interval": 1
          },
          "standardDiscount": { "amountMinor": 300 },
          "bindingContract": {
            "duration": {
              "unit": "MONTHS",
              "value": 12
            },
            "discount": {
              "amountMinor": 200
            }
          },
          "customUpfrontPayment": {
            "billingCycles": 3,
            "discount": {
              "amountMinor": 200,
              "duration": { "unit": "MONTHS", "value": 3 }
            }
          },
          "currencyOptionsMinor": {
            "USD": 1999,
            "SEK": 19900
          }
        }
      },
      "changeSchedule": "NEXT_RENEWAL_DAY",
      "changeScheduleDate": "2026-08-01"
    }
  ]
}
```

The `changeSchedule` values are:

| Schedule              | Takes effect                                              |
| --------------------- | --------------------------------------------------------- |
| `INSTANT`             | Immediately                                               |
| `FIRST_OF_NEXT_MONTH` | On the first day of the next calendar month               |
| `NEXT_RENEWAL_DAY`    | On the subscription's next renewal date                   |
| `NEXT_PAYMENT_DAY`    | At the end of the prepaid period, on the next payment day |

As a rule of thumb, upgrades and lateral moves are immediate while downgrades wait for the next renewal — but always trust `changeSchedule` and `changeScheduleDate` over assumptions. To apply a change, pass the chosen offering to the change endpoint:

```bash
curl -X PUT "{BASE_URL}/subscriptions/e7a12b90-45cd-4f38-9a61-08b3d5c2ef47/product-offering-change" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-Idempotency-Key: change-e7a12b90-20260712" \
  -H "Content-Type: application/json" \
  -d '{
    "productOfferingId": "6d2f8e11-3c49-4b7a-a5e0-84b9d1c72f35"
  }'
```

The same options-then-change pattern exists for addons and licenses:

- `GET /subscriptions/{subscriptionId}/addons/product-offering-options?currentProductOfferingId=...` and `PUT /subscriptions/{subscriptionId}/addons/product-offering-change` for changing an existing addon
- `GET /licenses/{licenseId}/product-offering-options` and `PUT /licenses/{licenseId}/product-offering-change` for licenses

## Filtering travel eSIM offerings by coverage

Travel eSIM packages carry coverage in `product.features.countries` (ISO 3166-1 alpha-3 codes) and `product.features.regions`. To build a destination picker, first fetch the full coverage map:

```bash
# List all countries and regions covered by travel eSIM offerings
curl -X GET "{BASE_URL}/product-offerings/countries?customerType=CONSUMER" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"

# Then list offerings that cover the selected destination
curl -X GET "{BASE_URL}/product-offerings?customerType=CONSUMER&types=SUBSCRIPTION_ADDON&categories=PRODUCT_CATEGORY_TRAVEL_ESIM_PACKAGE&countries=MEX" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "X-API-Key: YOUR_API_KEY"
```

The coverage response deduplicates countries across all offerings and lists each region with its constituent countries:
```json
{
  "countries": [
    { "code": "USA", "name": "United States" },
    { "code": "CAN", "name": "Canada" },
    { "code": "MEX", "name": "Mexico" }
  ],
  "regions": [{ "region": "NORTH_AMERICA", "countries": ["USA", "CAN", "MEX"] }]
}
```

A matching travel eSIM package offering looks like this — note the one-time price, the coverage features, and `addonCategories` binding it to travel eSIM subscriptions:
```json
{
  "productOfferingId": "2a91cf64-8e05-47d3-b18c-f60a24d9e573",
  "status": "AVAILABLE",
  "name": "North America 5GB",
  "customerType": "CONSUMER",
  "addonCategories": ["PRODUCT_CATEGORY_TRAVEL_ESIM"],
  "product": {
    "productId": "d80e6f21-5a4c-49b7-93d2-6c1e8b0f47a9",
    "internalName": "travel_esim_na_5gb",
    "type": "SUBSCRIPTION_ADDON",
    "category": "PRODUCT_CATEGORY_TRAVEL_ESIM_PACKAGE",
    "features": {
      "dataMb": 5120,
      "validityDays": 30,
      "countries": ["USA", "CAN", "MEX"],
      "regions": ["NORTH_AMERICA"],
      "activationType": "FIRST_USE"
    }
  },
  "price": {
    "netPriceMinor": 1900,
    "currency": "USD",
    "priceType": "ONE_TIME",
    "standardDiscount": {
      "amountMinor": 200
    },
    "currencyOptionsMinor": {
      "USD": 1900,
      "SEK": 18900
    }
  }
}
```

The country filter matches an offering that lists the country, **and** an offering that belongs
to a region with that country in it. A filter of `MEX` thus finds a Mexico-only package and
this North America package.

## Best practices

### Catalog data handling

- Cache the catalog and the offering data with a short TTL. Do not get them on every page view.
  An offering changes far less often than it is read.
- Identify an offering in your own systems by its `productOfferingId`. For a stable mapping
  across environments, use `product.internalName` or `metadata`. Never match on the display
  `name`.
- Price the order before checkout, and charge that number. A catalog price carries no discount,
  no promotion, no price list, and no tax, so it drifts away from what the customer pays.

### Presentation

- Drive your plan picker from `group`. Render one section per `productOfferingGroup`, and sort
  the offerings in it by `price.netPriceMinor`.
- Show what a discounted price becomes when its discount ends.
  `standardDiscount.duration` and `bindingContract.duration` carry the end date.
  `netPriceMinor` alone does not.
- Use `richContent` on a detail page and `description` on a card. Both are optional, so keep a
  fallback for each.
- Obey `customerType`. A consumer and a business see different offerings, and the parameter is
  required on every list call.

### Lifecycle safety

- Take the options endpoints as the authority on an upgrade and a downgrade. A raw catalog
  listing does not know the network, the billing cycle, or the current offering of the
  subscription.
- Send an `X-Idempotency-Key` header on an addon request and on a change request. Every retry
  of that one request must carry the same key and the same body, and the change then happens
  once. A new key starts a separate operation, and a key expires after 24 hours.
- Expect an `ARCHIVED` offering on an existing subscription, and handle it in your rendering and
  in your reporting.

## Next steps

With catalog discovery in place, put the offering IDs to work:

- [Place an order](/developer-guide/use-cases/place-an-order.md) — Turn a chosen product offering into a draft order, price it, and collect payment
- [Customer self-service](/developer-guide/use-cases/self-management.md) — Let customers browse their catalog and change plans from your own UI

## Common questions

**Q: What is the difference between a product and a product offering?**
A: A product is the technical definition of a service: its network, its features, and its
category. A product offering wraps a product with a price and a presentation. An order and a
subscription always point at the offering, not at the product.

**Q: Why does the same offering show different prices at different times?**
A: It does not. The price of an offering is the catalog entry, and it is the same for every
caller. It changes only when somebody edits the offering. What differs per customer is what
they pay. The order resolves that amount. It reads the discounts on the offering, and the promo
code, the price list, and the tax of that customer.

**Q: Can I change a subscription to any offering in the catalog?**
A: No. Call `GET /subscriptions/{subscriptionId}/product-offering-options` for the valid
targets. The platform limits a change by category, by network setup, and by billing cycle. The
response also tells you when each change can take effect.

**Q: What happens to a subscription when its offering is archived?**
A: The subscription keeps running on the archived offering. Archiving stops a new purchase, and
nothing else. Pass `includeArchived=true` when you need an archived offering in a list response.
