Upgrades and Downgrades

8 Aufrufe Markdown

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

post/api/v1/admin/services/{id}/upgrade-options
Services/GetUpgradeOptions admin prices worked out

Returns the products the service can move to, each with its price and the difference.

Body 1
gradestringThe direction: up to upgrade, down to downgrade. Defaults to up.
Response fields data[] — 6
idintId of the target product.
titlestringThe product name.
typestringThe product type.
modulestringThe product's server module.
categorystringThe product category.
pricesarrayThe price options for moving to this product. Tax, currency and cycle are already worked out.
price_idintId of the price. This is what you send when creating the change.
periodstringThe period unit.
period_timeintThe period multiplier.
cyclestringThe billing cycle.
amountfloatThe new per-period amount.
differencefloatThe difference against the current service.
tax_amountfloatThe tax amount.
payablefloatThe total payable with tax.
currencyintCurrency id.
Errors 2
not_found404No such service.
insufficient_scope403The key lacks the required scope.
Request
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'],
]);
Response
{
  "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

post/api/v1/admin/services/{id}/updowngrade
Services/CreateUpdowngrade admin the server picks the flow

Opens an upgrade or downgrade record. How it proceeds follows the settings you send.

Body 9
product_idintrequiredId of the target product.
price_idintThe price id from the options list. Left out, the default price is used.
typestringup or down. Defaults to up.
invoice_generationstringThe invoice mode: none no invoice, unpaid an unpaid one, paid one treated as settled.
refundstringThe refund on a downgrade: none or credit to the client's balance.
scheduleboolPuts a downgrade off until the term ends. The client keeps the term they paid for.
notificationboolSends the client a notification.
pmethodstringThe payment method for the invoice.
confirm_recreateboolConsents to the module rebuilding the account. When it is needed the first request opens no record and returns a warning instead.
Response fields data — 5
flowstringThe flow that ran: invoice_unpaid waits for payment, scheduled was left to the end of the term, queued was taken into processing.
updowngrade_idintId of the record opened.
invoice_idintId of the invoice produced. Zero when there is none.
warningstringComes back as recreate when consent is needed. On that response no record was opened.
warning_keystringThe language key for the warning.
Errors 5
not_found404No such service.
product_required422No target product was given.
blocked_by_gate422The gate:service.upgrade hook vetoed the operation.
subscription_cancel_failed500The old payment subscription could not be cancelled.
insufficient_scope403The key lacks the required scope.
Request
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,
    ]);
}
Response
{
  "data": {
    "flow": "invoice_unpaid",
    "updowngrade_id": 75,
    "invoice_id": 1222
  }
}
{
  "data": {
    "warning": "recreate",
    "warning_key": "updown-recreate-warning"
  }
}

Listing the Records

get/api/v1/admin/services/updowngrades
Services/GetUpdowngrades admin paged

Returns the upgrade and downgrade records that were opened.

Query parameters 5
searchstringSearches the invoice number, the product name and the client.
typestringup ya da down.
statusstringFilters by status.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 13
idintId of the record.
service_idintId of the service changing.
user_idintClient id.
invoice_idintId of the invoice behind it. Zero when none was produced.
typestringup is an upgrade, down a downgrade.
statusstringwaiting, pending, inprocess, completed or cancelled.
status_msgstringA message on the status. On a failed record the error itself is here.
refundstringHow a downgrade refunds.
old_product_idintId of the current product.
new_product_idintId of the target product.
created_atstringWhen the record was opened.
clientobjectWho owns the service.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
detailsobjectThe old and new product side by side.
old_namestringName of the current product.
new_namestringName of the target product.
old_categorystringCategory of the current product.
new_categorystringCategory of the target product.
old_amountfloatThe current per-period amount.
new_amountfloatThe new per-period amount.
differencefloatThe difference between the two.
currency_idintCurrency id.
Meta 4
totalintTotal records matching the filter.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page. Zero means you are on the last one.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl -G 'https://panel.example.com/api/v1/admin/services/updowngrades' \
  -H "Authorization: Bearer $API_KEY" \
  -d status=waiting
const 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

get/api/v1/admin/services/updowngrades/{uid}
Services/GetUpdowngrade admin

Returns one change record. The schema is the same as a list item.

Response fields data — 13
idintId of the record.
service_idintId of the service changing.
user_idintClient id.
invoice_idintId of the invoice behind it. Zero when none was produced.
typestringup is an upgrade, down a downgrade.
statusstringwaiting, pending, inprocess, completed or cancelled.
status_msgstringA message on the status. On a failed record the error itself is here.
refundstringHow a downgrade refunds.
old_product_idintId of the current product.
new_product_idintId of the target product.
created_atstringWhen the record was opened.
clientobjectWho owns the service.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
detailsobjectThe old and new product side by side.
old_namestringName of the current product.
new_namestringName of the target product.
old_categorystringCategory of the current product.
new_categorystringCategory of the target product.
old_amountfloatThe current per-period amount.
new_amountfloatThe new per-period amount.
differencefloatThe difference between the two.
currency_idintCurrency id.
Errors 2
not_found404No such record.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/updowngrades/{uid}/approve
Services/ApproveUpdowngrade admin

Handles the refund if there is one and takes the record into processing, which a background job runs.

Body
No body is needed. The record comes from the id in the path; send an empty body.
Response fields data — 2
statusstringThe status afterwards.
idintId of the record.
Errors 3
not_found404No such record.
already_completed422A completed record cannot be approved.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/updowngrades/{uid}/retry
Services/RetryUpdowngrade admin

Puts a stuck record back into processing.

Body
No body is needed. The record comes from the id in the path; send an empty body.
Response fields data — 2
statusstringThe status afterwards.
idintId of the record.
Errors 3
not_found404No such record.
already_completed422A completed record cannot be retried.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/updowngrades/{uid}/complete
Services/CompleteUpdowngrade admin for changes without a module

Finishes by hand a change that has no module to run it.

Body
No body is needed. The record comes from the id in the path; send an empty body.
Response fields data — 2
statusstringThe status afterwards.
idintId of the record.
Errors 3
not_found404No such record.
complete_failed500Completing it failed.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/services/updowngrades/{uid}
Services/DeleteUpdowngrade admin the invoice is cancelled too

Deletes the record, cancels the unpaid invoice behind it and stops the pending jobs.

Response fields data — 2
deletedboolWhether the delete succeeded.
idintId of the deleted record.
Errors 2
not_found404No such record.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/cancel-scheduled-downgrade
Services/CancelScheduledDowngrade admin

Calls off a downgrade left to the end of the term before it reaches the service.

Body
No body is needed. The service comes from the id in the path; send an empty body.
Response fields data — 2
cancelledboolWhether it was called off.
idintId of the record called off.
Errors 2
not_found404No scheduled downgrade was found.
insufficient_scope403The key lacks the required scope.
Request
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

On a warning response no record exists

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 comes from the options list

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.

Deleting does not undo a completed change

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.

Read the reason before retrying

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.

Cancelling a scheduled downgrade wants the service id

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.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.