# Renewal and Cancellation

https://dev.wisecp.com/es/renewal-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

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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/renewal-invoice' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 201 422

```json
{
  "data": {
    "invoice_id": 1234,
    "service_id": 529
  }
}
```

```json
{
  "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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/renewal-invoice' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
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}'
```

```javascript
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();
```

```php
$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);
```

```php
// 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.

statusstring`pending` 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.

statusstring`pending` is waiting, `approved` has gone through.

urgencystring`now` 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.

serviceobject 3 fieldsA summary of the service.

idintService id.

namestringThe service name.

typestringThe service type.

clientobject 4 fieldsThe 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -G 'https://panel.example.com/api/v1/admin/services/cancellation-requests' \
  -H "Authorization: Bearer $API_KEY" \
  -d status=pending
```

```javascript
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();
```

```php
$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);
```

```php
$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.

statusstring`pending` is waiting, `approved` has gone through.

urgencystring`now` 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.

serviceobject 3 fieldsA summary of the service.

idintService id.

namestringThe service name.

typestringThe service type.

clientobject 4 fieldsThe client who opened it.

idintClient id.

full_namestringFirst and last name.

company_namestringCompany name.

emailstringE-mail address. Only on the detail.

remainingobject 3 fieldsThe 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/services/cancellation-requests/91' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/services/cancellation-requests/91', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/cancellation-requests/91/accept' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/cancellation-requests/91' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/cancel-subscription' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/529/addons/44/cancel-subscription' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
$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 JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/services/482/remove-subscription' \
  -H "Authorization: Bearer $ADMIN_KEY"
```

```javascript
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();
```

```php
$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);
```

```php
// 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`.

## Related Articles

- [Service Lifecycle](https://dev.wisecp.com/en/service-lifecycle)
- [Add-ons on a Service](https://dev.wisecp.com/en/add-ons-on-a-service)
- [Service Endpoints](https://dev.wisecp.com/en/service-endpoints)
