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”

A domain check is served as two shapes, and they answer two different questions. Getting them confused is the single easiest way to misread this API.

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 the moment a run fails.

A failed run learned nothing, so its result row records nothing: expiry_date, domain_status, registrar and nameservers are all null, and failure_reason says why. History has to be able to say “we could not read the registry that day” rather than repeating the previous answer as though it had been confirmed.

The job row does the opposite. It keeps the values the last successful run recorded, moves current_status to unknown, and sets last_failure_reason and last_failure_at. That is deliberate: clearing the date over a rate limit or a timeout would throw away a good date, blank the registrar and nameserver baseline (so the next successful run would read as a first observation and swallow a real registrar change), and re-arm every alert rung, producing a duplicate 30-day warning on recovery.

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. Starts at unknown before the first run, and returns to unknown when a run fails.
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, whichever leg of the lookup chain produced it. null when nothing has produced one: 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. null whenever expiry_date is null. Read this before treating a date as authoritative: rdap and whois are the registry’s own answer, whoxy is a commercial aggregator, and manual is a date somebody at your company typed.
manual_expiry_daterfc3339 | nullThe renewal date you entered yourself, via PUT /dns/job/{dns_job_id}. null when none is set. Customer-maintained: nothing updates it on renewal. See Manual renewal dates.
registry_publishes_expiryboolfalse on the TLDs established to publish no expiry date to anybody. This is the flag to key “offer a manual renewal date” off. true for every TLD not on that list, including any we have not examined, so a true here 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. Derived server-side, never null, and [] when there is nothing wrong. This is the field to key “needs attention” off, 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, if it produced none. See Failure reasons. Non-null means the registry fields above are last known rather than current, with one exception: registry_publishes_no_expiry is a state, not a failure, and must not be rendered as a broken check. Cleared by the next run that reads a record.
last_failure_atrfc3339 | nullWhen that failure happened.
created_atrfc3339When the check was provisioned.

One row per run, kept as history. Every field describes that run, so a failed run carries a failure_reason and nulls, never the previous answer.

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 null when a leg answered but published no expiration event. A manual renewal date is not an observation, so it 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 when it came from IANA’s bootstrap registry, override when it came from the two hardcoded entries (.io and .co). null on a failed run, and null when the RDAP leg was not the one that answered.

The registry fields are kept per run so history can answer “what did the registry say on the day this broke”. The registry state and change alerts are decided by comparing a run’s observation against what the job already knew, which is why a failed run never fires one.

The monitor overview, GET /http/job/{job_id}/overview, embeds a compact domain summary as its dns key. This is what the dashboard’s Domain tile reads. 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. Three different situations produce it: the check has not run yet, the lookup failed, or the TLD publishes no expiry date at all. Read the failure reason to tell 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. The field is failure_reason on a result and last_failure_reason on the check itself.

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. The chain was walked and there is nothing to find. Set a manual renewal date instead.No, and there is nothing to retry
no_rdap_endpointNo RDAP service is listed for the TLD, in IANA’s bootstrap registry or our overrides. On its own this does not end a run: it advances the chain to port-43 WHOIS. It is recorded only when no later leg had a record either.No
registry_no_recordThe source returned 404 for this name. Like the row above, this advances the chain rather than ending the run, 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. Unexpected at this volume and worth reporting if it persists.Yes, against the same leg
transport_errorThe request failed or timed out. The per-request timeout is 10 seconds.Yes, against the same leg
malformed_responseThe source answered with something that would not parse.Yes, against the same leg

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. The four original keys keep their exact names and meaning; reason, headline and detail were added in August 2026 and are additive.

{
"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.
headlineOne short sentence, always naming the domain.
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.

RDAP is contractually mandatory for gTLDs and entirely optional for ccTLDs, which is the whole reason for legs 2 and 3: IANA’s bootstrap file lists an RDAP service for roughly 1,200 of the 1,438 TLDs in the root, and reading RDAP alone meant a .me domain got no expiry check at all.

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 leg. 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.

Eleven 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 exactly 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. Email, SMS, Slack, webhook, Discord, Microsoft Teams, Google Chat, Mattermost, PagerDuty, OpsGenie, Pushover and Pushbullet all receive them.

Telegram channels do not. Telegram is configurable and has a working transport, but no monitor type in SiteQwality produces a Telegram notification today, so a Telegram channel on the group is skipped for domain alerts.

Maintenance windows do not suppress domain alerts, deliberately. A renewal deadline does not pause for scheduled work.

PropertyValue
ScheduleOnce a day, with a flexible window of up to 4 hours
ConfigurableNo. There is no interval field on a domain check.
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. Body is { "manual_expiry_date": "2027-03-01T00:00:00Z" }, or { "manual_expiry_date": null } to clear it. 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.