Skip to content

Domain expiry reference

The wire-level schema is in the auto-generated API Reference. This page is the human-readable cheat sheet. The API calls these DNS jobs; see the overview for why.

The check and a run of the check are different objects

Section titled “The check and a run of the check are different objects”
ObjectEndpointAnswers
DnsJobGET /dns/job/{dns_job_id}What do we currently believe about this registration?
DnsJobResultGET /dns/job/{dns_job_id}/resultsWhat did one particular run observe?

They agree on every successful run and diverge when a run fails.

A failed run’s result records nothing: expiry_date, domain_status, registrar and nameservers are all null, and failure_reason says why.

The job keeps the values the last successful run recorded, moves current_status to unknown, and sets last_failure_reason and last_failure_at until the next successful run. So a failed run never blanks the registrar and nameserver baseline or re-arms an alert rung.

Current best knowledge about the registration.

FieldTypeDescription
iduuidDomain check ID.
account_iduuidOwning account.
http_job_iduuidThe HTTP check this was paired with.
current_statusenumSee Status. unknown before the first run and after a failed run.
domainstringThe registrable domain derived from the HTTP check’s URL (https://www.example.com/path becomes example.com).
expiry_daterfc3339 | nullThe registration expiry currently in force, from whichever source produced it. null before the first run, and on a TLD that publishes none and has no manual date set.
expiry_sourcestring | nullWhere expiry_date came from: rdap, whois, whoxy, or manual. See where the expiry date comes from. null whenever expiry_date is null.
manual_expiry_daterfc3339 | nullThe renewal date you entered via PUT /dns/job/{dns_job_id}. null when none is set. Nothing updates it on renewal. See Manual renewal dates.
registry_publishes_expiryboolfalse on the TLDs established to publish no expiry date; key “offer a manual renewal date” off this flag. true for every TLD not on that list, including any we have not examined, so true is not a promise that a date will arrive.
domain_statusstring[] | nullEPP status codes in RFC 8056 form: lowercase with spaces, so auto renew period, not autoRenewPeriod.
grace_statesstring[]The subset of domain_status that means the registration is in trouble. See Registry states that alert. Never null; [] when nothing is wrong. Key “needs attention” off this field, not the date.
registrarstring | nullSponsoring registrar.
nameserversstring[] | nullNameserver set, lowercased and sorted.
last_run_atrfc3339 | nullWhen the check last ran, successfully or not.
last_status_change_atrfc3339 | nullWhen current_status last transitioned.
last_failure_reasonstring | nullWhy the last run produced no registration record. See Failure reasons. Non-null means the registry fields above (expiry_date, domain_status, grace_states, registrar, nameservers) are last known rather than current, except registry_publishes_no_expiry, which is a state, not a failure. Cleared by the next run that reads a record.
last_failure_atrfc3339 | nullWhen that failure happened. Cleared with last_failure_reason.
created_atrfc3339When the check was provisioned.

One row per run. Every field describes that run.

FieldTypeDescription
iduuidResult ID.
account_iduuidOwning account.
dns_job_iduuidThe check this result belongs to.
created_atrfc3339When the run happened.
statusenumSee Status. Always unknown when failure_reason is set.
failure_reasonstring | nullSet when this run could not read the registry. See Failure reasons.
expiry_daterfc3339 | nullThe expiry date this run observed. null on a failed run, and when a source answered but published no expiration event. A manual renewal date never appears here.
domain_statusstring[] | nullEPP status codes this run observed, RFC 8056 form. null on a failed run.
grace_statesstring[]Derived from this run’s domain_status. Never null; [] on a failed run.
registrarstring | nullSponsoring registrar this run observed. null on a failed run.
nameserversstring[] | nullNameservers this run observed, lowercased and sorted. null on a failed run.
rdap_originstring | nullWhich RDAP endpoint served this run: iana for IANA’s bootstrap registry, override for the two hardcoded entries (.io and .co). null on a failed run, and when RDAP was not the source that answered.

A failed run never fires a registry state or change alert, because those compare a run’s observation against what the job already knew.

The monitor overview, GET /http/job/{job_id}/overview, embeds a compact domain summary as its dns key. It is absent when the HTTP check has no domain monitoring turned on.

FieldTypeDescription
statusenumSee Status.
domainstringThe registrable domain being monitored.
expiry_daterfc3339 | nullSame value as the job’s, with the same last-known caveat.
days_until_expiryinteger | nullWhole days from now to expiry_date, truncated toward zero, so a registration lapsing in 13 hours reads 0. Negative once it has passed.
grace_statesstring[]Same derived list as on the job. Never null.
last_failure_reasonstring | nullSet when the last run produced no record, so a UI can mark the date as last known rather than current. registry_publishes_no_expiry also arrives here and is a state, not a failure.
last_run_atrfc3339 | nullWhen the domain check last ran.
StatusMeaning
goodThere is an expiry date and it is in the future.
expiredThere is an expiry date and it has passed.
unknownNo usable expiry date: the check has not run yet, the lookup failed, or the TLD publishes no expiry date at all. The failure reason tells them apart: null before the first run, registry_publishes_no_expiry for the third case, and one of the other reasons for a real failure. It is failure_reason on a result and last_failure_reason on the check.

A registry hold does not change the status. A domain in auto renew period, redemption period or pending delete still reports good, because its expiry date is in the future; the registry state arrives as an alert. Anything that keys “is this domain healthy” off current_status alone will call a domain in a deletion window healthy. The overview explains why.

Recorded per run from the registration’s EPP status codes, in RFC 8056 form (lowercase with spaces).

StateTypical windowMeaning
auto renew period~45 daysRenewed automatically by the registry after a missed renewal. The registrar can still cancel for a credit, which deletes the domain.
redemption period~30 daysDeleted, recoverable for a restoration fee.
pending delete~5 daysPast redemption. Drops, and cannot be recovered.
pending restoreshortA restore was requested and is not complete.
client holduntil clearedThe registrar removed the domain from the zone. It does not resolve.
server holduntil clearedThe registry removed the domain from the zone. It does not resolve.

Every other EPP code (ok, active, client transfer prohibited, and the rest) is recorded and never alerts.

Recorded when a run ends with no expiry date. The check reports unknown in every case here, but only some of them mean anything went wrong.

ReasonMeaningRetried
registry_publishes_no_expiryNot a failure. This TLD publishes no expiry date to anybody, under any protocol, by registry policy. Set a manual renewal date instead.No
no_rdap_endpointNo RDAP service is listed for the TLD, in IANA’s bootstrap registry or our overrides. This advances the chain to port-43 WHOIS and is recorded only when no later source had a record either.No
registry_no_recordThe source returned 404 for this name. Like the row above, this advances the chain and is recorded only when nothing later had a record. It is not proof the domain is unregistered: registries also answer 404 for a subdomain of a live domain, and RFC 7480 permits a 404 in place of admitting to rate limiting.No
rate_limitedThe source returned 429. Worth reporting if it persists.Yes, against the same source
transport_errorThe request failed or timed out. The per-request timeout is 10 seconds.Yes, against the same source
malformed_responseThe source answered with something that would not parse.Yes, against the same source

registry_publishes_no_expiry is the one state in this table. A red badge, incident or alert keyed off a non-null last_failure_reason will be wrong about every domain on those ten TLDs, permanently. Key the “something is wrong” treatment off the other five values, and key an “enter a renewal date” prompt off registry_publishes_expiry being false.

A retryable failure is retried on the schedule, and each attempt records its own result row, so a bad afternoon at a registry can leave several unknown rows in the history for one day.

Every alert carries a stable reason slug. This is the field to switch on in a webhook consumer.

SlugFires when
domain_expiringA ladder rung is crossed: 90, 60, 30 or 7 days remaining, or the expiry date is today. Once per rung, per expiry date.
domain_expiredThe expiry date has already passed. Once.
domain_grace_stateThe domain newly entered one of the registry states above. Once per state entered.
domain_registrar_changedThe sponsoring registrar changed from the previously recorded one.
domain_nameservers_changedThe nameserver set changed from the previously recorded one.

A webhook channel on the parent HTTP check’s notification group receives a POST with a JSON body of this shape.

{
"data": {
"dns_job_id": "0f6a2c1e-9d5b-4a3f-8c11-2b7e4d0a6c93",
"monitor_name": "Marketing site",
"domain": "example.com",
"expiry_date": "2026-09-14",
"reason": "domain_grace_state",
"headline": "example.com is in auto renew period",
"detail": "The registry reports this domain as auto renew period. This usually means the registration was not renewed on time and the registry is holding it. The expiry date may still read as far away, because a registry auto-renew moves it forward while the registrar can still hand the domain back. Confirm the renewal with your registrar."
}
}
KeyNotes
dns_job_idThe domain check, not the HTTP check.
monitor_nameThe parent HTTP check’s friendly name.
domainThe registrable domain being monitored.
expiry_dateYYYY-MM-DD, not RFC 3339. The literal string "unknown" when the registry publishes no expiry, which is a real case for registrar and nameserver change alerts.
reasonOne of the alert reason slugs. A stable identifier: branch on this.
headlineOne short sentence, always naming the domain. Customer-facing prose that will be reworded, so do not branch on it.
detailThe explanatory sentence beneath the headline. Written for a human, not parsed.

Three sources, tried in order, and named by expiry_source on the check.

expiry_sourceSourceTried when
rdapThe registry’s own RDAP service, specifically the expiration event in its response. Located through IANA’s bootstrap file, plus two hardcoded entries for .io and .co, which run RDAP but are missing from it.Always, first.
whoisThe registry’s own WHOIS server, on port 43.The TLD has no RDAP service listed, or RDAP had no record for this name.
whoxyA commercial registration database. Catches registries that refuse our connections and formats we would otherwise hand-parse.Neither of the above produced a record.
manualA date you entered. See Manual renewal dates.It is set.

The chain advances only on “no endpoint for this TLD” and “no record for this name”. Every other outcome stops the run and is retried against the same source. See the failure reasons.

rdap and whois are both the registry’s own answer and are equally authoritative. whoxy is a third party’s copy, so a whoxy date is worth a second look before you act on it as fact.

On the RDAP leg, Site Qwality does not follow the related link to the registrar’s RDAP server, which publishes a second, different expiry (often a day earlier) and which some registrars omit entirely. A one-day difference against your registrar’s control panel is expected.

Ten TLDs publish no expiry date to anybody: .am, .at, .be, .de, .eu, .gg, .im, .nl, .no and .nz. They report registry_publishes_expiry: false and last_failure_reason: registry_publishes_no_expiry. For those, you supply the date.

Terminal window
# Set it
curl -X PUT https://api.siteqwality.com/dns/job/$DNS_JOB_ID \
-H "Authorization: Bearer $SITEQWALITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "manual_expiry_date": "2027-03-01T00:00:00Z" }'
# Clear it
curl -X PUT https://api.siteqwality.com/dns/job/$DNS_JOB_ID \
-H "Authorization: Bearer $SITEQWALITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "manual_expiry_date": null }'

Both forms return the updated DnsJob.

With a date set, the check behaves as it would on a registry date: the ladder fires at 90, 60, 30 and 7 days and then on the lapse, each rung once per date. expiry_source reads manual. registry_publishes_expiry stays false, because it describes the TLD rather than your domain.

Domain alerts fan out through the parent HTTP check’s notification groups, and every channel type receives them: email, SMS, Slack, webhook, Telegram, Discord, Microsoft Teams, Google Chat, Mattermost, PagerDuty, Jira Service Management (Opsgenie), Pushover and Pushbullet.

DNS record change alerts reach fewer channels. See which alerts reach which channels.

Maintenance windows do not suppress domain alerts.

PropertyValue
ScheduleOnce a day, with a flexible window of up to 4 hours
ConfigurableNo
Alert thresholdsFixed at 90, 60, 30 and 7 days plus the lapse itself. Not configurable.
EnablePUT /http/job/{job_id} with monitor_dns: true, or the toggle on the HTTP check
DisableThe same with monitor_dns: false. This deletes the domain check and its schedule.
Manual renewal datePUT /dns/job/{dns_job_id} with manual_expiry_date. See Manual renewal dates.

You create a domain check implicitly, by setting monitor_dns: true on an HTTP check. Its own endpoints are read-only apart from the manual renewal date.

MethodPathPurpose
GET/dns/job/{dns_job_id}Fetch the check and its current knowledge. Returns a DnsJob.
PUT/dns/job/{dns_job_id}Set or clear the manual renewal date. Returns the updated DnsJob.
GET/dns/job/{dns_job_id}/recentThe single most recent result. Returns a DnsJobResult.
GET/dns/job/{dns_job_id}/results?limit=50Recent results, newest first. limit defaults to 50 and is clamped to 200.
GET/http/job/{job_id}/overviewThe parent HTTP check’s overview, whose dns key carries a DnsOverview.