# Customer Account and Contact Hooks

https://dev.wisecp.com/es/client-account-and-contact-hooks

The fourteen hooks over the customer account and site contact: API keys, the profile, currency, the contact form and the newsletter.

## Overview

The traces a customer or visitor leaves on the site live here: API keys, the profile, the currency preference, the contact form and the newsletter.

In most of these hooks the data comes from an **unverified source**: the visitor filling in the contact form, the address signing up, the bot requesting a page that does not exist. Check it yourself before carrying it to an outside service.

## Reference

### Following API key permissions changing

actionclient.api_key_updated

`AccountApiKeys` the new permissions only

Runs after the permissions of an API key change. A widening of permissions is a security event.

Parameters 3

$owner_idintThe owner of the key.

$idintThe id of the key record.

$permsarrayThe **new** permission set. ? The old set is not passed: to answer "did permissions widen" you must have **kept the previous state yourself**.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.api_key_updated', 10, function ($owner_id, $id, $perms) {
    // The old set is not passed: keep it yourself to compare.
    Acme::recordScopes($id, $perms);
});
```

### Following an API key being regenerated

actionclient.api_key_regenerated

`AccountApiKeys` the record stays

Runs after the value of an API key is regenerated. **The record stays, the credential changes**: calls made with the old value no longer pass.

Parameters 2

$owner_idintThe owner of the key.

$idintThe record id; it is the same after the regeneration.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.api_key_regenerated', 10, function ($owner_id, $id) {
    // The old value no longer passes: refresh your own cache.
    Acme::invalidateCachedKey($id);
});
```

### Following an API key being deleted

actionclient.api_key_deleted

`AccountApiKeys` the record is gone

Runs after an API key is deleted.

Parameters 2

$owner_idintThe owner of the key.

$idintThe id of the deleted record; the row is gone.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.api_key_deleted', 10, function ($owner_id, $id) {
    Acme::revokeIntegration($id);
});
```

### Following a customer profile being updated

actionclient.profile_updated

`AccountProfile` the account, not the login

Runs after a customer profile is updated.

Parameters 3

$uidintThe id of the **account** whose profile changed. ? It is **not** the id of whoever signed in: a sub-user with the right permission can update another account’s profile. If you record the actor, read it from the session separately.

$data_updatesarrayThe fields written to the main record, **new values only**: name, phone, language, currency.

$info_updatesarrayWhat was written to the extra record: customer kind, company details, address fields.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.profile_updated', 10, function ($uid, $data_updates, $info_updates) {
    // $uid is the ACCOUNT id, not the login.
    Acme::syncCrm($uid, $data_updates + $info_updates);
});
```

### Following a currency change

actionclient.currency_changed

`website` empty for a guest

Runs when a visitor changes the site currency. The change is **already applied**.

Parameters 3

$new_cidintThe new currency.

$old_cidintThe previous currency.

$memberarrayThe session of the signed-in member. It is **empty for a guest**: this hook runs for visitors who are not signed in too.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.currency_changed', 10, function ($new_cid, $old_cid, $member) {
    // For a guest the member data arrives EMPTY.
    if (!$member) return;

    Acme::rememberCurrency((int) ($member['id'] ?? 0), $new_cid);
});
```

### Following a page that was not found

actionclient.page_not_found

`website` bot traffic included

Runs when a requested address does not resolve. It is the most direct way to find broken links.

Parameters 1

$urlstringThe full address requested. ? This hook also runs on addresses tried by **scanning bots**, and on a site those can outnumber human traffic. A listener writing a record on every call fills its own table with noise.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.page_not_found', 10, function ($url) {
    // Bot traffic can outnumber human traffic: filter it.
    if (Acme::looksLikeScanner($url)) return;

    Acme::noteBrokenLink($url);
});
```

### Following a contact form submission

actionclient.contact_submitted

`ClientContact` comes from a visitor

Runs after a visitor submits the contact form.

Parameters 6

$message_idintThe id of the message record created.

$full_namestringThe sender’s name.

$emailstringThe sender’s address.

$phonestringTheir phone; it **can be empty**.

$messagestringThe message body, stripped of markup.

$ipstringTheir network address. All of these **come from a visitor** and none are verified: check them yourself before sending them to an outside service.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.contact_submitted', 10,
    function ($message_id, $full_name, $email, $phone, $message, $ip) {
        // All of these come FROM A VISITOR and are unverified.
        Acme::pushToCrm($message_id, $email, $message);
    });
