# Domain Lifecycle End Hooks

https://dev.wisecp.com/es/domain-lifecycle-end-hooks

The eight hooks along an unpaid domain's road to the end: the grace period, redemption, the drop, and importing.

## Overview

A domain past its date does not vanish in **one step**. First comes the grace period, where the name still renews at the normal price. Then redemption: the name stops working and getting it back is **far more expensive**. Finally the drop, where the name opens to everyone.

Each of the three has its own hook and all of them run from a **scheduled task**. Nobody presses a button and no one is watching a screen.

## Reference

### Catching the start of the grace period

actiondomain.grace_started

`cronjobs/DomainLifecycle` a scheduled task

Runs the moment the domain entered its grace period; the name still works and renews at the normal price.

Parameters 2

$service_idintThe id of the domain service.

$statearrayThe lifecycle state: `stage`, `days_past_due`, `days_in_stage`. This stage also carries `grace_days`, `grace_fee` and `grace_fee_active`.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.grace_started', 10, function ($service_id, $state) {
    // Reaching the customer at this stage is the cheapest way back.
    Crm::nudge($service_id, 'domain-grace', (int) ($state['grace_days'] ?? 0));
});
```

### Catching the start of redemption

actiondomain.redemption_started

`cronjobs/DomainLifecycle` the name stopped working

Runs where the grace period ended. The name has **stopped working** here and a redemption fee applies.

Parameters 2

$service_idintThe id of the domain service.

$statearrayThe lifecycle state: `stage`, `days_past_due`, `days_in_stage`. This stage carries `redemption_days`, `redemption_fee`, `amount_cid` and `can_restore`.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.redemption_started', 10, function ($service_id, $state) {
    // The fee and its currency arrive in the state; do not work them out.
    if (!empty($state['can_restore']))
        Crm::urgent($service_id, (float) ($state['redemption_fee'] ?? 0),
            (int) ($state['amount_cid'] ?? 0));
});
```

### Stopping the drop request

gatedomain.delete_request

`cronjobs/DomainPurge` in a cron context

Runs before the cancellation request reaches the provider. It sits inside a scheduled task, so stopping it means **nobody sees an error**.

Parameters 3

$servicearrayThe domain service about to be dropped.

$statearrayThe lifecycle state: `stage`, `days_past_due`, `days_in_stage`.

$modulestringThe provider module's name. It arrives empty where no module is assigned.

Return 1

stringA non-empty string **stops** the drop. The task tries again next round, so keep your own mark for a lasting hold.

Listener PHP

```php
Hook::add('gate:domain.delete_request', 10, function ($service, $state, $module) {
    // Hold the drop a round where the customer promised to pay.
    if (Acme::promiseToPay((int) ($service['id'] ?? 0)))
        return 'A payment was promised; the drop was held.';

    return null;
});
```

### Following the drop request

actiondomain.delete_requested

`cronjobs/DomainPurge` it reached the provider

Runs after the cancellation request was sent to the provider.

Parameters 3

$servicearrayThe domain service being dropped.

$statearrayThe lifecycle state: `stage`, `days_past_due`, `days_in_stage`.

$modulestringThe provider module's name; it can be empty.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.delete_requested', 10,
    function ($service, $state, $module) {
        Audit::note('domain-drop', $service['name'] ?? '', $module ?: 'no module');
    });
```

### Catching the finished drop

actiondomain.purged

`cronjobs/DomainPurge` no way back

Runs after the service was cancelled and the domain dropped. This is the **last step**.

Parameters 3

$service_idintThe id of the domain service.

$statearrayThe lifecycle state: `stage`, `days_past_due`, `days_in_stage`. At this point `stage` is the drop stage.

$modulestringThe provider module's name; it can be empty.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.purged', 10, function ($service_id, $state, $module) {
    // The name is no longer ours: clear what hangs off it.
    DnsMonitor::forget($service_id);
});
```

### Following an expiry notice

actiondomain.expired_notice_sent

`cronjobs/DomainExpired` once per milestone

Runs after a notice was sent for a domain past its date.

Parameters 2

$service_idintThe id of the domain service.

$delayed_dayintThe days past the date. It is the notice milestone: the hook runs **more than once** for one name, with different day values.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.expired_notice_sent', 10,
    function ($service_id, $delayed_day) {
        // It runs per milestone: day 1, day 7 and day 15 arrive separately.
        if ($delayed_day >= 15) Ops::escalate('domain-expiry', $service_id);
    });
```

### Following an import

actiondomain.imported

`Imports` a bulk job

Runs after domains were imported from a provider. The hook runs **once for the whole batch**, not per name.

Parameters 2

$importedarrayThe imported service labels: strings shaped like `"example.com (#123)"`. Labels arrive, **not** service records, so the id wants picking out of the text.

$module_namestringThe provider module the import ran against.

Return 1

voidThe return is ignored.

Listener PHP

```php
Hook::add('action:domain.imported', 10, function ($imported, $module_name) {
    // Runs ONCE for the whole batch, not per name.
    Ops::note('domain-import', $module_name . ': ' . count($imported));
});
```

### Changing the grace and redemption fee

filterdomain.grace_redemption_save

`AdminProductsDomain` passed by link

Runs before a grace or redemption fee is saved. This is how you hold one fee policy in one place instead of typing it extension by extension.

Parameters 3

$set_dataarrayby linkThe price record about to be written: `amount`, `cid` (currency), `owner_id`, `status` and the rest.

$typestring`grace` or `redemption`.

$idintThe extension id. **A zero** means the record is not for one extension but the default for all of them.

Return 1

voidThe return is ignored; you write over the record. The hook fires once per currency and per type, so expect several calls for a single save.

Listener PHP

```php
Hook::add('filter:domain.grace_redemption_save', 10, function (&$set_data, $type, $id) {
    // Make the grace period free on every extension.
    if ($type === 'grace') $set_data['amount'] = 0;
});
```

## Pitfalls

> **No screen is watching these hooks**
> 
> Every hook here runs from a scheduled task. Stopping a gate means **nobody sees an error**: not the customer, not the operator. Record your decision on your own side, or no one can tell why the domain never dropped.

> **Stopping is not permanent**
> 
> The task **tries again** next round. Thinking "I stopped it once" at the gate is wrong; your condition is asked afresh every round. For a lasting hold, keep your own mark and read it at the gate.

> **The notice hook repeats for one name**
> 
> Expiry notices go out milestone by milestone and the hook runs **again at each**. A listener reacting without reading the day value produces a run of alerts for a single name.

> **The import hands you labels, not records**
> 
> The import hook hands you **strings** shaped like `"example.com (#123)"`. Expecting service records is wrong; where you need an id, pick it out of the text and read the record yourself.

## Related Articles

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