Skip to content

Set or clear the renewal date for a domain monitor.

PUT
/dns/job/{dns_job_id}
curl --request PUT \
--url https://api.siteqwality.com/dns/job/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "manual_expiry_date": "2026-04-15T12:00:00Z" }'

For the TLDs whose registries publish no expiry date under any protocol (registry_publishes_expiry: false on the monitor), this is the only date there can be, and the normal 90/60/30/7/0 ladder runs against it.

A registry date, when there is one, still wins. The date set here is kept and returned beside it rather than overriding it, so a customer can see the two disagree. See DnsJob::manual_expiry_date for the full argument.

dns_job_id
required
string format: uuid

DNS monitor identifier

Media type application/json

Body of PUT /dns/job/{dns_job_id}.

manual_expiry_date is REQUIRED, and null is how it is cleared. It is deliberately not #[serde(default)]: with exactly one field, an omitted key would silently clear the customer’s date, and {} deserializing into “delete what I typed” is not a request anybody meant to make. An empty body is a 422 from the shared Json extractor instead.

object
manual_expiry_date
required

RFC 3339. Must be in the future and inside [MAX_MANUAL_EXPIRY_YEARS] years. Send null to clear it.

required is asserted explicitly because utoipa reads an Option<T> as an optional key while serde, with no #[serde(default)], demands it. The published spec has to say what the endpoint actually does.

string | null format: date-time
Example generated
{
"manual_expiry_date": "2026-04-15T12:00:00Z"
}

The updated DNS monitor

Media type application/json
object
id
required
string format: uuid
account_id
required
string format: uuid
http_job_id
required
string format: uuid
current_status
required
string
Allowed values: good expired unknown
domain
required
string
expiry_date
string | null format: date-time
last_run_at
string | null format: date-time
last_status_change_at
string | null format: date-time
created_at
required
string format: date-time
domain_status

EPP status codes from the last check, RFC 8056 form (auto renew period, not autoRenewPeriod).

array | null
grace_states
required

Registry states that mean the registration is in trouble regardless of what expiry_date says.

Derived from domain_status rather than stored, so a client never has to carry its own copy of which EPP codes matter. It is the field a UI should key “needs attention” off: a domain in auto renew period reports an expiry date up to two years out while sitting in a 45-day window in which the registrar can hand it back.

Array<string>
registrar

Sponsoring registrar as of the last check.

string | null
nameservers

Nameservers as of the last check, lowercased and sorted.

array | null
manual_expiry_date

The renewal date the customer entered themselves.

For the TLDs in [crate::domain_expiry::NO_EXPIRY_TLDS], whose registries publish no date under any protocol, this is the only date there can be, and the normal 90/60/30/7/0 ladder runs against it.

When a registry date also exists, the registry wins and this stays visible beside it rather than being discarded. A stale manual date is exactly the silent wrongness domain monitoring exists to remove, so it never overrides the authority; but dropping it would leave the customer believing they had entered a date that was doing something. Seeing the two disagree is the information. expiry_source says which of them expiry_date currently is.

string | null format: date-time
expiry_source

Where the current expiry_date came from: rdap, whois, whoxy or manual. Null when there is no date at all.

string | null
registry_publishes_expiry
required

Whether this TLD’s registry publishes an expiry date.

DERIVED from the domain, never stored, so it can never go stale against the curated list. false for the eleven TLDs measured as publishing nothing, and that is the flag a UI keys “offer the manual entry field” off. It is true for every TLD not positively established otherwise, including every TLD nobody has looked at, which is what makes the default safe: being wrong in this direction costs three failed lookups, and being wrong the other way silently removes a customer’s check.

boolean
last_failure_reason

Why the last check could not read the registry, if it could not.

One of no_rdap_endpoint, registry_no_record, no_whois_server, whois_no_record, whois_access_denied, rate_limited, transport_error, malformed_response, no_source_has_a_record, or registry_publishes_no_expiry.

registry_publishes_no_expiry is a STATE, not a failure, and a UI must not present it as a broken check. It means the chain was never run because this registry publishes nothing, and the customer’s next move is to enter a date, not to open a support ticket. registry_publishes_expiry above is the same fact in the form a client can branch on.

A failed check DELIBERATELY leaves expiry_date, domain_status, registrar and nameservers untouched: it learned nothing, and clearing them would erase a good date over a transient blip and re-arm every alert rung. So a non-null value here means the sibling fields are LAST KNOWN rather than current, and a UI should say so. Cleared on the next successful check.

string | null
last_failure_at
string | null format: date-time
Example
{
"current_status": "good"
}

The renewal date is in the past or absurdly far in the future

Missing or invalid authentication

DNS monitor not found

Internal server error