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.
Check script
Section titled “Check script”/usr/local/bin/notifly-domain-check:
#!/usr/bin/env bash# Usage:# notifly-domain-check example.com example.ru notifly.рф [...]set -euset -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 fidoneMake it executable:
sudo chmod +x /usr/local/bin/notifly-domain-checkYou 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.
Using cron
Section titled “Using cron”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.
Using systemd timer (recommended)
Section titled “Using systemd timer (recommended)”/etc/systemd/system/notifly-domain.service:
[Unit]Description=Notifly domain expiry check
[Service]Type=oneshotExecStart=/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:00Persistent=true
[Install]WantedBy=timers.targetPersistent=true runs a missed check if the server was off at 9 am.
Activate it:
sudo systemctl daemon-reloadsudo systemctl enable --now notifly-domain.timersudo systemctl list-timers notifly-domain.timerWindows: PowerShell + Task Scheduler
Section titled “Windows: PowerShell + Task Scheduler”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.
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 HighestRegister-ScheduledTask -TaskName "Notifly Domain Check" ` -Action $Action -Trigger $Trigger -Principal $Princ -Description "Notifly: domain registration expiry check"Benefits
Section titled “Benefits”- 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
.ruand.рф(thepaid-tillfield 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?”.
What to improve next
Section titled “What to improve next”- Include the registrar name and status (
clientTransferProhibited, etc.) in the message via theextrasfield — 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.