Renewal and Cancellation
The nine endpoints that run the end of a term: renewal invoices, cancellation requests, refunds and subscriptions.
Overview
When a term ends there are two ways it can go: renewed, or finished. These nine endpoints run both. They produce the renewal invoice, settle the client's cancellation request, close the service with a refund, and stop the payment subscription.
Four separate things are easy to conflate. The renewal invoice asks for money. The cancellation request is what the client wants. Cancel and refund closes the service and gives money back. Cancelling the subscription only stops the automatic charging. None of them does another on its own.
Reference
A Service Renewal Invoice
Produces a renewal invoice for the service, running by hand the same path the cron uses.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/renewal-invoice' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/renewal-invoice', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/renewal-invoice');
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);// A skip is a DECISION, not a failure; the message says why.
$response = Api::Services()->GenerateServiceRenewalInvoice(['id' => 529]);
if (($response['error']['code'] ?? '') === 'renewal_skipped') {
$why = $response['error']['message'];
}{
"data": {
"invoice_id": 1234,
"service_id": 529
}
}{
"error": {
"code": "renewal_skipped",
"message": "Renewal skipped: this period is already invoiced."
}
}An Add-on Renewal Invoice
Produces a renewal invoice for one add-on on its own.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/renewal-invoice' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44/renewal-invoice', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44/renewal-invoice');
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);// A service renewal already gathers the add-ons; this endpoint is for renewing one ON ITS OWN.
$response = Api::Services()->GenerateAddonRenewalInvoice([
'id' => 529,
'addon_id' => 44,
]);Cancel and Refund
Cancels the service and, if you ask, refunds the part of the term that was not used.
none not at all, credit to the client's balance, cash as an expense record. Defaults to none.curl -X POST 'https://panel.example.com/api/v1/admin/services/529/cancel-refund' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"refund":"credit","apply_on_module":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/529/cancel-refund', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ refund: 'credit', apply_on_module: true }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/cancel-refund');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'refund' => 'credit',
'apply_on_module' => true,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The refund you asked for may NOT have happened: 'refunded' decides, not 'refund'.
$response = Api::Services()->CancelAndRefundService([
'id' => 529,
'refund' => 'credit',
]);
$paid = $response['data']['refunded'] ?? false;Listing the Cancellation Requests
Returns the cancellation requests clients opened, with the waiting ones first.
pending or approved.pending is waiting, approved has gone through.now means straight away, period_ending at the end of the term. This decides what approval does.curl -G 'https://panel.example.com/api/v1/admin/services/cancellation-requests' \
-H "Authorization: Bearer $API_KEY" \
-d status=pendingconst url = new URL('https://panel.example.com/api/v1/admin/services/cancellation-requests');
url.searchParams.set('status', 'pending');
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const body = await res.json();$url = 'https://panel.example.com/api/v1/admin/services/cancellation-requests?' . http_build_query(['status' => 'pending']);
$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()->GetCancellationRequests([], ['status' => 'pending']);Cancellation Request Detail
Returns one request together with the used and remaining part of the term.
pending is waiting, approved has gone through.now means straight away, period_ending at the end of the term. This decides what approval does.curl 'https://panel.example.com/api/v1/admin/services/cancellation-requests/91' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/cancellation-requests/91', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/cancellation-requests/91');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Read the remaining amount HERE before deciding on a refund; the cancel endpoint never asks.
$request = Api::Services()->GetCancellationRequest(['eid' => 91])['data'];
$owed = $request['remaining']['remaining_amount'];Accepting a Request
Approves the request, and cancels the service there and then when the urgency says so.
gate:service.cancellation_accept hook vetoed the operation.curl -X POST 'https://panel.example.com/api/v1/admin/services/cancellation-requests/91/accept' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/cancellation-requests/91/accept', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/cancellation-requests/91/accept');
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);// Approving refunds NOTHING. If money has to go back, call the cancel-and-refund endpoint too.
$response = Api::Services()->AcceptCancellationRequest(['eid' => 91]);
$stoppedNow = $response['data']['cancelled_now'] ?? false;Deleting a Request
Deletes the request record. It does not touch the service status.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/cancellation-requests/91' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/cancellation-requests/91', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/cancellation-requests/91');
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 request is not a refusal: nothing is said to the client.
$response = Api::Services()->DeleteCancellationRequest(['eid' => 91]);Cancelling a Service Subscription
Cancels the recurring payment subscription behind the service at the payment gateway.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/cancel-subscription' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/cancel-subscription', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/cancel-subscription');
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);// Cancelling the subscription does not cancel the SERVICE: charging stops, the service keeps running.
Api::Services()->CancelServiceSubscription(['id' => 529]);
Api::Services()->CancelService(['id' => 529]);Cancelling an Add-on Subscription
Cancels the recurring payment subscription behind an add-on.
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/cancel-subscription' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/529/addons/44/cancel-subscription', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/529/addons/44/cancel-subscription');
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);$response = Api::Services()->CancelServiceAddonSubscription([
'id' => 529,
'addon_id' => 44,
]);Taking a Service Out of an Agreement
Takes one service out of its agreement while the other members stay.
curl -X POST 'https://panel.example.com/api/v1/admin/services/482/remove-subscription' \
-H "Authorization: Bearer $ADMIN_KEY"const res = await fetch(`https://panel.example.com/api/v1/admin/services/${serviceId}/remove-subscription`, {
method: 'POST',
headers: { Authorization: `Bearer ${adminKey}` },
});
const { data } = await res.json();
if (data.subscription_status === 'cancelled') refreshAgreement();$ch = curl_init('https://panel.example.com/api/v1/admin/services/' . $serviceId . '/remove-subscription');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $adminKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// TAKING OUT differs from CANCELLING: this call takes only this service and the agreement runs on.
// The billing side is re-priced too; to end the whole agreement use cancel-subscription.
$r = Api::Services()->RemoveServiceFromSubscription(['id' => $serviceId])['data'];Pitfalls
The subscription endpoints stop the recurring charge at the gateway and nothing else. The service keeps running and still gets a renewal invoice when its term ends. That invoice now goes unpaid, because the automatic charge is gone. If the service is meant to end too, call the cancel endpoint as well.
Accepting a cancellation request closes the service but gives no money back. To return the unused term you have to call the cancel-and-refund endpoint separately, reading the amount from the calculation on the request detail.
The refund field in the body says what you asked for; refunded on the response says what happened. With no balance left no refund is made and the request still answers 200. Mixing the two ends with the client being told about a refund that never happened.
renewal_skipped does not mean your request was malformed. It means no invoice should be raised for that service right now: the term may already be invoiced, renewal invoicing may be off, or data may be missing. The reason is in the message, and retrying will not help.
The id on the cancellation request endpoints is the request's, not the service's. In the list id belongs to the request and service_id to the service; passing a service id answers 404.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.