Upgrades and Downgrades
The nine endpoints that move a service to another product, bill the difference and follow the change through.
Overview
When a client changes product two things happen at once: the money is worked out and the server is changed. These nine endpoints run both. They show which products are reachable and what the difference costs, then open a record and follow it to the end.
The change is not immediate. When the record opens, flow on the response says how it proceeds. Waiting for payment holds it until the invoice is settled, scheduling holds it until the term ends, and queueing hands it to a background job.
Reference
Reading the Options
Returns the products the service can move to, each with its price and the difference.
up to upgrade, down to downgrade. Defaults to up.curl -X POST 'https://panel.example.com/api/v1/admin/services/529/upgrade-options' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"grade":"up"}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/upgrade-options', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ grade: 'up' }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/upgrade-options');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['grade' => 'up']),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The price id comes from HERE; do not assume the catalogue's price id.
$options = Api::Services()->GetUpgradeOptions(['id' => 529, 'grade' => 'up'])['data'];
$target = $options[0];
$price = $target['prices'][0];
Api::Services()->CreateUpdowngrade([
'id' => 529,
'product_id' => $target['id'],
'price_id' => $price['price_id'],
]);{
"data": [
{
"id": 16,
"title": "Pro SSD 2",
"type": "hosting",
"module": "cpanel",
"category": "Hosting",
"prices": [
{
"price_id": 12722,
"period": "month",
"period_time": 1,
"cycle": "monthly",
"amount": 185.82,
"difference": 185.82,
"tax_amount": 37.16,
"payable": 222.99,
"currency": 840
}
]
}
]
}Creating the Change
Opens an upgrade or downgrade record. How it proceeds follows the settings you send.
up or down. Defaults to up.none no invoice, unpaid an unpaid one, paid one treated as settled.none or credit to the client's balance.invoice_unpaid waits for payment, scheduled was left to the end of the term, queued was taken into processing.recreate when consent is needed. On that response no record was opened.gate:service.upgrade hook vetoed the operation.curl -X POST 'https://panel.example.com/api/v1/admin/services/529/updowngrade' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"product_id":16,"price_id":12722,"type":"up","invoice_generation":"unpaid","notification":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/updowngrade', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
product_id: 16,
price_id: 12722,
type: 'up',
invoice_generation: 'unpaid',
notification: true,
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/updowngrade');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'product_id' => 16,
'price_id' => 12722,
'type' => 'up',
'invoice_generation' => 'unpaid',
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Services()->CreateUpdowngrade([
'id' => 529,
'product_id' => 16,
'price_id' => 12722,
]);
// On a warning NO RECORD EXISTS: consent and send the request AGAIN.
if (($response['data']['warning'] ?? '') === 'recreate') {
Api::Services()->CreateUpdowngrade([
'id' => 529,
'product_id' => 16,
'price_id' => 12722,
'confirm_recreate' => true,
]);
}{
"data": {
"flow": "invoice_unpaid",
"updowngrade_id": 75,
"invoice_id": 1222
}
}{
"data": {
"warning": "recreate",
"warning_key": "updown-recreate-warning"
}
}Listing the Records
Returns the upgrade and downgrade records that were opened.
up ya da down.up is an upgrade, down a downgrade.waiting, pending, inprocess, completed or cancelled.curl -G 'https://panel.example.com/api/v1/admin/services/updowngrades' \
-H "Authorization: Bearer $API_KEY" \
-d status=waitingconst url = new URL('https://panel.example.com/api/v1/admin/services/updowngrades');
url.searchParams.set('status', 'waiting');
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();$url = 'https://panel.example.com/api/v1/admin/services/updowngrades?' . http_build_query(['status' => 'waiting']);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$response = Api::Services()->GetUpdowngrades([], ['status' => 'waiting']);Record Detail
Returns one change record. The schema is the same as a list item.
up is an upgrade, down a downgrade.waiting, pending, inprocess, completed or cancelled.curl 'https://panel.example.com/api/v1/admin/services/updowngrades/74' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/updowngrades/74', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/updowngrades/74');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);$record = Api::Services()->GetUpdowngrade(['uid' => 74])['data'];
// Why a record is stuck is in status_msg.
$why = $record['status_msg'];Approving a Record
Handles the refund if there is one and takes the record into processing, which a background job runs.
curl -X POST 'https://panel.example.com/api/v1/admin/services/updowngrades/74/approve' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/updowngrades/74/approve', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/updowngrades/74/approve');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Approval handles the refund ONCE; approving the same record again produces no second refund.
$response = Api::Services()->ApproveUpdowngrade(['uid' => 74]);Retrying a Record
Puts a stuck record back into processing.
curl -X POST 'https://panel.example.com/api/v1/admin/services/updowngrades/74/retry' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/updowngrades/74/retry', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/updowngrades/74/retry');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Read the reason before retrying: the same condition stalls it in the same place.
$record = Api::Services()->GetUpdowngrade(['uid' => 74])['data'];
if ($record['status_msg'] === '') {
Api::Services()->RetryUpdowngrade(['uid' => 74]);
}Completing a Record
Finishes by hand a change that has no module to run it.
curl -X POST 'https://panel.example.com/api/v1/admin/services/updowngrades/74/complete' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/updowngrades/74/complete', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/updowngrades/74/complete');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Do not call this on a service with a module: the background job runs that change.
$response = Api::Services()->CompleteUpdowngrade(['uid' => 74]);Deleting a Record
Deletes the record, cancels the unpaid invoice behind it and stops the pending jobs.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/updowngrades/74' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/updowngrades/74', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/updowngrades/74');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Deleting a completed record does NOT undo the change: the service stays on the new product.
$response = Api::Services()->DeleteUpdowngrade(['uid' => 74]);Cancelling a Scheduled Downgrade
Calls off a downgrade left to the end of the term before it reaches the service.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/cancel-scheduled-downgrade' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/cancel-scheduled-downgrade', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/cancel-scheduled-downgrade');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The path takes the SERVICE id, not the record id: the pending plan is found from the service.
$response = Api::Services()->CancelScheduledDowngrade(['id' => 529]);Pitfalls
When the change needs the module to rebuild the account, the first request answers a warning and opens no record at all. To make it happen you add the consent and send the request again. No updowngrade_id on the response means nothing happened.
The price id you send when creating a change is the one from the options endpoint. Using a catalogue price directly opens the change on the wrong term or currency. The amounts in the options list already have tax and exchange worked out.
The delete is there to call off a pending record: it also cancels the unpaid invoice and stops the pending jobs. Deleting a completed one does not put the service back on the old product. Reversing it means opening a new change in the other direction.
Why a record stalled is written in status_msg. Retrying while the same condition holds stalls it in the same place, so clear the cause first. A change with no module never progresses on its own anyway; the complete endpoint is there for that.
The other eight endpoints work on a record id. Cancelling a scheduled downgrade takes the service id instead, because the pending plan is found from the service. Passing a record id answers 404.
Related Articles
Vielen Dank für Ihre Rückmeldung!
Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.