Skip to content

Domain Registration Expiration Notifications

This is not about the SSL certificate: that one can be reissued in a minute, but the registration of the domain name itself is not so easy to get back. If you forget to renew a domain, it goes into the redemption period, then to auction — and the site, along with its email, ceases to exist. Certbot won’t save you here: the certificate may be fresh while the domain already belongs to someone else.

The catch is that the registration expiry date comes from whois, and every registrar labels the field differently: Registry Expiry Date, Expiration Date, Expiry date, paid-till (that’s how .ru/.рф do it via TCINET), and a dozen other variants. Date formats also differ — from ISO to YYYY.MM.DD. Below is a script that digests all the common variants, normalizes the date via date -d, and sends a push when only a few days remain.

/usr/local/bin/notifly-domain-check:

#!/usr/bin/env bash
# Usage:
# notifly-domain-check example.com example.ru notifly.рф [...]
set -eu
set -a; source /etc/notifly.env; set +a
WARN_DAYS=30 # priority 5 (normal warning)
CRIT_DAYS=7 # priority 9 (loud, heads-up)
# Extracts the raw registration expiry date from whois output.
# We try registrar labels top to bottom and take the first match.
extract_expiry() {
local w="$1"
# .ru / .рф / .su via TCINET: paid-till field, format YYYY.MM.DD
local ru
ru=$(echo "$w" | grep -iE '^\s*paid-till:' | head -n1 | sed -E 's/^[^:]*:\s*//')
if [ -n "$ru" ]; then
# 2027.04.15 -> 2027-04-15 so date -d parses it reliably
echo "${ru//./-}"
return
fi
# All other TLDs: typical labels for the expiry date
echo "$w" | grep -iE \
'Registry Expiry Date|Registrar Registration Expiration Date|Expiration Date|Expiration Time|Expiry Date|Expiry date|expire:|expires:|Domain Expiration Date|renewal date' \
| head -n1 | sed -E 's/^[^:]*:\s*//'
}
for DOMAIN in "$@"; do
WHOIS=$(whois "$DOMAIN" 2>/dev/null || true)
if [ -z "$WHOIS" ]; then
/usr/local/bin/notifly-send \
"🚨 Domain: whois did not respond — $DOMAIN" \
"Could not get whois data. Check the network or the query rate limit." 8
continue
fi
RAW=$(extract_expiry "$WHOIS")
if [ -z "$RAW" ]; then
/usr/local/bin/notifly-send \
"🚨 Domain: expiry date not found — $DOMAIN" \
"The whois output has no known date field. Check manually: whois $DOMAIN" 8
continue
fi
# Normalize into unix time. If the format is unexpected — report, don't stay silent.
if ! EXP_TS=$(date -d "$RAW" +%s 2>/dev/null); then
/usr/local/bin/notifly-send \
"🚨 Domain: could not parse the date — $DOMAIN" \
"whois returned: $RAW. Unknown format, check manually." 8
continue
fi
NOW_TS=$(date +%s)
DAYS_LEFT=$(( (EXP_TS - NOW_TS) / 86400 ))
EXP_HUMAN=$(date -d "@$EXP_TS" +%Y-%m-%d)
if [ "$DAYS_LEFT" -le "$CRIT_DAYS" ]; then
/usr/local/bin/notifly-send \
"🔥 Domain CRITICAL: $DOMAIN — $DAYS_LEFT days" \
"Registration expires $EXP_HUMAN. Renew now or you'll lose the domain!" 9
elif [ "$DAYS_LEFT" -le "$WARN_DAYS" ]; then
/usr/local/bin/notifly-send \
"⚠️ Domain: $DOMAIN expires in $DAYS_LEFT days" \
"The domain registration is valid until $EXP_HUMAN. Time to renew." 5
fi
done

Make it executable:

Окно терминала
sudo chmod +x /usr/local/bin/notifly-domain-check

You need the whois package (usually apt install whois / dnf install whois). It also understands Cyrillic domains — TCINET correctly returns paid-till for .рф too.

A domain is renewed once a year, so there’s no point checking more than once a day.

Once a day at 9 am:

0 9 * * * root /usr/local/bin/notifly-domain-check \
example.com \
notifly.ru \
notifly.рф

Domains are passed as arguments — as many as you like, across different zones.

