Renewal and Cancellation

9 views Markdown

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

post/api/v1/admin/services/{id}/renewal-invoice
Services/GenerateServiceRenewalInvoice admin 201

Produces a renewal invoice for the service, running by hand the same path the cron uses.

Body
No body is needed; send an empty one. The renewal settings — the source, the add-on gathering, the metrics, the notification and the hook — are fixed on the server and cannot be set from here.
Response fields data — 2
invoice_idintId of the invoice produced.
service_idintService id.
Errors 3
not_found404No such service.
renewal_skipped422No invoice was produced. The term may already be invoiced, renewal invoicing may be switched off on the service, the term may be invalid, or client data may be missing; the message says which.
insufficient_scope403The key lacks the required scope.
Request
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'];
}
Response
{
  "data": {
    "invoice_id": 1234,
    "service_id": 529
  }
}
{
  "error": {
    "code": "renewal_skipped",
    "message": "Renewal skipped: this period is already invoiced."
  }
}

An Add-on Renewal Invoice

post/api/v1/admin/services/{id}/addons/{addon_id}/renewal-invoice
Services/GenerateAddonRenewalInvoice admin 201

Produces a renewal invoice for one add-on on its own.

Body
No body is needed; send an empty one.
Response fields data — 2
invoice_idintId of the invoice produced.
addon_idintId of the add-on record.
Errors 3
not_found404No such service or add-on.
renewal_skipped422No invoice was produced.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/cancel-refund
Services/CancelAndRefundService admin cannot be undone

Cancels the service and, if you ask, refunds the part of the term that was not used.

Body 2
refundstringHow to refund: none not at all, credit to the client's balance, cash as an expense record. Defaults to none.
apply_on_moduleboolCancels the account at the provider too.
Response fields data — 5
statusstringThe status afterwards.
idintService id.
refundstringThe refund you asked for.
refundedboolWhether a refund actually happened. Asking is not enough: with no balance left this comes back false.
applied_on_moduleboolWhether it reached the provider.
Errors 3
not_found404No such service.
already_cancelled422The service is already cancelled.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/services/cancellation-requests
Services/GetCancellationRequests admin paged

Returns the cancellation requests clients opened, with the waiting ones first.

Query parameters 4
searchstringSearches the client name, e-mail, company and service name.
statusstringpending or approved.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 9
idintId of the request. An event id, not a service id.
service_idintId of the service being cancelled.
user_idintId of the client who opened it.
statusstringpending is waiting, approved has gone through.
urgencystringnow means straight away, period_ending at the end of the term. This decides what approval does.
reasonstringThe reason the client gave.
created_atstringWhen the request was opened.
serviceobjectA summary of the service.
idintService id.
namestringThe service name.
typestringThe service type.
clientobjectThe client who opened it.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
emailstringE-mail address. Only on the detail.
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/cancellation-requests' \
  -H "Authorization: Bearer $API_KEY" \
  -d status=pending
const 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

get/api/v1/admin/services/cancellation-requests/{eid}
Services/GetCancellationRequest admin the remainder is worked out

Returns one request together with the used and remaining part of the term.

Response fields data — 11
idintId of the request. An event id, not a service id.
service_idintId of the service being cancelled.
user_idintId of the client who opened it.
statusstringpending is waiting, approved has gone through.
urgencystringnow means straight away, period_ending at the end of the term. This decides what approval does.
reasonstringThe reason the client gave.
created_atstringWhen the request was opened.
serviceobjectA summary of the service.
idintService id.
namestringThe service name.
typestringThe service type.
clientobjectThe client who opened it.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
emailstringE-mail address. Only on the detail.
remainingobjectThe used and remaining part of the term.
used_daysintHow many days of the term were used.
remaining_daysintHow many days of the term are left.
remaining_amountfloatThe amount those days are worth. This is the number to read before deciding on a refund.
approvedobject | nullThe approval: who approved it, their name and when. Empty while the request is still waiting.
Errors 2
not_found404No such cancellation request.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/cancellation-requests/{eid}/accept
Services/AcceptCancellationRequest admin follows the urgency

Approves the request, and cancels the service there and then when the urgency says so.

Body
No body is needed; send an empty one. The urgency and the reason come from the request as the client saved it, and approval cannot override either.
Response fields data — 4
statusstringThe status afterwards.
idintId of the request.
service_idintService id.
cancelled_nowboolWhether the service was cancelled by this request. False means it was left to the end of the term.
Errors 4
not_found404No such request or service.
already_approved422The request has already been approved.
blocked_by_gate422The gate:service.cancellation_accept hook vetoed the operation.
insufficient_scope403The key lacks the required scope.
Request
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

delete/api/v1/admin/services/cancellation-requests/{eid}
Services/DeleteCancellationRequest admin

Deletes the request record. It does not touch the service status.

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

post/api/v1/admin/services/{id}/cancel-subscription
Services/CancelServiceSubscription admin at the payment gateway

Cancels the recurring payment subscription behind the service at the payment gateway.

Body
No body is needed; send an empty one. The subscription is found from the service.
Response fields data — 3
statusstringThe subscription status afterwards.
subscription_idintId of the subscription.
service_idintService id.
Errors 4
not_found404No such service.
subscription_not_found422The service has no subscription.
subscription_cancel_failed500The payment gateway could not cancel the subscription.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/addons/{addon_id}/cancel-subscription
Services/CancelServiceAddonSubscription admin at the payment gateway

Cancels the recurring payment subscription behind an add-on.

Body
No body is needed; send an empty one. The subscription is found from the service and the add-on record.
Response fields data — 3
statusstringThe subscription status afterwards.
subscription_idintId of the subscription.
addon_idintId of the add-on record.
Errors 4
not_found404No such service or add-on.
subscription_not_found422The add-on has no subscription.
subscription_cancel_failed500The payment gateway could not cancel the subscription.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/{id}/remove-subscription
Services/RemoveServiceFromSubscription the agreement lives on

Takes one service out of its agreement while the other members stay.

Body
No body is needed; send an empty one. The service comes from the path.
Response fields data — 4
statusstringWhat the removal came to.
subscription_statusstringWhere the agreement stands after the removal. It turns to cancelled once the last member leaves.
subscription_idintThe agreement the service left.
service_idintThe service taken out.
Errors 3
insufficient_scope403The key lacks the required scope.
not_found404No such service.
subscription_not_found422The service is bound to no agreement.
Request
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

Cancelling the subscription leaves the service running

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 does not refund

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.

Asking for a refund is not getting one

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.

A skip is not a failure

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 request endpoints want an event id

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.