Service Transfer Hooks
The seven hooks moving a service to a new owner: the request, the recipient's approval, a refusal, and calling it off.
Overview
A transfer wants consent from both sides: the current owner starts it and the recipient approves. A pending record sits between them and either side can call it off.
These hooks do not tell types apart: hosting, servers and domains all pass the same flow. Writing a rule meant for domains, check the service type yourself.
Reference
Stopping a transfer request
Runs where the current owner starts the transfer, before the pending record is written.
type field can be domain too.Hook::add('gate:service.transfer_request', 10,
function ($service, $from_id, $to_id) {
// A free developer licence does not move: put your own rule here.
if (Acme::nonTransferable($service))
return 'This service cannot move to another account.';
return null;
});Following a transfer request
Runs after the pending transfer record was created. The service is still with the old owner.
Hook::add('action:service.transfer.requested', 10,
function ($service, $data, $uid) {
// $data IS THE PAYLOAD (not the event row): read its fields directly.
Audit::transferOpened((int) ($service['id'] ?? 0),
(int) ($data['from_id'] ?? 0), (int) ($data['to_id'] ?? 0));
});Stopping a transfer approval
Runs where the recipient accepts, before ownership moves.
Hook::add('gate:service.transfer_approve', 10,
function ($service, $from_id, $to_id) {
// Check the recipient too: no service moves to an unverified account.
if (!Acme::verifiedAccount($to_id))
return 'The receiving account is not verified.';
return null;
});Learning that a transfer completed
Runs after ownership moved to the new account.
id, full_name, email.Hook::add('action:service.transfer.approved', 10,
function ($serviceId, $fromId, $toId, $newOwner) {
// Cut the old owner off: the service is no longer theirs.
Acme::revokeAccess($fromId, $serviceId);
Acme::grantAccess($toId, $serviceId);
});Following a refused transfer
Runs where the recipient refused the transfer.
service_id, service_name, the sending and receiving sides. This hook hands you no service record; take the id from here where you need it.Hook::add('action:service.transfer.rejected', 10, function ($me, $data) {
// No service record arrives: take the id from the payload.
Crm::transferRefused((int) ($data['service_id'] ?? 0), (int) ($data['from_id'] ?? 0));
});Following a transfer called off
Runs where the current owner called off a pending transfer.
Hook::add('action:service.transfer.cancelled', 10, function ($service, $pending) {
// Here $pending IS THE RECORD ROW: the payload sits one level in.
$payload = $pending['data'] ?? [];
Audit::transferClosed((int) ($service['id'] ?? 0), (int) ($payload['to_id'] ?? 0));
});Following an invitation sent again
Runs where the owner sent the transfer invitation again. The pending record does not change; only the message is repeated.
Hook::add('action:service.transfer.resent', 10, function ($service, $pending) {
// It repeats: hold back the resends on your own side.
Acme::countResend((int) ($service['id'] ?? 0));
});Pitfalls
On the transfer request the second parameter is the payload; on the called-off and resent hooks the same slot carries the record row, with the payload one level in. Taking one for the other reads empty fields: no error, no value.
The transfer hooks do not look at the service type. A rule you wrote for hosting also lands on domain transfers. Targeting one type, check the type field on the service record in your first line.
The refusal hook hands you the refusing account and the transfer data alone; there is no service record. A listener used to the other transfer hooks looks for the service name and finds nothing. Take the id from the payload and read the record yourself.
The request gate guards the sending side and the approval gate the receiving one. "This service cannot move" belongs at the request gate; "this account cannot receive" belongs at the approval gate. Writing both in one place leaves the other end open.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.