/etc/systemd/system/notifly-domain.service:

[Unit]
Description=Notifly domain expiry check
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notifly-domain-check example.com notifly.ru notifly.рф

/etc/systemd/system/notifly-domain.timer:

[Unit]
Description=Run notifly-domain-check daily
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true
[Install]
WantedBy=timers.target

Persistent=true runs a missed check if the server was off at 9 am. Activate it:

Окно терминала
sudo systemctl daemon-reload
sudo systemctl enable --now notifly-domain.timer
sudo systemctl list-timers notifly-domain.timer

Windows has no built-in whois, so we use whois.exe from Sysinternals (https://learn.microsoft.com/sysinternals/downloads/whois) — unpack it, for example, into C:\Tools\whois.exe. On first run it asks you to accept the license: whois.exe -accepteula example.com.

The script uses the common function Send-Notifly from the sysadmin/index template.

C:\scripts\Notifly-Domain-Check.ps1
param(
[string[]]$Domains = @("example.com", "notifly.ru"),
[string] $WhoisExe = "C:\Tools\whois.exe"
)
. C:\scripts\Notifly.ps1
$WarnDays = 30
$CritDays = 7
# Expiry-date field labels used by different registrars
$patterns = @(
'Registry Expiry Date', 'Registrar Registration Expiration Date',
'Expiration Date', 'Expiry Date', 'Expiry date',
'Domain Expiration Date', 'expire', 'paid-till'
)
foreach ($domain in $Domains) {
try {
$whois = & $WhoisExe -nobanner $domain 2>$null
$raw = $null
foreach ($p in $patterns) {
$line = $whois | Select-String -Pattern ([regex]::Escape($p)) | Select-Object -First 1
if ($line) {
$raw = ($line.Line -split ':', 2)[1].Trim()
# .ru/.рф paid-till: YYYY.MM.DD -> YYYY-MM-DD
if ($p -eq 'paid-till') { $raw = $raw -replace '\.', '-' }
break
}
}
if (-not $raw) {
Send-Notifly -Title "🚨 Domain: expiry date not found — $domain" `
-Message "The whois output has no known date field. Check manually." -Priority 8
continue
}
$exp = [datetime]::Parse($raw, [System.Globalization.CultureInfo]::InvariantCulture)
$daysLeft = [int]($exp - (Get-Date)).TotalDays
$expHuman = $exp.ToString('yyyy-MM-dd')
if ($daysLeft -le $CritDays) {
Send-Notifly -Title "🔥 Domain CRITICAL: $domain — $daysLeft days" `
-Message "Registration expires $expHuman. Renew now!" -Priority 9
} elseif ($daysLeft -le $WarnDays) {
Send-Notifly -Title "⚠️ Domain: $domain expires in $daysLeft days" `
-Message "Registration is valid until $expHuman. Time to renew." -Priority 5
}
} catch {
Send-Notifly -Title "🚨 Domain: check failed for $domain" `
-Message "$_" -Priority 8
}
}

If you can’t install a third-party whois.exe, you can look up the expiry date via the registry’s RDAP service (Invoke-RestMethod "https://rdap.org/domain/$domain"), but the completeness of the data depends on the zone — for .ru, RDAP often returns less than whois.

Schedule — once a day:

Окно терминала
$Action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File C:\scripts\Notifly-Domain-Check.ps1"
$Trigger = New-ScheduledTaskTrigger -Daily -At 09:00
$Princ = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "Notifly Domain Check" `
-Action $Action -Trigger $Trigger -Principal $Princ -Description "Notifly: domain registration expiry check"
  • The domain won’t “drop” out of nowhere. A calm warning arrives a month ahead, a loud one a week ahead: forgetting is no longer possible.
  • Works with any zone, including .ru and .рф (the paid-till field from TCINET) — one script for your whole fleet of domains, with zones passed as arguments.
  • A separate safeguard against that exact mistake, where SSL renews automatically but everyone forgot to pay for the domain itself because “the site works, doesn’t it?”.
  • Include the registrar name and status (clientTransferProhibited, etc.) in the message via the extras field — so you immediately see where to go to renew.
  • Store an “already warned” state, like in the disk recipe, so you don’t send the same notification every day.
  • Also check the delegation term at the NS-server registrar — sometimes it’s that which “expires”, not the domain itself.