Fetching Subscription Product Relationships
This guide explains how to retrieve available upgrade, downgrade, and swap (UDS) paths for customer subscriptions using the Nue Self-Service API. Understanding available product relationships enables you to build dynamic self-service experiences where customers can modify their subscriptions.
Prerequisites
Before you begin, ensure you have:
- A valid Nue API key
- Subscription names for the subscriptions you want to query
- Basic understanding of REST APIs and JSON
- Familiarity with product relationship concepts (upgrade, downgrade, swap)
Authentication
All product relationship operations require authentication using your Nue API key in the nue-api-key header:
const myHeaders = new Headers();
myHeaders.append("nue-api-key", "YOUR_API_KEY_HERE");
myHeaders.append("Content-Type", "application/json");Understanding Product Relationships
Product relationships in Nue define how subscribed products can be changed. There are three types of relationships:
Relationship Type | Description | Use Case |
|---|---|---|
Upgrade | Move to a higher-tier product | Basic Plan to Professional Plan |
Downgrade | Move to a lower-tier product | Enterprise Plan to Standard Plan |
Swap | Exchange for an equivalent product | US Region to EU Region |
REST Endpoint
GET https://api.nue.io/subscriptions/product-relationships?subscriptionNames={names}Query Parameters
Parameter | Type | Required | Description |
|---|---|---|---|
subscriptionNames | Array[String] | Yes | JSON-encoded array of subscription names to query |
Basic Usage
Fetch Product Relationships for a Single Subscription
const myHeaders = new Headers();
myHeaders.append("nue-api-key", "YOUR_API_KEY_HERE");
myHeaders.append("Content-Type", "application/json");
// Fetch product relationships for a single subscription
const subscriptionNames = ["SUB-000115"];
const encodedNames = encodeURIComponent(JSON.stringify(subscriptionNames));
fetch(`https://api.nue.io/subscriptions/product-relationships?subscriptionNames=${encodedNames}`, {
method: 'GET',
headers: myHeaders
})
.then(response => response.json())
.then(result => {
if (result.status === 'SUCCESS' && result.data) {
console.log('Product relationships retrieved successfully');
// Data is keyed by subscription name
Object.entries(result.data).forEach(([subName, subscription]) => {
console.log(`\nSubscription: ${subscription.subscriptionName}`);
console.log(` Current Product: ${subscription.productSku}`);
console.log(` Current UOM: ${subscription.subscriptionUomName}`);
const options = subscription.options;
// Display upgrade options
if (options.upgrade && options.upgrade.length > 0) {
console.log('\n Upgrade Options:');
options.upgrade.forEach(option => {
option.toProducts.forEach(product => {
console.log(` - ${product.name} (${product.sku})`);
console.log(` Same UOM Only: ${option.sameUomOnly}`);
console.log(` Available from: ${option.startDate}`);
});
});
}
// Display downgrade options
if (options.downgrade && options.downgrade.length > 0) {
console.log('\n Downgrade Options:');
options.downgrade.forEach(option => {
option.toProducts.forEach(product => {
console.log(` - ${product.name} (${product.sku})`);
});
});
}
// Display swap options
if (options.swap && options.swap.length > 0) {
console.log('\n Swap Options:');
options.swap.forEach(option => {
option.toProducts.forEach(product => {
console.log(` - ${product.name} (${product.sku})`);
console.log(` Same Price Swap: ${option.samePriceSwap}`);
});
});
}
});
}
})
.catch(error => console.log('Error:', error));Fetch Product Relationships for Multiple Subscriptions
Query relationships for multiple subscriptions in a single API call:
// Fetch product relationships for multiple subscriptions
const subscriptionNames = ["SUB-000115", "SUB-000116"];
const encodedNames = encodeURIComponent(JSON.stringify(subscriptionNames));
fetch(`https://api.nue.io/subscriptions/product-relationships?subscriptionNames=${encodedNames}`, {
method: 'GET',
headers: myHeaders
})
.then(response => response.json())
.then(result => {
if (result.status === 'SUCCESS' && result.data) {
const subscriptions = Object.values(result.data);
console.log(`Retrieved relationships for ${subscriptions.length} subscriptions`);
// Analyze available options across all subscriptions
const withUpgrades = subscriptions.filter(s => s.options.upgrade?.length > 0);
const withDowngrades = subscriptions.filter(s => s.options.downgrade?.length > 0);
const withSwaps = subscriptions.filter(s => s.options.swap?.length > 0);
console.log(`\nSummary:`);
console.log(` Subscriptions with upgrades: ${withUpgrades.length}`);
console.log(` Subscriptions with downgrades: ${withDowngrades.length}`);
console.log(` Subscriptions with swaps: ${withSwaps.length}`);
// Display details for each subscription
subscriptions.forEach(subscription => {
const options = subscription.options;
const upgradeCount = options.upgrade?.length || 0;
const downgradeCount = options.downgrade?.length || 0;
const swapCount = options.swap?.length || 0;
console.log(`\n${subscription.subscriptionName}:`);
console.log(` Product: ${subscription.productSku}`);
console.log(` Available: ${upgradeCount} upgrade(s), ${downgradeCount} downgrade(s), ${swapCount} swap(s)`);
});
}
})
.catch(error => console.log('Error:', error));Building Self-Service Experiences
Extracting Product Options for UI Display
Build a selection interface for customers to choose their desired product change:
function extractChangeOptions(subscriptionData) {
const options = {
subscriptionName: subscriptionData.subscriptionName,
subscriptionId: subscriptionData.subscriptionId,
currentProduct: {
sku: subscriptionData.productSku,
uom: subscriptionData.subscriptionUomName
},
availableChanges: []
};
const relationshipOptions = subscriptionData.options;
// Process upgrade options
if (relationshipOptions.upgrade) {
relationshipOptions.upgrade.forEach(upgrade => {
upgrade.toProducts.forEach(product => {
options.availableChanges.push({
type: 'upgrade',
relationshipId: upgrade.id,
targetProduct: {
id: product.id,
name: product.name,
sku: product.sku,
priceBookEntries: product.priceBookEntries
},
sameUomOnly: upgrade.sameUomOnly,
startDate: upgrade.startDate
});
});
});
}
// Process downgrade options
if (relationshipOptions.downgrade) {
relationshipOptions.downgrade.forEach(downgrade => {
downgrade.toProducts.forEach(product => {
options.availableChanges.push({
type: 'downgrade',
relationshipId: downgrade.id,
targetProduct: {
id: product.id,
name: product.name,
sku: product.sku,
priceBookEntries: product.priceBookEntries
},
sameUomOnly: downgrade.sameUomOnly,
startDate: downgrade.startDate
});
});
});
}
// Process swap options
if (relationshipOptions.swap) {
relationshipOptions.swap.forEach(swap => {
swap.toProducts.forEach(product => {
options.availableChanges.push({
type: 'swap',
relationshipId: swap.id,
targetProduct: {
id: product.id,
name: product.name,
sku: product.sku,
priceBookEntries: product.priceBookEntries
},
sameUomOnly: swap.sameUomOnly,
samePriceSwap: swap.samePriceSwap,
startDate: swap.startDate
});
});
});
}
return options;
}
// Usage
fetch(`https://api.nue.io/subscriptions/product-relationships?subscriptionNames=${encodedNames}`, {
method: 'GET',
headers: myHeaders
})
.then(response => response.json())
.then(result => {
if (result.status === 'SUCCESS' && result.data) {
Object.values(result.data).forEach(subscription => {
const changeOptions = extractChangeOptions(subscription);
console.log(`\n${changeOptions.subscriptionName}`);
console.log(`Current: ${changeOptions.currentProduct.sku} (${changeOptions.currentProduct.uom})`);
if (changeOptions.availableChanges.length === 0) {
console.log(' No changes available');
return;
}
changeOptions.availableChanges.forEach((change, index) => {
const typeIcon = change.type === 'upgrade' ? '(up)' :
change.type === 'downgrade' ? '(down)' : '(swap)';
console.log(` ${index + 1}. ${typeIcon} ${change.targetProduct.name}`);
console.log(` SKU: ${change.targetProduct.sku}`);
// Show pricing options
if (change.targetProduct.priceBookEntries) {
const prices = change.targetProduct.priceBookEntries
.map(p => `${p.listPrice} ${p.uom.name}`)
.join(', ');
console.log(` Pricing: ${prices}`);
}
});
});
}
});Displaying Pricing Options for Target Products
The response includes full pricing details for target products, allowing you to show customers what their new subscription will cost:
function displayPricingOptions(priceBookEntries) {
console.log(' Available Pricing:');
priceBookEntries.forEach(entry => {
const uom = entry.uom;
const recommended = entry.recommended ? ' (Recommended)' : '';
console.log(` - $${entry.listPrice} per ${uom.name}${recommended}`);
console.log(` Billing: ${entry.billingTiming}`);
console.log(` Price Book Entry ID: ${entry.id}`);
});
}
// Usage within the change options flow
changeOptions.availableChanges.forEach(change => {
console.log(`\nUpgrade to: ${change.targetProduct.name}`);
displayPricingOptions(change.targetProduct.priceBookEntries);
});Response Structure
Success Response (200 OK)
{
"status": "SUCCESS",
"data": {
"SUB-000115": {
"currencyIsoCode": "USD",
"subscriptionName": "SUB-000115",
"subscriptionId": "a0udi00000484tlAAA",
"productSku": "ORDER FORMS UDS",
"subscriptionUomName": "User/Year",
"options": {
"upgrade": [
{
"id": "a0odi000000y2QVAAY",
"relationshipType": "Upgrade",
"sameUomOnly": false,
"startDate": "2025-01-16",
"productRelationshipPriceTags": [
{
"id": "a0xdi000001ABC",
"name": "Upgrade Discount",
"priceTagType": "Discount",
"publishStatus": "Published"
}
],
"fromProduct": {
"id": "01tdi000008IMqIAAW",
"name": "Order Forms uds",
"sku": "ORDER FORMS UDS",
"priceBookEntries": [
{
"id": "01udi00000341kWAAQ",
"listPrice": 100,
"billingTiming": "In Advance",
"uom": {
"id": "a0wdi0000020DSQAA2",
"name": "User/Year",
"quantityDimension": "User",
"termDimension": "Year"
}
}
],
"priceModel": "Recurring",
"productCategory": "RecurringServices"
},
"toProducts": [
{
"id": "01tdi000008IMqXAAW",
"name": "Nue Recurring Test",
"sku": "NUE RECURRING TEST",
"priceBookEntries": [
{
"id": "01udi00000341kXAAQ",
"listPrice": 120,
"billingTiming": "In Advance",
"uom": {
"id": "a0wdi0000020DSQAA2",
"name": "User/Year",
"quantityDimension": "User",
"termDimension": "Year"
}
}
],
"priceModel": "Recurring",
"priceTags": [...]
}
]
}
],
"swap": [
{
"id": "a0odi000000y2QRAAY",
"relationshipType": "Swap",
"sameUomOnly": false,
"samePriceSwap": false,
"startDate": "2025-07-30",
"fromProduct": {...},
"toProducts": [...]
}
]
}
}
}
}Partial Success Response (200 OK)
When some subscriptions are found but others fail, the API returns a partial success with warnings:
{
"status": "PARTIAL_SUCCESS",
"data": {
"SUB-000115": {
"subscriptionName": "SUB-000115",
"subscriptionId": "a0udi00000484tlAAA",
"productSku": "ORDER FORMS UDS",
"subscriptionUomName": "User/Year",
"options": {...}
}
},
"warnings": [
{
"code": "SUBSCRIPTION_NOT_FOUND",
"message": "Subscription not found: SUB-INVALID"
}
]
}Response Fields
Top-Level Response:
Field | Type | Description |
|---|---|---|
status | String | Response status: SUCCESS, PARTIAL_SUCCESS, or error |
data | Object | Object keyed by subscription name |
warnings | Array | (Optional) Array of warning objects when some subscriptions fail |
Warning Object:
Field | Type | Description |
|---|---|---|
code | String | Warning code (e.g., SUBSCRIPTION_NOT_FOUND, SUBSCRIPTION_EVALUATION_FAILED) |
message | String | Human-readable warning message |
Subscription Object:
Field | Type | Description |
|---|---|---|
currencyIsoCode | String | (Optional) Currency ISO code for multi-currency tenants |
subscriptionName | String | Name of the subscription |
subscriptionId | String | Unique subscription identifier |
productSku | String | SKU of the currently subscribed product |
subscriptionUomName | String | Current unit of measure name |
options | Object | Contains upgrade, downgrade, and swap arrays |
Relationship Option Object:
Field | Type | Description |
|---|---|---|
id | String | Unique relationship identifier |
relationshipType | String | Upgrade, Downgrade, or Swap |
sameUomOnly | Boolean | Whether only same UOM products are valid |
samePriceSwap | Boolean | (Swap only) Whether price is preserved |
startDate | String | Date when relationship becomes effective |
productRelationshipPriceTags | Array | (Optional) Price tags/discounts associated with this relationship |
fromProduct | Object | Product object for current product (contains only the subscription's specific price book entry) |
toProducts | Array | Array of target product objects (contains all available price book entries) |
Product Object:
Field | Type | Description |
|---|---|---|
id | String | Product ID |
name | String | Product name |
sku | String | Product SKU |
priceBookEntries | Array | Available pricing options (see note below) |
priceModel | String | Recurring, OneTime, etc. |
priceTags | Array | Applied price/discount tags |
productCategory | String | Product category |
autoRenew | Boolean | Default auto-renewal setting |
creditType | String | Cash or Credit |
creditConversion | Object | (Optional) Credit conversion settings |
defaultRenewTerm | Number | Default renewal term |
evergreen | Boolean | Whether the product is evergreen |
freeTrialType | String | Free trial type |
freeTrialUnit | Number | Free trial duration |
recordType | String | Record type |
showIncludedProductOptions | Boolean | Whether to show included product options |
taxCode | String | Tax code |
taxMode | String | Tax mode |
Note: The priceBookEntries array differs between fromProduct and toProducts:
- fromProduct: Contains only the subscription's specific price book entry (the one the customer is currently subscribed to)
- toProducts: Contains all available active price book entries for the target product
Price Tag Object:
Field | Type | Description |
|---|---|---|
id | String | Price tag ID |
name | String | Price tag name |
code | String | Price tag code |
priceTagType | String | Type of price tag (e.g., Discount) |
priceType | String | Price type |
publishStatus | String | Publish status |
active | Boolean | Whether the tag is active |
recordType | String | Record type |
Price Book Entry Object:
Field | Type | Description |
|---|---|---|
id | String | Price book entry ID |
listPrice | Number | List price amount |
billingTiming | String | In Advance or In Arrears |
recommended | Boolean | Whether this is the recommended option |
uom | Object | Unit of measure details |
uom.name | String | UOM display name (e.g., User/Month) |
uom.quantityDimension | String | Quantity dimension (e.g., User) |
uom.termDimension | String | Term dimension (e.g., Month) |
Empty Options Response
When a subscription has no available product relationships:
{
"status": "SUCCESS",
"data": {
"SUB-000117": {
"subscriptionName": "SUB-000117",
"subscriptionId": "a0udi00000485ABCDE",
"productSku": "STANDALONE-PRODUCT",
"subscriptionUomName": "License",
"options": {}
}
}
}Error Handling
Robust Error Handling
async function safeGetProductRelationships(subscriptionNames) {
const myHeaders = new Headers();
myHeaders.append("nue-api-key", "YOUR_API_KEY_HERE");
myHeaders.append("Content-Type", "application/json");
try {
// Validate input
if (!Array.isArray(subscriptionNames) || subscriptionNames.length === 0) {
throw new Error('subscriptionNames must be a non-empty array');
}
const encodedNames = encodeURIComponent(JSON.stringify(subscriptionNames));
const url = `https://api.nue.io/subscriptions/product-relationships?subscriptionNames=${encodedNames}`;
const response = await fetch(url, {
method: 'GET',
headers: myHeaders
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.status !== 'SUCCESS') {
throw new Error(`API Error: ${result.message || 'Unknown error'}`);
}
return {
success: true,
data: result.data || {},
subscriptions: Object.values(result.data || {})
};
} catch (error) {
console.error('Failed to fetch product relationships:', error);
return {
success: false,
data: {},
subscriptions: [],
error: error.message
};
}
}
// Usage with error handling
safeGetProductRelationships(["SUB-000115", "SUB-000116"])
.then(result => {
if (result.success) {
console.log(`Retrieved relationships for ${result.subscriptions.length} subscriptions`);
// Process result.subscriptions
} else {
console.error('Failed:', result.error);
// Handle error state in UI
}
});Best Practices
Performance
- Batch requests - Query multiple subscriptions in a single API call
- Cache results - Product relationships change infrequently
- Lazy load - Only fetch relationships when the user accesses subscription management
User Experience
- Clear labeling - Use descriptive labels like "Upgrade to Enterprise" instead of just product names
- Show pricing impact - Display price differences using the included priceBookEntries
- Indicate restrictions - Show when sameUomOnly applies to set expectations
- Highlight swap benefits - Indicate when samePriceSwap is true
Integration
- Combine with subscription data - Fetch subscriptions first, then query relationships for active ones
- Use for change orders - After user selects an option, use the relationshipId and target product details to create a change order
- Handle empty states - Gracefully handle subscriptions with no available changes
Next Steps
After retrieving available product relationships, you can:
- Create Change Orders - Execute the selected upgrade, downgrade, or swap using the relationship data
- Fetch Pricing Data - Use the priceBookEntries to show customers accurate pricing
- Update Subscriptions - Modify subscription settings and custom fields
See the Creating Draft Change Orders guide to learn how to implement product changes based on the relationships retrieved from this endpoint.