Domain Catalogue Hooks
The ten hooks running while a customer searches for a name and an operator builds the extension catalogue: availability, suggestions, categories, the price matrix.
Overview
Selling domains has two sides. On the customer's side there is the search: a name is typed, the provider is asked, a result and suggestions come back. On the operator's side there is the catalogue: which extensions sell, in which category, at what price.
Every hook here works by reference: you are handed an array, you change it, and your return is not read. The shape contract is tight, because the screen and the cart read the output directly.
Reference
Changing the availability answer
Runs after the provider's availability answer was worked out, before the response is built.
false takes the name off sale.Hook::add('filter:domain.availability', 10,
function (&$available, $sld, $tld, $check_result) {
// Brand protection: nobody registers our own name.
if (Acme::brandTerm($sld)) $available = false;
});Changing the customer search result
Runs after the primary result and the suggestions were built, the cart state was applied and the suggestions were sorted.
status, type (register or transfer), currency, primary (the primary result or null), suggestions. Cart state arrives on each entry as in_cart.sld, tld, transfer, ucid (the display currency).Hook::add('filter:domain.client_search_results', 10,
function (&$response, $searchContext) {
// Sorting already ran: put your suggestion FIRST, not last.
$extra = Acme::suggest($searchContext['sld'] ?? '');
if ($extra) array_unshift($response['suggestions'], $extra);
});Changing the featured extensions
Runs after the list from the language file was lower-cased and any leading dot trimmed.
["net","org","io"]). A dot or a capital makes the extension unfindable.Hook::add('filter:domain.spotlight_tlds', 10, function (&$tlds) {
$tlds = ['com', 'net', 'co.uk']; // no dot, lower case
});Changing the extension categories
Runs after the category keys were matched with their translated labels.
key and label. The key has to match the one on the extension rows; an invented key returns no extensions.Hook::add('filter:domain.tld_categories', 10, function (&$out) {
// The key HAS TO MATCH the category list on the extension rows.
$out[] = ['key' => 'local', 'label' => 'Local'];
});Changing the extension price table
Runs after the extension rows were built and the prices resolved into the display currency.
name (without a dot), categories (comma-separated keys), the register and renew prices.Hook::add('filter:domain.tld_table', 10, function (&$out, $ucid) {
// Hide rows without a price so nobody clicks a priceless extension.
$out = array_values(array_filter($out,
fn ($r) => (float) ($r['register'] ?? 0) > 0));
});Changing the renewal price
Runs after the extension's renewal price was found and the currency conversion and year multiplier applied.
id, name, min_years.Hook::add('filter:domain.premium_renewal_price', 10,
function (&$renewal_amount, $service, $tld, $target_currency) {
// A premium name renews at the provider's price, not the catalogue's.
$real = Acme::premiumRenewal($service['name'] ?? '', $target_currency);
if ($real > 0) $renewal_amount = $real;
});Changing the price matrix before it is saved
Runs after the costs pulled from the provider were turned into prices with the profit rate, before they are written.
[type][year][currency] → cost and promo. Types: register, renewal, transfer.module, tld, cost_cid, profit_rate.Hook::add('filter:domain.pricing_save', 10, function (&$pricing, $context) {
// Sell the first year at cost; leave renewals alone.
foreach ($pricing['register'][1] ?? [] as $cid => $row)
$pricing['register'][1][$cid]['promo'] = $row['cost'];
});Changing a new extension record
Runs before a new extension is written to the database.
name, module, status, rank, dns_manage, forwarding, whois_privacy, epp_code.Hook::add('filter:domain.tld_save_data', 10, function (&$insert_data, $extension) {
// A new extension arrives switched off, so nothing sells without a price.
$insert_data['status'] = 0;
});Stopping an extension delete
Runs before an extension is deleted from the catalogue.
Hook::add('gate:domain.tld_delete', 10, function ($extension, $tld) {
// Deleting an extension with sold names leaves those records orphaned.
if (Acme::soldCount((int) ($tld['id'] ?? 0)) > 0)
return 'Names are sold on this extension; it cannot be deleted.';
return null;
});Following a new extension
Runs after the extension was created in the catalogue.
Hook::add('action:domain.tld_created', 10,
function ($tld_id, $extension, $registrar) {
Ops::note('tld-added', $extension . ' -> ' . ($registrar ?: 'no module'));
});Taking over a single lookup
Runs before the availability question is asked for one extension. Return a filled answer and the system asks neither WHOIS nor the registrar: you answered.
sld, tld, module. An empty module puts you on the WHOIS path, a filled one on the registrar path. Both pass through here.status (available, unavailable or error), with optional message, premium, premium_price. An empty array, false or no return lets the system carry on. With several listeners answering, the last one holds.Hook::add('filter:domain.available', 10, function ($query) {
// Never ask the registrar about names on your block list.
if (Acme::blocked($query['sld'] ?? ''))
return ['status' => 'unavailable', 'message' => 'this name is not offered'];
return false; // let the system ask for the rest
});Changing the collected result map
Runs once every extension has answered, right before the result goes back to the caller. You hold the whole map, not one extension.
sld, tld, status, and may carry message, premium, premium_price.sld and tlds.Hook::add('filter:domain.whois_result', 10, function (&$result, $context) {
foreach ($result as $domain => $row) {
if (($row['premium'] ?? false) !== true) continue;
// Add your own margin to the premium price.
$result[$domain]['premium_price'] = round(((float) $row['premium_price']) * 1.15, 2);
}
});Masking the raw record
Runs before the raw text from the registrar is shown to a visitor. That text holds somebody else's name, email and phone, so this is where masking belongs.
sld, tld, status.Hook::add('filter:domain.whois_record', 10, function (&$raw, $whoisContext) {
if ($raw === '') return;
// Hide the email and phone lines.
$raw = preg_replace('~^(.*(?:Email|Phone).*)$~mi', '[hidden]', $raw);
});Following a document requirement change
Runs after the required document list of an extension changes. The change reaches backwards: orders already waiting on that extension may now need a different document.
Hook::add('action:domain.doc_saved', 10, function ($tld, $added, $updated, $removed) {
// A new document means waiting orders on that extension deserve a second look.
if ($added) Acme::reviewPendingOrders($tld);
});Pitfalls
The suggestions are sorted by state before you see them. A suggestion appended to the list stays at the bottom, where most customers never look. To put yours forward, place it at the front.
The featured list and the table rows want the extension without a dot and in lower case. Writing .COM breaks the match: the extension is found in no catalogue, no price resolves, and the screen quietly shows nothing.
Both the renewal price and the table rows hand you a converted amount. Write your own price in that same currency; dropping the provider's cost in as-is shows the customer a number from another currency.
Adding a key to the category list opens the button and nothing else. That key also has to appear in the category list on the extension rows, or a customer pressing it sees an empty list.
Related Articles
- Domain Acquisition Hooks
- Order Flow Hooks
- Domain Hooks
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.