# Domain DNS Hooks

https://dev.wisecp.com/es/domain-dns-hooks

The ten hooks setting where a domain points: name servers, DNS records, DNSSEC and child name servers.

## Overview

What these four surfaces share: they all work **live**. Reads and writes alike send a real request to the provider, and our record is only a mirror.

Learn the pattern once and all four follow it: a gate in front of a write, an event behind it, and on the read side a filter handing you the list by reference.

## Reference

### Stopping a name server write

gatedomain.nameservers_save

`AdminServices::save_nameservers()` `ClientDomains:197` from both paths

Runs before the name server list is written to the provider, with the input already checked. **Both** paths, panel and customer panel, pass through here.

Parameters 2

$servicearrayThe domain service record.

$dnsarrayThe name server list about to be written. The non-empty `ns1..ns4` values, in order.

Return 1

stringA non-empty string **stops** the action; the text is thrown as the error and the provider is never called. `null` or an empty string lets it carry on.

Listener PHP

```php
Hook::add('gate:domain.nameservers_save', 10, function ($service, $dns) {
    // Changing name servers mid-transfer breaks the transfer.
    if (($service['status'] ?? '') === 'transfer')
        return 'Name servers cannot change while a transfer runs.';

    return null;
});
```

### Following a name server change

actiondomain.nameservers_saved

`AdminServices::save_nameservers()` on success only

Runs after the provider call succeeded, the values landed on the service and the activity was recorded.

Parameters 2

$servicearrayThe domain service record.

$dnsarrayThe list that was written to the provider.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.nameservers_saved', 10, function ($service, $dns) {
    DnsMonitor::resync($service['name'] ?? '', $dns);
});
```

### Stopping a DNS record change

gatedomain.dns_record_save

`AdminServices::save_dns_record()` `AdminServices::delete_dns_record()` three actions, one hook

Runs before a DNS record changes at the provider. Adding, updating and deleting **all three** pass here, and the third parameter says which.

Parameters 3

$servicearrayThe domain service record.

$recordarrayThe record being worked on: `type`, `name`, `value`, `identity`. On an update also `ttl` and `priority`.

$taskstringThe action: `create`, `update` or `delete`.

Return 1

stringA non-empty string **stops** the action; the text is thrown as the error and the provider is never called. `null` or an empty string lets it carry on.

Listener PHP

```php
Hook::add('gate:domain.dns_record_save', 10, function ($service, $record, $task) {
    $type = strtoupper((string) ($record['type'] ?? ''));

    // Stop a delete that cuts the mail flow; adds and updates carry on.
    if ($task === 'delete' && $type === 'MX')
        return 'An MX record cannot be deleted; change the mail settings first.';

    return null;
});
```

### Following a DNS record change

actiondomain.dns_record_saved

`AdminServices::save_dns_record()` on success only

Runs after the change at the provider finished successfully.

Parameters 3

$servicearrayThe domain service record.

$recordarrayThe record that was worked on.

$taskstringThe action that ran.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.dns_record_saved', 10, function ($service, $record, $task) {
    Audit::dns($service['name'] ?? '', $task, $record);
});
```

### Stopping a DNSSEC record action

gatedomain.dnssec_save

`AdminServices` create and delete

Runs before a DS record is created or deleted at the provider.

Parameters 3

$servicearrayThe domain service record.

$recordarrayThe DS record: `digest`, `key_tag`, `digest_type`, `algorithm`. On a delete also `identity`.

$verbstring`create` or `delete`.

Return 1

stringA non-empty string **stops** the action; the text is thrown as the error and the provider is never called. `null` or an empty string lets it carry on.

Listener PHP

```php
Hook::add('gate:domain.dnssec_save', 10, function ($service, $record, $verb) {
    // Deleting the last DS record switches validation off entirely.
    if ($verb === 'delete' && Acme::dsCount((int) ($service['id'] ?? 0)) <= 1)
        return 'The last DNSSEC record cannot be deleted.';

    return null;
});
```

### Following a DNSSEC change

actiondomain.dnssec_saved

`AdminServices` on success only

