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”| Object | Endpoint | Answers |
|---|---|---|
DnsJob | GET / | What do we currently believe about this registration? |
DnsJobResult | GET / | What 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.
Object: DnsJob
Section titled “Object: DnsJob”Current best knowledge about the registration.
| Field | Type | Description |
|---|---|---|
id | uuid | Domain check ID. |
account_id | uuid | Owning account. |
http_job_id | uuid | The HTTP check this was paired with. |
current_status | enum | See Status. unknown before the first run and after a failed run. |
domain | string | The registrable domain derived from the HTTP check’s URL (https://www.example.com/ becomes example.com). |
expiry_date | rfc3339 | null | The 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_source | string | null | Where expiry_date came from: rdap, whois, whoxy, or manual. See where the expiry date comes from. null whenever expiry_date is null. |
manual_expiry_date | rfc3339 | null | The renewal date you entered via PUT /. null when none is set. Nothing updates it on renewal. See Manual renewal dates. |
registry_publishes_expiry | bool | false 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_status | string[] | null | EPP status codes in RFC 8056 form: lowercase with spaces, so auto renew period, not autoRenewPeriod. |
grace_states | string[] | 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. |
registrar | string | null | Sponsoring registrar. |
nameservers | string[] | null | Nameserver set, lowercased and sorted. |
last_run_at | rfc3339 | null | When the check last ran, successfully or not. |
last_status_change_at | rfc3339 | null | When current_status last transitioned. |
last_failure_reason | string | null | Why 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_at | rfc3339 | null | When that failure happened. Cleared with last_failure_reason. |
created_at | rfc3339 | When the check was provisioned. |
Object: DnsJobResult
Section titled “Object: DnsJobResult”One row per run. Every field describes that run.
| Field | Type | Description |
|---|---|---|
id | uuid | Result ID. |
account_id | uuid | Owning account. |
dns_job_id | uuid | The check this result belongs to. |
created_at | rfc3339 | When the run happened. |
status | enum | See Status. Always unknown when failure_reason is set. |
failure_reason | string | null | Set when this run could not read the registry. See Failure reasons. |
expiry_date | rfc3339 | null | The 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_status | string[] | null | EPP status codes this run observed, RFC 8056 form. null on a failed run. |
grace_states | string[] | Derived from this run’s domain_status. Never null; [] on a failed run. |
registrar | string | null | Sponsoring registrar this run observed. null on a failed run. |
nameservers | string[] | null | Nameservers this run observed, lowercased and sorted. null on a failed run. |
rdap_origin | string | null | Which 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.
Object: DnsOverview
Section titled “Object: DnsOverview”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.
| Field | Type | Description |
|---|---|---|
status | enum | See Status. |
domain | string | The registrable domain being monitored. |
expiry_date | rfc3339 | null | Same value as the job’s, with the same last-known caveat. |
days_until_expiry | integer | null | Whole days from now to expiry_date, truncated toward zero, so a registration lapsing in 13 hours reads 0. Negative once it has passed. |
grace_states | string[] | Same derived list as on the job. Never null. |
last_failure_reason | string | null | Set 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_at | rfc3339 | null | When the domain check last ran. |
Status
Section titled “Status”| Status | Meaning |
|---|---|
good | There is an expiry date and it is in the future. |
expired | There is an expiry date and it has passed. |
unknown | No 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.
Registry states that alert
Section titled “Registry states that alert”Recorded per run from the registration’s EPP status codes, in RFC 8056 form (lowercase with spaces).
| State | Typical window | Meaning |
|---|---|---|
auto renew period | ~45 days | Renewed automatically by the registry after a missed renewal. The registrar can still cancel for a credit, which deletes the domain. |
redemption period | ~30 days | Deleted, recoverable for a restoration fee. |
pending delete | ~5 days | Past redemption. Drops, and cannot be recovered. |
pending restore | short | A restore was requested and is not complete. |
client hold | until cleared | The registrar removed the domain from the zone. It does not resolve. |
server hold | until cleared | The 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.
Failure reasons
Section titled “Failure reasons”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.
| Reason | Meaning | Retried |
|---|---|---|
registry_publishes_no_expiry | Not 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_endpoint | No 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_record | The 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_limited | The source returned 429. Worth reporting if it persists. | Yes, against the same source |
transport_error | The request failed or timed out. The per-request timeout is 10 seconds. | Yes, against the same source |
malformed_response | The 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.
Alert reasons
Section titled “Alert reasons”Every alert carries a stable reason slug. This is the field to switch on in a webhook consumer.
| Slug | Fires when |
|---|---|
domain_expiring | A ladder rung is crossed: 90, 60, 30 or 7 days remaining, or the expiry date is today. Once per rung, per expiry date. |
domain_expired | The expiry date has already passed. Once. |
domain_grace_state | The domain newly entered one of the registry states above. Once per state entered. |
domain_registrar_changed | The sponsoring registrar changed from the previously recorded one. |
domain_nameservers_changed | The nameserver set changed from the previously recorded one. |
Webhook payload
Section titled “Webhook payload”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." }}| Key | Notes |
|---|---|
dns_job_id | The domain check, not the HTTP check. |
monitor_name | The parent HTTP check’s friendly name. |
domain | The registrable domain being monitored. |
expiry_date | YYYY-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. |
reason | One of the alert reason slugs. A stable identifier: branch on this. |
headline | One short sentence, always naming the domain. Customer-facing prose that will be reworded, so do not branch on it. |
detail | The explanatory sentence beneath the headline. Written for a human, not parsed. |
Where the expiry date comes from
Section titled “Where the expiry date comes from”Three sources, tried in order, and named by expiry_source on the check.
expiry_source | Source | Tried when |
|---|---|---|
rdap | The 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. |
whois | The registry’s own WHOIS server, on port 43. | The TLD has no RDAP service listed, or RDAP had no record for this name. |
whoxy | A commercial registration database. Catches registries that refuse our connections and formats we would otherwise hand-parse. | Neither of the above produced a record. |
manual | A 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.
Manual renewal dates
Section titled “Manual renewal dates”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.
# Set itcurl -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 itcurl -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.
Notification channels
Section titled “Notification channels”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.
Cadence and lifecycle
Section titled “Cadence and lifecycle”| Property | Value |
|---|---|
| Schedule | Once a day, with a flexible window of up to 4 hours |
| Configurable | No |
| Alert thresholds | Fixed at 90, 60, 30 and 7 days plus the lapse itself. Not configurable. |
| Enable | PUT / with monitor_dns: true, or the toggle on the HTTP check |
| Disable | The same with monitor_dns: false. This deletes the domain check and its schedule. |
| Manual renewal date | PUT / with manual_expiry_date. See Manual renewal dates. |
Endpoints
Section titled “Endpoints”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.
| Method | Path | Purpose |
|---|---|---|
GET | /dns/ | Fetch the check and its current knowledge. Returns a DnsJob. |
PUT | /dns/ | Set or clear the manual renewal date. Returns the updated DnsJob. |
GET | /dns/ | The single most recent result. Returns a DnsJobResult. |
GET | /dns/ | Recent results, newest first. limit defaults to 50 and is clamped to 200. |
GET | /http/ | The parent HTTP check’s overview, whose dns key carries a DnsOverview. |
See also
Section titled “See also”- Domain expiry overview: the three alert signals, and the auto-renew trap.
- Quickstart: turn it on and read the first result.
- HTTP checks reference: the parent check.