Licence Transfers
The six endpoints that move a service licence to another client: starting, following, chasing and cancelling.
Overview
A licence transfer moves a service's licence to another client. These six endpoints manage the transfer record: starting it, following it, chasing it and calling it off.
The flow does not finish in one step. Starting only opens the record; both sides confirm by e-mail, the fee invoice is paid if there is one, and only then does the licence change hands. What the transfer is waiting on right now is in the confirmation state on the detail.
This is not a service ownership handover. Changing who owns a service is a separate system with its own endpoints.
Reference
Listing the Transfers
Returns the service's transfer history, newest first.
percentage is a share, fixed a set amount.curl 'https://panel.example.com/api/v1/admin/services/506/license-transfers' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/506/license-transfers', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506/license-transfers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The list does NOT carry the confirmation state; read one transfer's detail to see it.
$transfers = Api::Services()->GetServiceLicenseTransfers(['id' => 506])['data'];Starting a Transfer
Opens a transfer, sends a verification to both sides and raises the fee invoice if there is one.
percentage is a share, fixed a set amount.transferor hands over, transferee takes on.curl -X POST 'https://panel.example.com/api/v1/admin/services/506/license-transfers' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"transferee_id":88,"notes":"sold","notify_parties":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/506/license-transfers', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
transferee_id: 88,
notes: 'sold',
notify_parties: true,
}),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506/license-transfers');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'transferee_id' => 88,
'notes' => 'sold',
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Starting does NOT complete it: the licence stays put until both sides confirm by e-mail.
$transfer = Api::Services()->CreateServiceLicenseTransfer([
'id' => 506,
'transferee_id' => 88,
])['data'];
$waiting = array_filter($transfer['verifications'], fn (array $v): bool => !$v['verified']);{
"data": {
"id": 7,
"service_id": 506,
"transferor_id": 50,
"transferee_id": 88,
"status": "pending_verification",
"fee": { "type": "percentage", "amount": 5.0, "currency_id": 840 },
"invoice_id": 0,
"invoice_recipient": "transferee",
"expires_at": "2026-06-28 12:00:00",
"verifications": [
{
"party": "transferor",
"email": "[email protected]",
"verified": false,
"verified_at": null,
"sent_at": "2026-06-21 12:00:00",
"last_resent_at": null,
"resend_count": 0
},
{
"party": "transferee",
"email": "[email protected]",
"verified": false,
"verified_at": null,
"sent_at": "2026-06-21 12:00:00",
"last_resent_at": null,
"resend_count": 0
}
]
}
}{
"error": {
"code": "transfer_not_allowed",
"message": "License transfer is not enabled for this product."
}
}Transfer Detail
Returns one transfer together with the confirmation state of both sides.
percentage is a share, fixed a set amount.transferor hands over, transferee takes on.curl 'https://panel.example.com/api/v1/admin/services/506/license-transfers/7' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/506/license-transfers/7', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506/license-transfers/7');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// This is where you see what the transfer is waiting on: a confirmation or a payment.
$transfer = Api::Services()->GetServiceLicenseTransfer(['id' => 506, 'tid' => 7])['data'];
$allConfirmed = !array_filter($transfer['verifications'], fn (array $v): bool => !$v['verified']);
$feeUnpaid = $transfer['invoice_id'] > 0;Cancelling a Transfer
Stops a transfer in progress and cancels the unpaid fee invoice behind it.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/506/license-transfers/7' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"notify_parties":true}'const res = await fetch('https://panel.example.com/api/v1/admin/services/506/license-transfers/7', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ notify_parties: true }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506/license-transfers/7');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['notify_parties' => true]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A completed transfer CANNOT be cancelled: taking ownership back needs a new transfer the other way.
$response = Api::Services()->CancelServiceLicenseTransfer(['id' => 506, 'tid' => 7]);Resending a Verification
Sends the verification e-mail again to a side that has not confirmed.
transferor, transferee or both. It defaults to both.curl -X POST 'https://panel.example.com/api/v1/admin/services/506/license-transfers/7/resend' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"party":"both"}'const res = await fetch('https://panel.example.com/api/v1/admin/services/506/license-transfers/7/resend', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ party: 'both' }),
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506/license-transfers/7/resend');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['party' => 'both']),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Asking for both is harmless: a side that confirmed is skipped, and the list says who got one.
$response = Api::Services()->ResendServiceLicenseTransferVerification([
'id' => 506,
'tid' => 7,
'party' => 'both',
]);
$sentTo = $response['data']['resent'];Reminding About the Fee Invoice
Sends a reminder for the transfer fee invoice.
curl -X POST 'https://panel.example.com/api/v1/admin/services/506/license-transfers/7/remind-invoice' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/506/license-transfers/7/remind-invoice', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/506/license-transfers/7/remind-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);// The reminder goes to whoever the invoice is RAISED AGAINST, which can be either side.
$transfer = Api::Services()->GetServiceLicenseTransfer(['id' => 506, 'tid' => 7])['data'];
if ($transfer['invoice_id'] > 0) {
Api::Services()->RemindServiceLicenseTransferInvoice(['id' => 506, 'tid' => 7]);
}Pitfalls
Even when the start request answers 201, the licence is still with its old owner. The transfer only moves once both sides confirm by e-mail and the fee invoice, if any, is paid. The confirmation list on the response says who is being waited on.
The record carries an expiry. If the confirmations are not in by then the transfer lapses on its own and the licence never moves. On one that has been waiting a while, check the deadline before cancelling: it may have lapsed already.
transfer_not_allowed is not one rule but four conditions at once: the licence transfer addon may not be installed, transfers may be off on the product, the service may already have an open transfer, or it may not meet the eligibility rules. The message says which one; reading the code alone is misleading.
The transfer here moves the licence and has its own verification and fee flow. Changing who owns a service, so that its invoices start going to another client, is an entirely different system living on its own endpoints.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.