```

### Following a contact message being answered

actionclient.contact_message_replied

`ClientContact` after the reply

Runs after an administrator answers a contact message.

Parameters 2

$messagearrayThe original message answered.

$admin_messagestringThe reply text sent.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.contact_message_replied', 10, function ($message, $admin_message) {
    Acme::closeCrmCase((int) ($message['id'] ?? 0));
});
```

### Following a message being reported as spam

actionclient.contact_message_spam_reported

`ClientContact` two separate flags

Runs after a contact message is reported as spam. The two flags beside it say **how far the action went**.

Parameters 3

$messagearrayThe message moved to spam, with its address and phone.

$block_emailsintWhether the address and phone went onto the block list.

$report_spamintWhether the network address was blocked. The two flags are independent: one can be set and the other not. A listener treating them as one reports the wrong thing.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.contact_message_spam_reported', 10,
    function ($message, $block_emails, $report_spam) {
        // The two flags are independent.
        if ($report_spam) Acme::shareBadAddress($message['ip'] ?? '');
    });
```

### Stopping a message becoming a ticket

gateclient.contact_message_to_ticket

`ClientContact` before the conversion

Runs before a contact message is turned into a support ticket.

Parameters 3

$messagearrayThe message to be converted.

$departmentintThe target department.

$staffintThe staff member to assign; a **zero** means none.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the visitor as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:client.contact_message_to_ticket', 10,
    function ($message, $department, $staff) {
        if (Acme::blockedSender($message['email'] ?? '')) return 'No ticket can be opened for this sender.';

        return null;
    });
```

### Changing the contact page data

filterclient.contact_page_data

`ClientContact` added keys reach the template

Runs before the contact page is shown. The offices and support hours are already resolved for the active language.

Parameters 2

$page_dataarrayby linkThe page data. Every key you add **becomes a template variable**: you can use it directly in your theme.

$langstringby linkThe active language.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:client.contact_page_data', 10, function (&$page_data, &$lang) {
    // A key you add becomes a template variable.
    $page_data['acme_map'] = Acme::mapEmbed($lang);
});
```

### Stopping a newsletter sign-up

gateclient.newsletter_subscribe

`ClientNewsletter` before the record

Runs before an address joins the newsletter list. The place to keep disposable addresses out.

Parameters 2

$emailstringThe candidate address, lower-cased with its format checked.

$langstringThe site language at the moment of signing up.

Return 1

string|null**A non-empty text blocks the operation** and is shown to the visitor as the error. An empty return lets it carry on.

Listener PHP

```php
Hook::add('gate:client.newsletter_subscribe', 10, function ($email, $lang) {
    if (Acme::disposableDomain($email)) return 'This address is not accepted.';

    return null;
});
```

### Following a newsletter sign-up

actionclient.newsletter_subscribed

`ClientNewsletter` the language is kept too

Runs after an address joins the newsletter list.

Parameters 3

$emailstringThe subscriber address.

$langstringThe language at sign-up. It is kept and **decides the language of bulk mailings**: get it wrong and the subscriber receives an email they cannot read.

$addedintThe id of the record created.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.newsletter_subscribed', 10, function ($email, $lang, $added) {
    // The language decides the content language of bulk mailings.
    Acme::syncMailingList($email, $lang);
});
```

### Following a newsletter unsubscribe

actionclient.newsletter_unsubscribed

`ClientNewsletter` through a link

Runs after an address leaves the list. It happens through the link in the email, with no sign-in needed.

Parameters 1

$emailstringThe address removed from the list.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:client.newsletter_unsubscribed', 10, function ($email) {
    // Remove it from your own list too, or mailings carry on.
    Acme::dropFromMailingList($email);
});
```

## Pitfalls

> **The id in the profile hook is the account id**
> 
> The id handed to the update hook belongs to the **account being changed**, not to whoever made the change. A sub-user with the right permission can update another account’s profile. If you record "who did it", read it from the session separately.

> **The not-found hook fills with bot traffic**
> 
> Scanning bots try addresses that do not exist all day, and on a site those requests easily outnumber human ones. A listener writing a record on every call inflates its own table and buries the real broken links. Put a filter in front of it.

## Related Articles

- [Customer Site Hooks](https://dev.wisecp.com/en/hooks-on-the-customer-site)
- [Account and Contact Hooks](https://dev.wisecp.com/en/account-contact-hooks)
- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work)
