This guide shows you how to implement self-service upgrades and downgrades using product groups in your application.
Overview
The upgrade/downgrade flow consists of three main steps:
- Check eligibility - Get available change options for a subscription item
- Display options - Show the customer available upgrades/downgrades
- Apply change - Execute the selected package change
Prerequisites
Before implementing upgrades:
- Create a Product Group with a tier for each package level
- Assign the product group to the subscription item via
PUT /subscription-items/{id}/product-group
Step 1: Get Change Options
Fetch available upgrade/downgrade options for a subscription item:
curl -X GET "https://coreapi.io/subscription-items/{itemId}/change-options" \
-H "Authorization: Bearer YOUR_API_KEY"
Response
{
"current": {
"id": "tier-starter-uuid",
"name": "Starter Plan",
"pricePlan": {
"id": "price-starter-uuid",
"name": "Starter Monthly",
"price": 2900,
"billingInterval": "monthly"
},
"product": {
"id": "product-starter-uuid",
"name": "Starter"
}
},
"options": [
{
"id": "tier-pro-uuid",
"label": "Pro",
"product": {
"id": "product-pro-uuid",
"name": "Pro Plan"
},
"pricePlans": [
{
"id": "price-pro-monthly-uuid",
"name": "Pro Monthly",
"price": 4900,
"billingInterval": "monthly"
}
],
"changeTiming": "immediately",
"creditType": "pro_rata",
"isUpgrade": true,
"isDowngrade": false,
"quantitySetting": null
}
],
"hasPendingChange": false
}
Response Fields
| Field | Description |
|---|
current | Currently active tier and price plan |
options | Available upgrade/downgrade options |
options[].isUpgrade | true if this is an upgrade |
options[].isDowngrade | true if this is a downgrade |
options[].changeTiming | When the change takes effect |
options[].creditType | How unused time is credited |
hasPendingChange | true if a change is already scheduled |
If hasPendingChange is true, no further changes can be made until the pending change is applied.
Step 2: Display Options to Customer
Use the response to build a selection UI:
async function getUpgradeOptions(subscriptionItemId) {
const response = await fetch(
`/subscription-items/${subscriptionItemId}/change-options`,
{ headers: { Authorization: `Bearer ${token}` } }
);
const data = await response.json();
// Check if changes are possible
if (data.hasPendingChange) {
return { error: 'A change is already pending' };
}
if (data.options.length === 0) {
return { error: 'No upgrade options available' };
}
// Separate upgrades and downgrades
const upgrades = data.options.filter(o => o.isUpgrade);
const downgrades = data.options.filter(o => o.isDowngrade);
return { current: data.current, upgrades, downgrades };
}
Step 3: Apply the Change
When the customer selects an option, apply the change:
curl -X POST "https://coreapi.io/product-group-memberships/{tierId}/apply" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"subscriptionItem": "subscription-item-uuid",
"quantity": 1
}'
Request Parameters
| Parameter | Type | Required | Description |
|---|
subscriptionItem | string | Yes | UUID of the subscription item to change |
selectedPricePlan | string | No | UUID of the price plan. Only required if the tier has multiple price plans. In most cases, this can be omitted. |
quantity | integer | No | New quantity. Only applied if the tier allows quantity changes. |
sendInvoiceEmail | boolean | No | Send credit note via email (default: true) |
Response
{
"subscriptionItem": {
"id": "item-uuid",
"product": { "id": "...", "name": "Pro Plan" },
"pricePlan": { "id": "...", "name": "Pro Monthly" },
"status": "active"
},
"creditNote": {
"id": "credit-note-uuid",
"number": "CN-2026-0001",
"amount": 1450
}
}
Change Timing Behavior
The change takes effect right away:
- Current subscription item is ended
- Credit note is created (based on
creditType)
- New subscription item is created and starts immediately
- New invoice is generated
End of Period
The change is scheduled for the next billing cycle:
- Current subscription item continues until period end
- At period end, item switches to new product/plan
- No credit note is created
- Next invoice uses new pricing
Credit Types
When changeTiming is immediately, a credit is issued based on creditType:
| Type | Formula | Example |
|---|
pro_rata | Price × (Days remaining / Days in period) | €29 × (15/30) = €14.50 |
full | Full period price | €29.00 |
last_invoiced | Amount from last invoice | Varies |
none | No credit | €0.00 |
Complete Example
async function upgradeSubscription(subscriptionItemId, targetTierId) {
// 1. Get current options to validate
const options = await fetch(
`/subscription-items/${subscriptionItemId}/change-options`,
{ headers: { Authorization: `Bearer ${token}` } }
).then(r => r.json());
if (options.hasPendingChange) {
throw new Error('Cannot change: pending change exists');
}
// 2. Find the selected option
const selectedOption = options.options.find(o => o.id === targetTierId);
if (!selectedOption) {
throw new Error('Invalid tier selected');
}
// 3. Apply the change
const result = await fetch(
`/product-group-memberships/${targetTierId}/apply`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
subscriptionItem: subscriptionItemId,
quantity: 1
})
}
).then(r => r.json());
return {
newItem: result.subscriptionItem,
creditNote: result.creditNote
};
}
Error Handling
| HTTP Status | Cause | Solution |
|---|
| 400 | Invalid request | Check required fields |
| 404 | Subscription item not found | Verify item UUID |
| 422 | Pending change exists | Wait for pending change to complete |
| 422 | Not eligible for change | Check tier eligibility settings |
Webhooks
Listen for these events to track changes:
| Event | Description |
|---|
subscription.updated | Subscription items changed |
invoice.created | New invoice for upgraded subscription |
credit_note.created | Credit note for prorated amount |