# Customer-side Ticket Hooks

https://dev.wisecp.com/es/client-ticket-hooks

The eight hooks over what a customer does: opening, replying, closing and rating.

## Overview

Everything a customer can do on the support side lives here: opening a ticket, replying, closing, reopening and rating.

The difficulty of this branch is the question **who did it**. A sub-user may act on somebody else’s account and a guest may belong to no account at all, so the hooks carry the account and the login separately.

## Reference

### Following a customer opening a ticket

actionticket.opened_by_client

`ClientTickets` zero for a guest

Runs when a ticket is opened by a customer or through email.

Parameters 4

$ticketAfterarrayThe freshly created ticket.

$messagestringThe first message with its normalised body.

$clientIdintThe owner. It is **zero** for an **unrecognised sender** arriving by email: do not use it without looking the account up.

$replyIdintThe id of the first reply row.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:ticket.opened_by_client', 10,
    function ($ticketAfter, $message, $clientId, $replyId) {
        // It arrives as ZERO for an unrecognised sender.
        if (!$clientId) { Acme::queueIdentify($ticketAfter); return; }

        Acme::classify((int) ($ticketAfter['id'] ?? 0), $message);
    });
```

### Stopping a customer reply

gateticket.reply_by_client

`ClientTickets` shape varies by source

Runs before a customer replies to a ticket. Replies from the panel, from email and from a guest link all pass here.

Parameters 5

$ticketarrayThe ticket being replied to. ? **Its shape varies by source**: what arrives from the panel and what arrives by email do not carry the same fields. Test a field before reaching for it.

$messagestringThe text to be sent, as plain text.

$ownerIdintThe account that owns the ticket. **Zero for a guest.**

$loginIdintThe login doing it. It **differs** from the owner when a sub-user replies on another account; zero for a guest.

$isGuestboolWhether it came through a guest link.

Return 1

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

Listener PHP

```php
Hook::add('gate:ticket.reply_by_client', 10,
    function ($ticket, $message, $ownerId, $loginId, $isGuest) {
        // For a guest both ids arrive as ZERO.
        if ($isGuest && Acme::guestRepliesClosed()) return 'Guest replies are closed.';

        return null;
    });
```

### Following a customer reply

actionticket.reply_added_by_client

`ClientTickets` zero for a guest

Runs after a customer replies to a ticket.

Parameters 4

$ticketAfterarrayThe fresh ticket after the reply.

$messagestringThe message added.

$clientIdintWho wrote it; it can be zero on an anonymous email reply.

$replyIdintThe id of the reply added.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:ticket.reply_added_by_client', 10,
    function ($ticketAfter, $message, $clientId, $replyId) {
        Acme::restartSlaClock((int) ($ticketAfter['id'] ?? 0));
    });
```

### Following a customer closing a ticket

actionticket.closed_by_client

`ClientTickets` two separate ids

Runs when a customer closes their own ticket.

Parameters 3

$ticketarrayThe snapshot from **before** closing.

$ownerIdintThe account the ticket belongs to.

$loginIdintThe identity that **actually did it**. When a sub-user closes another account’s ticket the two differ: this is the one that belongs in an audit record.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:ticket.closed_by_client', 10, function ($ticket, $ownerId, $loginId) {
    // The LOGIN id is the one that belongs in the audit.
    Acme::audit('ticket-closed', $loginId, $ownerId);
});
```

### Following a customer reopening a ticket

actionticket.reopened_by_client

`ClientTickets` two separate ids

Runs when a customer reopens a closed ticket.

Parameters 3

$ticketarrayThe snapshot from **before** reopening.

$ownerIdintThe account the ticket belongs to.

$loginIdintThe login that did it.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:ticket.reopened_by_client', 10, function ($ticket, $ownerId, $loginId) {
    Acme::restartSlaClock((int) ($ticket['id'] ?? 0));
});
```

### Following a rating

actionticket.rated

`ClientTickets` two scopes

Runs when a customer rates a ticket or a single staff reply. **Two separate scopes** land on the same hook.

Parameters 5

$scopestringWhat was rated: `ticket` or `reply`. Check this first: the two scopes fill their parameters differently.

$ratingintThe score, one to five. A value outside that range **never reaches** the hook.

$ticketIdintThe ticket id; **filled in both scopes**.

$replyIdintThe reply rated. It is **zero** in the ticket scope.

$ownerIdintThe account the ticket belongs to.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:ticket.rated', 10,
    function ($scope, $rating, $ticketId, $replyId, $ownerId) {
        // Check the scope first: the reply id is ZERO in the ticket scope.
        if ($scope === 'reply') { Acme::scoreAgent($replyId, $rating); return; }

        Acme::scoreTicket($ticketId, $rating);
    });
```

### Following a guest viewing

actionticket.guest.viewed

`ClientTickets` carries the access value

Runs when somebody not signed in views a ticket through a link.

Parameters 2

$ticketarrayThe resolved ticket. ? It holds the **sender’s name, email and access value**. Whoever sees that value can open the ticket: keep it out of your records and logs.

$tokenstringThe access value from the address, already verified.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:ticket.guest.viewed', 10, function ($ticket, $token) {
    // Write the access value NOWHERE.
    Acme::countGuestView((int) ($ticket['id'] ?? 0));
});
```

### Changing a customer message

filterticket.client_message

`ClientTickets` passed by link

Runs before the text a customer wrote is saved. New tickets and replies both pass here.

Parameters 2

$messagestringby linkThe message body, as plain text.

$ctxarrayContext: the source, the ticket id, the account and whether it is a guest. On a new ticket the id is **zero**: the ticket does not exist yet.

Return 1

voidThe return is ignored; you write over the data.

Listener PHP

```php
Hook::add('filter:ticket.client_message', 10, function (&$message, $ctx) {
    // On a new ticket the ticket id is ZERO.
    $message = Acme::stripSecrets($message);
});
```

## Pitfalls

> **The account and the login are not the same thing**
> 
> The closing, reopening and reply hooks carry two ids: the **account the ticket belongs to** and the login that **actually did it**. When a sub-user works on somebody else’s account they differ. The login belongs in the audit record; the permission decision follows the account.

> **The guest view hook carries the access value**
> 
> The ticket data holds the sender’s name, email and **access value**. Whoever sees that value can open the ticket: writing it into your records, your logs or an outside service copies the link to a third place.

## Related Articles

- Support Ticket Hooks
- [Customer Account Hooks](https://dev.wisecp.com/en/customer-account-hooks)
- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work)
