Licence Transfer Hooks
The seven hooks moving a software licence to another account and resetting an installation.
Overview
A licence transfer is a separate flow from a service transfer. It keeps its own record, charges its own fee, and moves along on e-mail confirmation from both sides.
Reissuing has nothing to do with transfers: the installation details a licence is tied to are cleared, so the customer can set the software up on another server.
Reference
Stopping a licence transfer
Runs before a licence transfer starts.
Hook::add('gate:service.license_transfer', 10, function ($service, $lt_config) {
// A developer licence given free of charge does not move.
if (!empty($service['options']['developer']))
return 'A developer licence cannot move to another account.';
return null;
});Changing the transfer fee
Runs after the transfer fee was worked out, before the invoice is raised.
type, amount, currency_id, basis, base_amount, base_currency_id. For the percentage type the base is the product's current one-time price (basis = product_price); it falls back to the service amount only when the product has no active one-time price. Zeroing the amount makes the transfer free.Hook::add('filter:service.license_transfer_fee', 10,
function (&$fee, $service, $lt_config) {
// Charge nothing where both sides are the same company.
if (Acme::sameCompany($service)) $fee['amount'] = 0.0;
});Learning that a transfer started
Runs after the transfer record was created. The licence has not moved yet.
Hook::add('action:service.license_transfer_started', 10,
function ($transfer_id, $service_id, $transferor_id, $transferee_id) {
// The licence has NOT moved: it waits on both sides confirming.
Audit::licenseTransfer('started', $transfer_id, $service_id);
});Following one side confirming
Runs where one side confirmed by e-mail. It runs twice per transfer.
transferor (the sender) or transferee (the recipient). Read which side from here, since the hook runs twice.Hook::add('action:license.transfer.verified', 10, function ($transfer, $party) {
// The hook runs TWICE: react to the recipient's confirmation alone.
if ($party === 'transferee') Crm::licenseAccepted($transfer['id'] ?? 0);
});Learning that a transfer completed
Runs after the licence moved to its new owner.
Hook::add('action:service.license_transfer.completed', 10,
function ($transfer_id, $transfer) {
// The record was read afresh: status and time are current.
Acme::licenseMoved((int) ($transfer['owner_id'] ?? 0), $transfer);
});Following a licence reissue
Runs after the licence's installation binding was cleared, so the customer can set the software up on another server.
Hook::add('action:service.license_reissued', 10, function ($service, $uid) {
// The old installation is STILL on the record: you see the previous one.
Acme::forgetInstall($service['options']['install'] ?? '');
});Following a licence update
Runs after an operator saved the service's Management tab and something about the licence changed. Anything holding that licence elsewhere is now stale. Bring your own record up to date here.
Watched fields: the key, the domain, the IP, the version and the licence parameters. Nothing fires when none of them moved. The save already happened, so nothing you return is read.
field => ['old' => …, 'new' => …]. Keys: key, domain, ip, version, parameters.$changes['key'] 3Hook::add('action:service.license_updated', 10,
function ($serviceId, $changes, $service) {
// The key did not move: something else did, so just republish.
if (!isset($changes['key'])) return Acme::republish((int) $serviceId);
$old = (string) $changes['key']['old'];
$new = (string) $changes['key']['new'];
if ($new === '') return; // cleared, not deleted
$old === ''
? Acme::register((int) $serviceId, $new) // issued for the first time
: Acme::rekey($old, $new); // moved onto a new key
});Following a licence report
Runs where a customer submitted a report about their licence.
Hook::add('action:service.license_report_submitted', 10, function ($service, $uid) {
Ops::queue('license-report', (int) ($service['id'] ?? 0));
});Following a transfer ending
Runs when a licence transfer comes to an end. Three separate endings land here: cancelled, rejected and timed out.
cancelled, rejected or expired.Hook::add('action:service.license_transfer_cancelled', 10,
function ($transfer_id, $status, $reason) {
// Lift the transfer lock on the licence server.
Acme::unlockTransfer($transfer_id);
});Following verification being sent again
Runs when the transfer verification emails go out again. They reach only the parties who have not verified yet; nobody who confirmed is written to twice.
Hook::add('action:service.license_transfer.verification_resent', 10,
function ($service, $active, $pending) {
// Nudge over a second channel, only the parties still waiting.
foreach ($pending as $party) Acme::remind($party, (int) ($active['id'] ?? 0));
});Stopping a software domain change
Runs while a customer changes the domain their licence is bound to, after the core checks pass and before anything is written.
Hook::add('gate:service.software_domain_change', 10,
function ($service, $old, $domain, $uid) {
// Apply your own block list.
if (Acme::blockedDomain($domain)) return 'This domain cannot hold a licence.';
return null;
});Following a software domain change
Runs once the bound domain of a licence is saved. This is where you tell the licence server.
Hook::add('action:service.software_domain_changed', 10,
function ($service, $old, $domain, $uid) {
Acme::rebindLicence((int) ($service['id'] ?? 0), $domain);
});Pitfalls
The sender and the recipient confirm separately, and the hook runs on both. A listener reacting without reading the side does its work twice. Check the party value in your first line.
The hook hands you the service record from before the reset: the installation and address fields point at the old server. That is not a fault but your only chance: do whatever you need with the old installation here, because the value is about to go.
The id on the reissue hook is always the service owner. A sub-user or a staff member may have triggered it, and that id never reaches this hook. "Who did this" cannot be answered from here.
The two flows look alike and use separate hooks: a licence transfer has a record of its own, a fee of its own and e-mail confirmation from both sides. A system listening to the service transfer hooks never sees a licence one.
Related Articles
- Service Transfer Hooks
- Service Status Hooks
- Invoice and Payment Hooks
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.