Product Catalog
Discover product catalogs and offerings, understand pricing and billing cycles, and use offerings in orders, add-ons, and subscription changes
Everything a customer can buy through the Connect API — mobile plans, travel eSIMs, add-ons, licenses — is represented as a product offering. This guide shows you how to discover what is available, interpret pricing, resolve the exact set of offerings a specific customer may purchase, and feed offering IDs into orders and subscription changes.
Prerequisites
Before you begin, ensure you have:
- API Credentials: A valid access token and API key for the Connect API
- Customer Context: Whether you are selling to
CONSUMERorBUSINESScustomers - Order Basics: Familiarity with placing an order helps for the later steps
Overview
A typical catalog integration follows this flow:
Explore catalogs
List product catalogs to understand how offerings are segmented.
List offerings
Fetch product offerings, filtered by type, category, or catalog.
Interpret pricing
Read prices, billing cycles, and promotional discounts correctly.
Resolve per-customer catalogs
Fetch the exact offerings and groups available to one customer.
Sell and change
Use offering IDs in orders, add-ons, 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, add-ons, 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 a single list call gives you the full picture: what the service is (product), what it costs (price), and how to present it (group, name, description, imageUrl).
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.
Add-on offerings additionally carry addonCategories: the subscription categories the add-on can
be attached to. For example, a PRODUCT_CATEGORY_TRAVEL_ESIM_PACKAGE add-on 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:
# List product catalogs, optionally filtered by name
curl -X GET "https://apiv2.example.com/api/v2/product-catalogs?filter=US&limit=100" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const listProductCatalogs = async (filter) => {
const query = new URLSearchParams({ limit: '100' });
if (filter) {
query.set('filter', filter);
}
const response = await fetch(`https://apiv2.example.com/api/v2/product-catalogs?${query}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
const { items, pagination } = await response.json();
console.log('Catalogs:', items.length, 'next cursor:', pagination.nextCursor);
return items;
};A catalog listing looks like this:
{
"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 composes: when true, the catalog inherits every offering from the default catalog and adds its own on top; when false, it stands alone with only its explicitly assigned offerings.
Step 2: List Product Offerings
Fetch the offerings themselves. The customerType parameter is required; everything else narrows the result:
types— filter by offering type (SUBSCRIPTION,SUBSCRIPTION_ADDON,LICENSE,EXTERNAL_PRODUCT)categories— filter by product categoryproductCatalogId— only offerings belonging to a specific catalogincludeArchived— includeARCHIVEDofferings (defaultfalse)promoCode— apply promotional pricing to the returned pricescountries/regions— coverage filters for travel eSIM offerings (see below)
# List consumer subscription offerings in a specific catalog
curl -X GET "https://apiv2.example.com/api/v2/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 "https://apiv2.example.com/api/v2/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"// List all consumer subscription offerings, following pagination
const listAllProductOfferings = async () => {
const offerings = [];
let cursor = null;
do {
const query = new URLSearchParams({
customerType: 'CONSUMER',
limit: '100',
});
query.append('types', 'SUBSCRIPTION');
query.set('productCatalogId', 'f47ac10b-58cc-4372-a567-0e02b2c3d479');
if (cursor) {
query.set('cursor', cursor);
}
const response = await fetch(`https://apiv2.example.com/api/v2/product-offerings?${query}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
const page = await response.json();
offerings.push(...page.items);
cursor = page.pagination.nextCursor;
} while (cursor);
return offerings;
};Each item is a full ProductOffering with its product embedded:
{
"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": {
"netPrice": 25,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": {
"period": "MONTHLY",
"interval": 1
}
},
"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 — dataMb, includedCallSeconds, and includedSms for cellular plans; validityDays, countries, regions, and activationType for travel eSIM packages.
An ARCHIVED offering can no longer be ordered, but existing subscriptions may still reference
it. When rendering details for an existing subscription, pass includeArchived=true (or fetch the
offering directly by ID) so lookups do not come back empty.
To fetch a single offering — for example to render a detail page or re-validate before checkout — use its ID:
curl -X GET "https://apiv2.example.com/api/v2/product-offerings/0b54a9c2-7f13-4e8a-b2d6-91c37f5a04e8" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const getProductOffering = async (productOfferingId) => {
const response = await fetch(
`https://apiv2.example.com/api/v2/product-offerings/${productOfferingId}`,
{
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
if (!response.ok) {
throw new Error(`Offering lookup failed: ${response.status}`);
}
return response.json();
};Step 3: Understand Pricing and Billing Cycles
Every offering carries one price object. Read it with these fields:
| Field | Meaning |
|---|---|
netPrice |
The final amount after all discounts are applied — the number to display and charge |
discount |
The discount amount off the original price (present when a discount or promo applies) |
currency |
The currency code, e.g. 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) |
boundMonths |
For recurring prices: the number of months the price is contractually bound |
currencyOptions |
Per-currency price overrides keyed by ISO currency code, for offerings sold in multiple currencies |
The two price types map to two selling motions:
RECURRING— subscriptions, licenses, and recurring add-ons. ThebillingCycledescribes cadence:{ "period": "MONTHLY", "interval": 1 }bills every month.ONE_TIME— single charges such as travel eSIM packages or external products. NobillingCycleis present.
Promotional pricing is applied server-side: pass a promoCode query parameter to the offering list, single offering, or customer catalog endpoints, and discounted prices come back with discount and a reduced netPrice:
# Fetch an offering with a promo code applied
curl -X GET "https://apiv2.example.com/api/v2/product-offerings/0b54a9c2-7f13-4e8a-b2d6-91c37f5a04e8?promoCode=SPRING25" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const getDiscountedOffering = async (productOfferingId, promoCode) => {
const query = new URLSearchParams({ promoCode });
const response = await fetch(
`https://apiv2.example.com/api/v2/product-offerings/${productOfferingId}?${query}`,
{
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
const offering = await response.json();
const { netPrice, discount, currency } = offering.price;
console.log(`Price with ${promoCode}: ${netPrice} ${currency} (discount ${discount ?? 0})`);
return offering;
};With SPRING25 applied, the price block reflects the discount:
{
"price": {
"discount": 5,
"netPrice": 20,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": {
"period": "MONTHLY",
"interval": 1
}
}
}
Step 4: Fetch a Customer’s Product Catalog
To know what one specific customer may buy, do not filter the global list yourself — ask the API. The customer catalog endpoint merges the default catalog with every catalog assigned to that customer and returns the result ready to render:
# By internal customer UUID
curl -X GET "https://apiv2.example.com/api/v2/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 "https://apiv2.example.com/api/v2/customers/rid_crm-customer-12345/product-catalog" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const getCustomerCatalog = async (customerId, promoCode) => {
const query = new URLSearchParams();
if (promoCode) {
query.set('promoCode', promoCode);
}
const response = await fetch(
`https://apiv2.example.com/api/v2/customers/${customerId}/product-catalog?${query}`,
{
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
const catalog = await response.json();
// Group offerings by their group for display
const byGroup = new Map();
for (const offering of catalog.productOfferings ?? []) {
const groupId = offering.group?.productOfferingGroupId ?? 'ungrouped';
const bucket = byGroup.get(groupId) ?? [];
bucket.push(offering);
byGroup.set(groupId, bucket);
}
return { groups: catalog.productOfferingGroups ?? [], byGroup };
};The response contains the groups and the offerings side by side:
{
"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": {
"netPrice": 25,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": { "period": "MONTHLY", "interval": 1 }
},
"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": {
"netPrice": 40,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": { "period": "MONTHLY", "interval": 1 }
},
"group": {
"productOfferingGroupId": "mobile-plans",
"name": "Mobile Plans",
"category": "PRODUCT_CATEGORY_SUBSCRIPTION_CELL"
}
}
]
}
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 Add-Ons
The productOfferingId is the currency of the rest of the platform. In an order, each line item names the offering it purchases:
# Create an order with a subscription line item for a chosen offering
curl -X POST "https://apiv2.example.com/api/v2/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 }
}
]
}'const createOrderForOffering = async (productOfferingId) => {
const response = await fetch('https://apiv2.example.com/api/v2/orders', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
customerType: 'CONSUMER',
lineItems: [
{
type: 'SUBSCRIPTION',
lineItemId: 'line-item-1',
productOfferingId,
sim: { esim: true },
},
],
}),
});
return response.json();
};Add-on offerings (type: SUBSCRIPTION_ADDON) attach to an existing subscription instead. Pick an add-on whose addonCategories includes the subscription’s category, then add it:
# Add a travel eSIM package to an existing travel eSIM subscription
curl -X POST "https://apiv2.example.com/api/v2/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"
}'const addAddon = async (subscriptionId, productOfferingId, scheduledAt) => {
const response = await fetch(
`https://apiv2.example.com/api/v2/subscriptions/${subscriptionId}/addons`,
{
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'X-Idempotency-Key': `addon-${subscriptionId}-${productOfferingId}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
productOfferingId,
// Optional: schedule the add-on for a future date (YYYY-MM-DD)
...(scheduledAt ? { scheduledAt } : {}),
}),
},
);
if (response.status !== 201) {
throw new Error(`Add-on failed: ${response.status}`);
}
return response.json();
};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:
curl -X GET "https://apiv2.example.com/api/v2/subscriptions/e7a12b90-45cd-4f38-9a61-08b3d5c2ef47/product-offering-options" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "X-API-Key: YOUR_API_KEY"const getChangeOptions = async (subscriptionId) => {
const response = await fetch(
`https://apiv2.example.com/api/v2/subscriptions/${subscriptionId}/product-offering-options`,
{
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
const { items } = await response.json();
return items;
};Each option pairs an offering with a change schedule:
{
"items": [
{
"productOffering": {
"productOfferingId": "6d2f8e11-3c49-4b7a-a5e0-84b9d1c72f35",
"name": "Seamless 25GB",
"price": {
"netPrice": 40,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": { "period": "MONTHLY", "interval": 1 }
}
},
"changeSchedule": "INSTANT",
"changeScheduleDate": "2026-07-12"
},
{
"productOffering": {
"productOfferingId": "9f3b6d84-2c71-4a5e-b90d-57e1f8a3c266",
"name": "Seamless 5GB",
"price": {
"netPrice": 15,
"currency": "USD",
"priceType": "RECURRING",
"billingCycle": { "period": "MONTHLY", "interval": 1 }
}
},
"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:
curl -X PUT "https://apiv2.example.com/api/v2/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"
}'const changeSubscriptionOffering = async (subscriptionId, productOfferingId, earliestDate) => {
const response = await fetch(
`https://apiv2.example.com/api/v2/subscriptions/${subscriptionId}/product-offering-change`,
{
method: 'PUT',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
'X-Idempotency-Key': `change-${subscriptionId}-${productOfferingId}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
productOfferingId,
// Optional: earliest date (YYYY-MM-DD) to perform the change on.
// If the change schedule does not fit this date, the earliest date after it is chosen.
...(earliestDate ? { scheduledAt: earliestDate } : {}),
}),
},
);
const subscription = await response.json();
console.log('Change scheduled for subscription:', subscription.subscriptionId);
return subscription;
};The same options-then-change pattern exists for add-ons and licenses:
GET /subscriptions/{subscriptionId}/addons/product-offering-options?currentProductOfferingId=...andPUT /subscriptions/{subscriptionId}/addons/product-offering-changefor changing an existing add-onGET /licenses/{licenseId}/product-offering-optionsandPUT /licenses/{licenseId}/product-offering-changefor 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:
# List all countries and regions covered by travel eSIM offerings
curl -X GET "https://apiv2.example.com/api/v2/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 "https://apiv2.example.com/api/v2/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"const getTravelCoverage = async () => {
const query = new URLSearchParams({ customerType: 'CONSUMER' });
const response = await fetch(
`https://apiv2.example.com/api/v2/product-offerings/countries?${query}`,
{
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
},
);
const { countries, regions } = await response.json();
return { countries, regions };
};
const listPackagesForCountry = async (countryCode) => {
const query = new URLSearchParams({ customerType: 'CONSUMER' });
query.append('types', 'SUBSCRIPTION_ADDON');
query.append('categories', 'PRODUCT_CATEGORY_TRAVEL_ESIM_PACKAGE');
query.append('countries', countryCode);
const response = await fetch(`https://apiv2.example.com/api/v2/product-offerings?${query}`, {
method: 'GET',
headers: {
Authorization: 'Bearer YOUR_ACCESS_TOKEN',
'X-API-Key': 'YOUR_API_KEY',
},
});
const { items } = await response.json();
return items;
};The coverage response deduplicates countries across all offerings and lists each region with its constituent countries:
{
"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:
{
"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": {
"netPrice": 19,
"currency": "USD",
"priceType": "ONE_TIME"
}
}
The country filter matches offerings that list the country explicitly or belong to a region that includes it, so filtering by MEX finds both a Mexico-only package and this North America package.
Best Practices
Catalog Data Handling
- Cache catalog and offering data with a short TTL rather than fetching on every page view; offerings change far less often than they are read
- Identify offerings in your own systems by
productOfferingId, and useproduct.internalNameormetadatafor stable cross-environment mapping — never match on displayname - Re-fetch the offering (or the customer catalog) with the active
promoCoderight before checkout so displayed and charged prices cannot drift
Presentation
- Drive plan pickers from
group— render one section perproductOfferingGroupand sort offerings within it byprice.netPrice - Use
richContentfor detail pages anddescriptionfor cards; both are optional, so fall back gracefully - Respect
customerType: consumer and business customers see different offerings, and the parameter is required on list calls
Lifecycle Safety
- Treat the options endpoints as the source of truth for upgrades and downgrades; a raw catalog listing does not know the subscription’s network, billing cycle, or current offering
- Send an
X-Idempotency-Keyheader on add-on and change requests so retries never double-apply - Expect
ARCHIVEDofferings on existing subscriptions and handle them in rendering and reporting
Next Steps
With catalog discovery in place, put the offering IDs to work:
Place an order
Turn a chosen product offering into a draft order, price it, and collect payment
Customer self-service
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 service definition (network, features, category); a product offering wraps a product with a price and presentation. Orders and subscriptions always reference the offering, not the product.
Q: Why does the same offering show different prices at different times?
A: Prices reflect the request context. A promoCode parameter applies promotional discounts (discount and a reduced netPrice), and currencyOptions may override the price per currency.
Q: Can I change a subscription to any offering in the catalog?
A: No. Use GET /subscriptions/{subscriptionId}/product-offering-options to see the valid targets — the platform constrains changes by category, network setup, and billing cycle, and tells you when each change can take effect.
Q: What happens to subscriptions when their offering is archived?
A: They keep running on the archived offering. Archiving only prevents new purchases; use includeArchived=true when you need archived offerings in list responses.