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
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.