Ownership Transfers
The four endpoints behind the requests that move a service to another client.
Overview
A client can hand a service over to another client. These four endpoints read those requests and settle them; approval moves the service to the target client.
The id in the paths is the request's, not the service's. Requests are kept as event records, so they carry a number of their own.
This is not a licence transfer. Moving a licence from one installation to another is a separate system with its own endpoints.
Reference
Listing the Requests
Returns the service handover requests, with the waiting ones first.
pending ya da approved.pending is waiting, approved has gone through.curl -G 'https://panel.example.com/api/v1/admin/services/transfer-requests' \
-H "Authorization: Bearer $API_KEY" \
-d status=pendingconst url = new URL('https://panel.example.com/api/v1/admin/services/transfer-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/transfer-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()->GetTransferRequests([], ['status' => 'pending']);{
"data": [
{
"id": 7382,
"service_id": 558,
"status": "pending",
"reason": "Sold to another account",
"created_at": "2026-06-21 18:00:00",
"service": { "id": 558, "name": "example.com", "type": "hosting" },
"from": {
"id": 88,
"full_name": "John Doe",
"company_name": "",
"email": "[email protected]"
},
"to": {
"id": 89,
"full_name": "Jane Doe",
"company_name": "",
"email": "[email protected]"
}
}
],
"meta": { "total": 1, "page": 1, "limit": 25, "next_page": 0 }
}Request Detail
Returns one handover request, with the approval details when it has gone through.
pending is waiting, approved has gone through.curl 'https://panel.example.com/api/v1/admin/services/transfer-requests/7382' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/transfer-requests/7382', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/transfer-requests/7382');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The id in the path is the REQUEST'S id; passing a service id answers 404.
$response = Api::Services()->GetTransferRequest(['eid' => 7382]);Approving a Request
Moves the service to the target client and marks the request approved.
gate:service.transfer_approve hook vetoed the operation. One of your own addons may be blocking the handover.curl -X POST 'https://panel.example.com/api/v1/admin/services/transfer-requests/7382/approve' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/transfer-requests/7382/approve', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/transfer-requests/7382/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 is one way: undoing it means opening a NEW request in the other direction.
$response = Api::Services()->ApproveTransferRequest(['eid' => 7382]);
$movedFrom = $response['data']['old_owner_id'];
$movedTo = $response['data']['new_owner_id'];{
"data": {
"status": "approved",
"id": 7382,
"service_id": 558,
"old_owner_id": 88,
"new_owner_id": 89
}
}{
"error": {
"code": "not_pending",
"message": "Transfer request is not pending."
}
}Deleting a Request
Deletes the request record. It does not touch who owns the service.
curl -X DELETE 'https://panel.example.com/api/v1/admin/services/transfer-requests/7382' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/services/transfer-requests/7382', {
method: 'DELETE',
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/services/transfer-requests/7382');
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 an approved request does NOT undo the handover; only the record goes.
$response = Api::Services()->DeleteTransferRequest(['eid' => 7382]);Pitfalls
These endpoints expect the request id; passing a service id answers 404. Both are numbers, so the mistake never lands on the wrong record quietly, but it is easy to make: in the list id is the request's and service_id is the service's.
Approving an already approved request answers not_pending, and deleting it does not bring the ownership back; only the record goes. The only way to reverse a handover is a new request in the other direction.
A blocked_by_gate error does not mean your request was wrong; it means an addon in the installation refused the handover. That is how non-transferable service types are defined, so if you wrote your own hook, look there first.
The handover here changes who owns the service: the service stays as it is and its invoices start going to another client. Moving a licence from one server to another is entirely different work living on its own endpoints.
Related Articles
Grazie per il tuo feedback!
Il nostro team di assistenza è disponibile 24 ore su 24 per aiutarti a trovare le risposte che cerchi.