# Domain Contact Hooks

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

The ten hooks over the person behind a domain, its privacy and its transfer lock.

## Overview

These three go together. Contact details are what the registry **requires**, privacy hides them from outside, and the transfer lock stops the name moving without permission.

All three are written live to the provider. Saved contact profiles, on the other hand, live **on our side**: they are templates kept so a customer types the same details once, and they carry hooks of their own.

## Reference

### Stopping a contact write

gatedomain.contacts_save

`AdminServices` all four roles

Runs before the contact details are written to the provider.

Parameters 2

$servicearrayThe domain service record.

$whoisarrayThe four roles: `registrant`, `administrative`, `technical`, `billing`. Each role carries name, surname, e-mail, phone and address fields.

Return 1

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

Listener PHP

```php
Hook::add('gate:domain.contacts_save', 10, function ($service, $whois) {
    // The registrant e-mail cannot change before it is verified.
    $mail = (string) ($whois['registrant']['EMail'] ?? '');
    if ($mail !== '' && !Acme::verified($mail))
        return 'The registrant e-mail wants verifying first.';

    return null;
});
```

### Following a contact change

actiondomain.contacts_saved

`AdminServices` on success only

Runs after the contact details reached the provider.

Parameters 2

$servicearrayThe domain service record.

$whoisarrayThe four roles as written to the provider.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.contacts_saved', 10, function ($service, $whois) {
    Audit::whois($service['name'] ?? '', $whois['registrant']['EMail'] ?? '');
});
```

### Stopping a privacy change

gatedomain.privacy_change

`AdminServices` on and off

Runs before privacy changes at the provider.

Parameters 2

$servicearrayThe domain service record.

$statusstringThe state being asked for: `enable` or `disable`. What was asked, not what it became.

Return 1

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

Listener PHP

```php
Hook::add('gate:domain.privacy_change', 10, function ($service, $status) {
    // Some extensions offer no privacy at all.
    if ($status === 'enable' && Acme::noPrivacyTld($service['name'] ?? ''))
        return 'This extension offers no privacy.';

    return null;
});
```

### Following the privacy state

actiondomain.privacy_changed

`AdminServices` the new state is a bool

Runs after privacy changed at the provider.

Parameters 2

$servicearrayThe domain service record.

$enabledboolThe new state: `true` means privacy is on. Unlike the text value at the gate, this one is a boolean.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.privacy_changed', 10, function ($service, $enabled) {
    Billing::privacyFee((int) ($service['id'] ?? 0), (bool) $enabled);
});
```

### Stopping a transfer lock change

gatedomain.transfer_lock_change

`AdminServices` on and off

Runs before the lock changes at the provider.

Parameters 2

$servicearrayThe domain service record.

$statusstringThe state being asked for: `enable` to lock, `disable` to open.

Return 1

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

Listener PHP

```php
Hook::add('gate:domain.transfer_lock_change', 10, function ($service, $status) {
    // Opening the lock is the first step to the name moving away.
    if ($status === 'disable' && Acme::recentlyChangedOwner($service))
        return 'The lock stays on for 60 days after an owner change.';

    return null;
});
```

### Following the lock state

actiondomain.transfer_lock_changed

`AdminServices` the new state is a bool

Runs after the lock changed at the provider.

Parameters 2

$servicearrayThe domain service record.

$lockedboolThe new state: `true` means locked.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.transfer_lock_changed', 10, function ($service, $locked) {
    if (!$locked) Ops::watch('domain-unlocked', $service['name'] ?? '');
});
```

### Changing the lock state reported

filterdomain.transfer_lock_status

`Hook::runRefs` by reference

Runs before the lock state reaches a screen or an API answer.

Parameters 2

$lockedboolrefThe state about to be reported. Where the provider says nothing, you can fill it in here.

$servicearrayThe domain service record.

Return 1

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

Listener PHP

```php
Hook::add('filter:domain.transfer_lock_status', 10, function (&$locked, $service) {
    // Fill from our own record where the provider does not know.
    if ($locked === null) $locked = Acme::lockMirror((int) ($service['id'] ?? 0));
});
```

### Stopping a transfer code request

gatedomain.epp_code_get

`AdminServices` the code goes to the owner

Runs before the transfer code is asked of the provider.

Parameters 2

$servicearrayThe domain service record.

$serviceIdintThe service id.

Return 1

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

Listener PHP

```php
Hook::add('gate:domain.epp_code_get', 10, function ($service, $serviceId) {
    // The transfer code is the key to the name changing hands.
    if (Acme::openDispute($serviceId)) return 'No code while a dispute is open.';

    return null;
});
```

### Following a transfer code request

actiondomain.epp_code_retrieved

`AdminServices` two kinds of value

Runs after the code came back from the provider.

Parameters 2

$servicearrayThe domain service record.

$codestring|boolThe code itself, or `true`. `true` means "the code was not returned, it was e-mailed to the owner".

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.epp_code_retrieved', 10, function ($service, $code) {
    // NEVER log the code: it is the key to the name.
    Audit::note('epp-requested', $service['name'] ?? '',
        $code === true ? 'sent by e-mail' : 'shown on screen');
});
```

### Stopping a contact profile save

gatedomain.whois_profile_save

`ClientDomains` on our side

Runs before a saved contact profile is written to the database. This one **never reaches** the provider; a profile is a template kept on our side.

Parameters 3

$uidintThe account id owning the profile. An **account** arrives here, not a service.

$informationarrayThe contact fields: `FirstName`, `LastName`, `Company`, `EMail`, `Phone`, address fields.

$profileIdint`0` for a new profile; a value above zero is the id of the one being edited.

Return 1

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

Listener PHP

```php
Hook::add('gate:domain.whois_profile_save', 10,
    function ($uid, $information, $profileId) {
        if ($profileId > 0) return null;                  // leave edits alone
        if (Acme::profileCount($uid) >= 10) return 'At most 10 profiles are kept.';
        return null;
    });
```

### Following a profile save

actiondomain.whois_profile_saved

`ClientDomains` the id is filled in now

Runs after the profile was saved.

Parameters 3

$uidintThe account id.

$profileIdintThe id of the saved profile. Filled in even for a new one.

$informationarrayThe fields that were saved.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.whois_profile_saved', 10,
    function ($uid, $profileId, $information) {
        Acme::indexProfile($uid, $profileId, $information['EMail'] ?? '');
    });
```

## Pitfalls

> **The transfer code is the key to the name**
> 
> Anyone holding the code can move the domain to another provider. Do **not** log the value the event hands you, put it in a notification or send it to an outside system. Where the value is `true` the code already went to the owner by e-mail; you do not hold it, and you should not.

> **Text at the gate, a boolean at the event**
> 
> The privacy and lock gates hand you **the state asked for as text** (`enable`/`disable`), while the events hand you **the state reached as a boolean**. Mixing them up fails silently: a text value always counts as true.

> **Profile hooks carry an account, not a service**
> 
> The first parameter of the saved-profile hooks is an **account id**, not a service record. A listener used to the other domain hooks reaches for `$service['name']` here and finds nothing. A profile belongs to an account, not to a domain.

> **Changing the registrant can trigger verification**
> 
> Changing the registrant e-mail starts **re-verification** on most extensions, and an unverified name can be suspended. A listener touching the contact write should be written knowing that; the verification state has a filter of its own.

## Related Articles

- [Domain Acquisition Hooks](https://dev.wisecp.com/en/domain-acquisition-hooks)
- [Domain DNS Hooks](https://dev.wisecp.com/en/domain-dns-hooks)
- Domain Hooks