Runs after the DS record landed at the provider.

Parameters 3

$servicearrayThe domain service record.

$recordarrayThe DS record that was handled.

$verbstringThe action that ran.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.dnssec_saved', 10, function ($service, $record, $verb) {
    Audit::dnssec($service['name'] ?? '', $verb, $record['key_tag'] ?? '');
});
```

### Stopping a child name server action

gatedomain.child_ns_save

`ClientDomains` create and delete

Runs before a child name server is added at the provider or removed from it.

Parameters 4

$servicearrayThe domain service record.

$hoststringThe fully qualified name, such as `ns1.example.com`.

$ipstringThe address behind the name. It can arrive empty on a delete.

$verbstring`create` or `delete`.

Return 1

stringA non-empty string **stops** the action; the text is thrown as the error and the provider is never called. `null` or an empty string lets it carry on.

Listener PHP

```php
Hook::add('gate:domain.child_ns_save', 10, function ($service, $host, $ip, $verb) {
    if ($verb === 'delete') return null;

    // Do not glue a name to an address outside your own block.
    if (!Acme::ownsIp($ip)) return 'That address is not ours.';

    return null;
});
```

### Following a child name server change

actiondomain.child_ns_saved

`ClientDomains` on success only

Runs after the child name server landed at the provider.

Parameters 4

$servicearrayThe domain service record.

$hoststringThe name that was handled.

$ipstringThe address behind it.

$verbstringThe action that ran.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.child_ns_saved', 10, function ($service, $host, $ip, $verb) {
    Ipam::glue($verb, $host, $ip);
});
```

### Changing the child name server list

filterdomain.child_ns_list

`Hook::runRefs` by reference

Runs after the list read from the provider was normalised, before it reaches the screen.

Parameters 2

$listarrayrefThe normalised list: every row carries `ns` and `ip`. Keep the shape; the screen draws to this contract.

$servicearrayThe domain service record.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:domain.child_ns_list', 10, function (&$list, $service) {
    // Sort without breaking the shape: the screen wants ns and ip keys.
    usort($list, fn ($a, $b) => strcmp($a['ns'] ?? '', $b['ns'] ?? ''));
});
```

### Changing the DNSSEC list

filterdomain.dnssec_records

`Hook::runRefs` by reference

Runs after the DS records read from the provider were normalised.

Parameters 2

$recordsarrayrefThe normalised records: `identity`, `digest`, `key_tag`, `digest_type`, `algorithm`. A delete sends `identity` back; do not drop it.

$servicearrayThe domain service record.

Return 1

voidThe value changes **by reference**; the return is not read.

Listener PHP

```php
Hook::add('filter:domain.dnssec_records', 10, function (&$records, $service) {
    // KEEP the identity field: a delete targets the record with it.
    $records = array_values(array_filter($records,
        fn ($r) => (int) ($r['algorithm'] ?? 0) !== 5));   // hide the old algorithm
});
```

## Pitfalls

> **One gate carries three actions**
> 
> The DNS record gate is the **same** hook for adding, updating and deleting. Without reading the third parameter, a rule you wrote to "stop deletes" stops adds as well. The same holds for the DNSSEC and child name server gates, where the action sits in the fourth parameter.

> **Keep the shape in list filters**
> 
> Read filters hand you the array the screen **draws directly**. Renaming a key or dropping `identity` breaks more than the list: it leaves **the delete call** without a target, because the record is found by that field.

> **The gate is on both paths**
> 
> The name server gate sits on the panel path and the customer path alike. A rule written as "the customer should not" stops **the operator** too. To tell the paths apart, look at the calling context rather than at the service record.

> **Propagation is not instant**
> 
> The event says the provider **accepted** the change, not that the world sees it. Propagation takes minutes. A listener acting on the new value gets **the old answer** where it checks straight away.

## Related Articles

- [Domain Acquisition Hooks](https://dev.wisecp.com/en/domain-acquisition-hooks)
- Domain Hooks
- [Writing a Hook Listener](https://dev.wisecp.com/en/writing-a-hook-listener)
