Ownership Transfers

7 views Markdown

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

get/api/v1/admin/services/transfer-requests
Services/GetTransferRequests admin paged

Returns the service handover requests, with the waiting ones first.

Query parameters 7
searchstringSearches the name, e-mail, company and service name on either side.
statusstringpending ya da approved.
from_idintFilters by the client handing over.
to_idintFilters by the client taking on.
service_idintFilters by service.
pageintDefaults to 1.
limitintDefaults to 25, maximum 100.
Response fields data[] — 8
idintId of the request. This is an event id, not a service id, and it is what the other endpoints expect.
service_idintId of the service being handed over.
statusstringpending is waiting, approved has gone through.
reasonstringWhy the transfer was asked for.
created_atstringWhen the request was opened.
serviceobjectA summary of the service.
idintService id.
namestringThe service name.
typestringThe service type.
fromobjectThe client handing it over.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
emailstringE-mail address.
toobjectThe client taking it on.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
emailstringE-mail address.
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/transfer-requests' \
  -H "Authorization: Bearer $API_KEY" \
  -d status=pending
const 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']);
Response
{
  "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

get/api/v1/admin/services/transfer-requests/{eid}
Services/GetTransferRequest admin request id

Returns one handover request, with the approval details when it has gone through.

Response fields data — 9
idintId of the request. This is an event id, not a service id, and it is what the other endpoints expect.
service_idintId of the service being handed over.
statusstringpending is waiting, approved has gone through.
reasonstringWhy the transfer was asked for.
created_atstringWhen the request was opened.
serviceobjectA summary of the service.
idintService id.
namestringThe service name.
typestringThe service type.
fromobjectThe client handing it over.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
emailstringE-mail address.
toobjectThe client taking it on.
idintClient id.
full_namestringFirst and last name.
company_namestringCompany name.
emailstringE-mail address.
approvedobject | nullThe approval: who approved it, their name and when. Empty while the request is still waiting.
Errors 2
not_found404No such transfer request.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/services/transfer-requests/{eid}/approve
Services/ApproveTransferRequest admin cannot be undone

Moves the service to the target client and marks the request approved.

Body
No body is needed. The target client is the one named in the stored request and cannot be changed at approval time; send an empty body.
Response fields data — 5
statusstringThe status afterwards.
idintId of the request.
service_idintId of the service handed over.
old_owner_idintId of the previous owner.
new_owner_idintId of the new owner.
Errors 6
not_found404No such request or service.
not_pending422The request has already been approved.
invalid_target422The request carries no target client.
target_not_found422The target client was not found.
blocked_by_gate422The gate:service.transfer_approve hook vetoed the operation. One of your own addons may be blocking the handover.
insufficient_scope403The key lacks the required scope.
Request
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'];
Response
{
  "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

delete/api/v1/admin/services/transfer-requests/{eid}
Services/DeleteTransferRequest admin

Deletes the request record. It does not touch who owns the service.

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

The id in the path is not the service's

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.

Approval cannot be undone

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 hook can block the handover

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.

Do not confuse it with a licence transfer

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.

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.