Ownership Transfers

674 visualizações 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.

Foi útil?

Obrigado pela sua opinião!

Ainda precisa de ajuda?

A nossa equipa de apoio está disponível a qualquer hora para ajudar com o que não encontrar acima.