Skip to main content

namecheap_client/
domains.rs

1//! The `namecheap.domains` command namespace: availability checks, registration,
2//! and DNS host records.
3
4use serde::Deserialize;
5
6use crate::client::Client;
7use crate::error::Result;
8use crate::response::{de_bool, de_opt_bool, de_opt_from_str};
9
10/// Accessor for `namecheap.domains.*` commands, returned by
11/// [`Client::domains`](crate::Client::domains).
12#[derive(Debug, Clone, Copy)]
13pub struct Domains<'a> {
14    client: &'a Client,
15}
16
17impl<'a> Domains<'a> {
18    pub(crate) fn new(client: &'a Client) -> Self {
19        Self { client }
20    }
21
22    /// Checks the availability of one or more domains
23    /// (`namecheap.domains.check`).
24    ///
25    /// Accepts anything iterable of string-like values, so `&["a.com", "b.io"]`,
26    /// a `Vec<String>`, and similar all work. The returned vector preserves the
27    /// order Namecheap reports, which generally matches the input order.
28    ///
29    /// # Example
30    ///
31    /// ```no_run
32    /// # async fn run(client: namecheap_client::Client) -> Result<(), namecheap_client::Error> {
33    /// for result in client.domains().check(["example.com", "example.io"]).await? {
34    ///     println!("{}: available = {}", result.domain, result.available);
35    /// }
36    /// # Ok(())
37    /// # }
38    /// ```
39    ///
40    /// # Errors
41    ///
42    /// Returns an [`Error`](crate::Error) on transport failure, an API error
43    /// response, or a decode failure.
44    pub async fn check<I, S>(&self, domains: I) -> Result<Vec<DomainCheckResult>>
45    where
46        I: IntoIterator<Item = S>,
47        S: AsRef<str>,
48    {
49        let list = domains
50            .into_iter()
51            .map(|domain| domain.as_ref().to_owned())
52            .collect::<Vec<_>>()
53            .join(",");
54        let params = vec![("DomainList".to_owned(), list)];
55        let payload: CheckPayload = self.client.send("namecheap.domains.check", params).await?;
56        Ok(payload.results)
57    }
58
59    /// Registers a domain (`namecheap.domains.create`).
60    ///
61    /// <div class="warning">
62    ///
63    /// This places a real, billable order against your Namecheap account when
64    /// the client targets [`Environment::Production`](crate::Environment::Production).
65    /// Develop against [`Environment::Sandbox`](crate::Environment::Sandbox)
66    /// first.
67    ///
68    /// </div>
69    ///
70    /// # Errors
71    ///
72    /// Returns an [`Error`](crate::Error) on transport failure, an API error
73    /// response (for example insufficient funds or an unsupported TLD), or a
74    /// decode failure.
75    pub async fn create(&self, request: &DomainCreateRequest) -> Result<DomainCreateResult> {
76        let payload: CreatePayload = self
77            .client
78            .send("namecheap.domains.create", request.to_params())
79            .await?;
80        Ok(payload.result)
81    }
82
83    /// Lists the domains in the account (`namecheap.domains.getList`).
84    ///
85    /// Returns the first page (up to 100 domains) together with paging totals.
86    /// Compare [`DomainListResult::total_items`] with the number of returned
87    /// domains to tell whether more pages exist. Each [`DomainListItem`] reports
88    /// its [`auto_renew`](DomainListItem::auto_renew) flag and expiry, which is
89    /// the read side of managing renewals.
90    ///
91    /// # Errors
92    ///
93    /// Returns an [`Error`](crate::Error) on transport failure, an API error
94    /// response, or a decode failure.
95    pub async fn list(&self) -> Result<DomainListResult> {
96        let params = vec![("PageSize".to_owned(), "100".to_owned())];
97        let payload: GetListPayload = self
98            .client
99            .send("namecheap.domains.getList", params)
100            .await?;
101        Ok(DomainListResult {
102            domains: payload.result.domains,
103            total_items: payload.paging.total_items,
104            current_page: payload.paging.current_page,
105            page_size: payload.paging.page_size,
106        })
107    }
108
109    /// Enables or disables auto-renewal for a domain
110    /// (`namecheap.domains.setAutoRenew`).
111    ///
112    /// Pass the full domain name (for example `"example.com"`). Passing `false`
113    /// is how you stop a domain from renewing automatically: it will then lapse
114    /// at expiry unless you renew it. Read the current state from
115    /// [`list`](Domains::list) via [`DomainListItem::auto_renew`].
116    ///
117    /// # Errors
118    ///
119    /// Returns an [`Error`](crate::Error) on transport failure, an API error
120    /// response, or a decode failure.
121    pub async fn set_auto_renew(&self, domain: &str, enabled: bool) -> Result<SetAutoRenewResult> {
122        let params = vec![
123            ("DomainName".to_owned(), domain.to_owned()),
124            (
125                "AutoRenew".to_owned(),
126                if enabled { "true" } else { "false" }.to_owned(),
127            ),
128        ];
129        let payload: SetAutoRenewPayload = self
130            .client
131            .send("namecheap.domains.setAutoRenew", params)
132            .await?;
133        payload
134            .into_result()
135            .ok_or(crate::error::Error::EmptyResponse)
136    }
137
138    /// Accessor for `namecheap.domains.dns.*` commands.
139    #[must_use]
140    pub fn dns(&self) -> Dns<'a> {
141        Dns {
142            client: self.client,
143        }
144    }
145}
146
147/// Accessor for `namecheap.domains.dns.*` commands, returned by
148/// [`Domains::dns`].
149#[derive(Debug, Clone, Copy)]
150pub struct Dns<'a> {
151    client: &'a Client,
152}
153
154impl Dns<'_> {
155    /// Reads the current DNS host records for a domain
156    /// (`namecheap.domains.dns.getHosts`).
157    ///
158    /// Pass the domain split into its second-level and top-level parts, the same
159    /// way [`SetHostsRequest`] takes them (so `example.com` is `"example"`,
160    /// `"com"`). This is the read half of a read-modify-write update: fetch the
161    /// records, adjust them via [`GetHostsResult::to_host_records`], and send the
162    /// complete set back through [`set_hosts`](Dns::set_hosts).
163    ///
164    /// [`GetHostsResult::is_using_our_dns`] reports whether the domain points at
165    /// Namecheap's DNS; `set_hosts` only takes effect when it does.
166    ///
167    /// # Errors
168    ///
169    /// Returns an [`Error`](crate::Error) on transport failure, an API error
170    /// response, or a decode failure.
171    pub async fn get_hosts(&self, sld: &str, tld: &str) -> Result<GetHostsResult> {
172        let params = vec![
173            ("SLD".to_owned(), sld.to_owned()),
174            ("TLD".to_owned(), tld.to_owned()),
175        ];
176        let payload: GetHostsPayload = self
177            .client
178            .send("namecheap.domains.dns.getHosts", params)
179            .await?;
180        Ok(payload.result)
181    }
182
183    /// Replaces the full set of DNS host records for a domain
184    /// (`namecheap.domains.dns.setHosts`).
185    ///
186    /// <div class="warning">
187    ///
188    /// This command is destructive: it overwrites *all* existing host records
189    /// with exactly the records you supply. Any record you omit is deleted. To
190    /// change one record, send the complete desired set.
191    ///
192    /// </div>
193    ///
194    /// This is the command to use when wiring up email for a domain: supply the
195    /// `MX` records and set [`SetHostsRequest::email_type`] to
196    /// [`EmailType::Mx`], then add the `TXT` records for SPF, DKIM, and DMARC.
197    ///
198    /// # Errors
199    ///
200    /// Returns an [`Error`](crate::Error) on transport failure, an API error
201    /// response, or a decode failure.
202    pub async fn set_hosts(&self, request: &SetHostsRequest) -> Result<SetHostsResult> {
203        let payload: SetHostsPayload = self
204            .client
205            .send("namecheap.domains.dns.setHosts", request.to_params())
206            .await?;
207        Ok(payload.result)
208    }
209}
210
211// --- domains.check ---------------------------------------------------------
212
213#[derive(Debug, Deserialize)]
214struct CheckPayload {
215    #[serde(rename = "DomainCheckResult", default)]
216    results: Vec<DomainCheckResult>,
217}
218
219/// The availability result for a single domain from
220/// [`Domains::check`].
221#[derive(Debug, Clone, Deserialize)]
222#[non_exhaustive]
223pub struct DomainCheckResult {
224    /// The domain that was checked.
225    #[serde(rename = "@Domain")]
226    pub domain: String,
227    /// Whether the domain is available to register.
228    #[serde(rename = "@Available", deserialize_with = "de_bool")]
229    pub available: bool,
230    /// Whether Namecheap classifies the domain as a premium name.
231    #[serde(rename = "@IsPremiumName", default, deserialize_with = "de_bool")]
232    pub is_premium_name: bool,
233    /// The premium registration price, when the domain is a premium name.
234    #[serde(
235        rename = "@PremiumRegistrationPrice",
236        default,
237        deserialize_with = "de_opt_from_str"
238    )]
239    pub premium_registration_price: Option<f64>,
240    /// A per-domain error code, when Namecheap could not check this entry
241    /// (`"0"` indicates no error).
242    #[serde(rename = "@ErrorNo", default)]
243    pub error_no: Option<String>,
244    /// A per-domain description or error message, when present.
245    #[serde(rename = "@Description", default)]
246    pub description: Option<String>,
247}
248
249// --- domains.create --------------------------------------------------------
250
251/// A single postal/email contact used when registering a domain.
252///
253/// Namecheap requires four contact roles (registrant, technical,
254/// administrative, and billing). [`DomainCreateRequest::new`] reuses one
255/// `Contact` for all four; build per-role contacts manually if they differ.
256///
257/// `phone` must use Namecheap's `+NNN.NNNNNNNNNN` format (country code, a dot,
258/// then the number), and `country` must be a two-letter ISO country code such
259/// as `"US"`.
260#[derive(Debug, Clone)]
261pub struct Contact {
262    /// Given name.
263    pub first_name: String,
264    /// Family name.
265    pub last_name: String,
266    /// First address line.
267    pub address1: String,
268    /// Optional second address line.
269    pub address2: Option<String>,
270    /// City.
271    pub city: String,
272    /// State or province.
273    pub state_province: String,
274    /// Postal or ZIP code.
275    pub postal_code: String,
276    /// Two-letter ISO country code (for example `"US"`).
277    pub country: String,
278    /// Phone number in `+NNN.NNNNNNNNNN` format.
279    pub phone: String,
280    /// Email address.
281    pub email_address: String,
282    /// Optional organization name.
283    pub organization: Option<String>,
284}
285
286impl Contact {
287    /// Creates a contact from the fields Namecheap requires, leaving the
288    /// optional `address2` and `organization` unset.
289    #[allow(clippy::too_many_arguments)]
290    pub fn new(
291        first_name: impl Into<String>,
292        last_name: impl Into<String>,
293        address1: impl Into<String>,
294        city: impl Into<String>,
295        state_province: impl Into<String>,
296        postal_code: impl Into<String>,
297        country: impl Into<String>,
298        phone: impl Into<String>,
299        email_address: impl Into<String>,
300    ) -> Self {
301        Self {
302            first_name: first_name.into(),
303            last_name: last_name.into(),
304            address1: address1.into(),
305            address2: None,
306            city: city.into(),
307            state_province: state_province.into(),
308            postal_code: postal_code.into(),
309            country: country.into(),
310            phone: phone.into(),
311            email_address: email_address.into(),
312            organization: None,
313        }
314    }
315
316    /// Sets the optional second address line.
317    #[must_use]
318    pub fn with_address2(mut self, address2: impl Into<String>) -> Self {
319        self.address2 = Some(address2.into());
320        self
321    }
322
323    /// Sets the optional organization name.
324    #[must_use]
325    pub fn with_organization(mut self, organization: impl Into<String>) -> Self {
326        self.organization = Some(organization.into());
327        self
328    }
329}
330
331/// A request to register a domain via [`Domains::create`].
332#[derive(Debug, Clone)]
333pub struct DomainCreateRequest {
334    /// The domain to register (for example `"example.com"`).
335    pub domain: String,
336    /// The number of years to register for.
337    pub years: u32,
338    /// The registrant contact.
339    pub registrant: Contact,
340    /// The technical contact.
341    pub tech: Contact,
342    /// The administrative contact.
343    pub admin: Contact,
344    /// The billing contact.
345    pub aux_billing: Contact,
346    /// Custom nameservers. When empty, Namecheap's default DNS is used.
347    pub nameservers: Vec<String>,
348    /// Whether to request the free WhoisGuard privacy add-on with the order.
349    pub add_free_whoisguard: bool,
350    /// Whether to enable WhoisGuard privacy once the order completes.
351    pub enable_whoisguard: bool,
352}
353
354impl DomainCreateRequest {
355    /// Builds a registration request that reuses `contact` for all four
356    /// required contact roles, with WhoisGuard privacy enabled.
357    pub fn new(domain: impl Into<String>, years: u32, contact: Contact) -> Self {
358        Self {
359            domain: domain.into(),
360            years,
361            registrant: contact.clone(),
362            tech: contact.clone(),
363            admin: contact.clone(),
364            aux_billing: contact,
365            nameservers: Vec::new(),
366            add_free_whoisguard: true,
367            enable_whoisguard: true,
368        }
369    }
370
371    /// Sets custom nameservers for the domain.
372    #[must_use]
373    pub fn with_nameservers<I, S>(mut self, nameservers: I) -> Self
374    where
375        I: IntoIterator<Item = S>,
376        S: Into<String>,
377    {
378        self.nameservers = nameservers.into_iter().map(Into::into).collect();
379        self
380    }
381
382    /// Enables or disables the WhoisGuard privacy add-on (enabled by default).
383    #[must_use]
384    pub fn with_whois_privacy(mut self, enabled: bool) -> Self {
385        self.add_free_whoisguard = enabled;
386        self.enable_whoisguard = enabled;
387        self
388    }
389
390    fn to_params(&self) -> Vec<(String, String)> {
391        let mut params = Vec::new();
392        params.push(("DomainName".to_owned(), self.domain.clone()));
393        params.push(("Years".to_owned(), self.years.to_string()));
394        push_contact(&mut params, "Registrant", &self.registrant);
395        push_contact(&mut params, "Tech", &self.tech);
396        push_contact(&mut params, "Admin", &self.admin);
397        push_contact(&mut params, "AuxBilling", &self.aux_billing);
398        if !self.nameservers.is_empty() {
399            params.push(("Nameservers".to_owned(), self.nameservers.join(",")));
400        }
401        params.push((
402            "AddFreeWhoisguard".to_owned(),
403            yes_no(self.add_free_whoisguard),
404        ));
405        params.push(("WGEnabled".to_owned(), yes_no(self.enable_whoisguard)));
406        params
407    }
408}
409
410fn push_contact(params: &mut Vec<(String, String)>, role: &str, contact: &Contact) {
411    let key = |suffix: &str| format!("{role}{suffix}");
412    params.push((key("FirstName"), contact.first_name.clone()));
413    params.push((key("LastName"), contact.last_name.clone()));
414    params.push((key("Address1"), contact.address1.clone()));
415    if let Some(address2) = &contact.address2 {
416        params.push((key("Address2"), address2.clone()));
417    }
418    params.push((key("City"), contact.city.clone()));
419    params.push((key("StateProvince"), contact.state_province.clone()));
420    params.push((key("PostalCode"), contact.postal_code.clone()));
421    params.push((key("Country"), contact.country.clone()));
422    params.push((key("Phone"), contact.phone.clone()));
423    params.push((key("EmailAddress"), contact.email_address.clone()));
424    if let Some(organization) = &contact.organization {
425        params.push((key("OrganizationName"), organization.clone()));
426    }
427}
428
429fn yes_no(value: bool) -> String {
430    if value { "yes" } else { "no" }.to_owned()
431}
432
433#[derive(Debug, Deserialize)]
434struct CreatePayload {
435    #[serde(rename = "DomainCreateResult")]
436    result: DomainCreateResult,
437}
438
439/// The outcome of a [`Domains::create`] call.
440#[derive(Debug, Clone, Deserialize)]
441#[non_exhaustive]
442pub struct DomainCreateResult {
443    /// The domain that was registered.
444    #[serde(rename = "@Domain")]
445    pub domain: String,
446    /// Whether registration succeeded.
447    #[serde(rename = "@Registered", deserialize_with = "de_bool")]
448    pub registered: bool,
449    /// The amount charged for the order, when reported.
450    #[serde(
451        rename = "@ChargedAmount",
452        default,
453        deserialize_with = "de_opt_from_str"
454    )]
455    pub charged_amount: Option<f64>,
456    /// The Namecheap internal domain identifier, when reported.
457    #[serde(rename = "@DomainID", default, deserialize_with = "de_opt_from_str")]
458    pub domain_id: Option<u64>,
459    /// The order identifier, when reported.
460    #[serde(rename = "@OrderID", default, deserialize_with = "de_opt_from_str")]
461    pub order_id: Option<u64>,
462    /// The transaction identifier, when reported.
463    #[serde(
464        rename = "@TransactionID",
465        default,
466        deserialize_with = "de_opt_from_str"
467    )]
468    pub transaction_id: Option<u64>,
469    /// Whether WhoisGuard privacy was enabled for the domain.
470    #[serde(
471        rename = "@WhoisguardEnable",
472        default,
473        deserialize_with = "de_opt_bool"
474    )]
475    pub whoisguard_enabled: Option<bool>,
476    /// Whether the registration is processed out of band rather than in real
477    /// time.
478    #[serde(
479        rename = "@NonRealTimeDomain",
480        default,
481        deserialize_with = "de_opt_bool"
482    )]
483    pub non_real_time_domain: Option<bool>,
484}
485
486// --- domains.dns.setHosts --------------------------------------------------
487
488/// A DNS record type accepted by [`Dns::set_hosts`].
489#[derive(Debug, Clone, Copy, PartialEq, Eq)]
490#[non_exhaustive]
491pub enum RecordType {
492    /// IPv4 address record.
493    A,
494    /// IPv6 address record.
495    Aaaa,
496    /// Canonical name (alias) record.
497    Cname,
498    /// Mail exchanger record.
499    Mx,
500    /// Namecheap "mail forwarding" record type.
501    Mxe,
502    /// Text record (used for SPF, DKIM, DMARC, verification tokens).
503    Txt,
504    /// Nameserver record.
505    Ns,
506    /// Namecheap URL redirect (unmasked) record.
507    Url,
508    /// Namecheap permanent (301) URL redirect record.
509    Url301,
510    /// Namecheap masked URL redirect (frame) record.
511    Frame,
512    /// Certification Authority Authorization record.
513    Caa,
514}
515
516impl RecordType {
517    /// The value Namecheap expects for this record type in the `RecordType`
518    /// parameter.
519    #[must_use]
520    pub fn as_str(self) -> &'static str {
521        match self {
522            RecordType::A => "A",
523            RecordType::Aaaa => "AAAA",
524            RecordType::Cname => "CNAME",
525            RecordType::Mx => "MX",
526            RecordType::Mxe => "MXE",
527            RecordType::Txt => "TXT",
528            RecordType::Ns => "NS",
529            RecordType::Url => "URL",
530            RecordType::Url301 => "URL301",
531            RecordType::Frame => "FRAME",
532            RecordType::Caa => "CAA",
533        }
534    }
535
536    /// Parses a record type from the string Namecheap reports (for example in a
537    /// [`Dns::get_hosts`] response). Case-insensitive; returns `None` for an
538    /// unrecognized type.
539    #[must_use]
540    pub fn from_api_str(value: &str) -> Option<Self> {
541        Some(match value.trim().to_ascii_uppercase().as_str() {
542            "A" => RecordType::A,
543            "AAAA" => RecordType::Aaaa,
544            "CNAME" => RecordType::Cname,
545            "MX" => RecordType::Mx,
546            "MXE" => RecordType::Mxe,
547            "TXT" => RecordType::Txt,
548            "NS" => RecordType::Ns,
549            "URL" => RecordType::Url,
550            "URL301" => RecordType::Url301,
551            "FRAME" => RecordType::Frame,
552            "CAA" => RecordType::Caa,
553            _ => return None,
554        })
555    }
556}
557
558/// The mail setting to apply when calling [`Dns::set_hosts`].
559///
560/// Namecheap stores a single email routing mode per domain alongside the host
561/// records. Set [`EmailType::Mx`] when supplying your own `MX` records (for
562/// Google Workspace, Fastmail, and so on).
563#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564#[non_exhaustive]
565pub enum EmailType {
566    /// Custom user-defined `MX` records.
567    Mx,
568    /// Namecheap email forwarding via `MXE`.
569    Mxe,
570    /// Namecheap email forwarding.
571    Forward,
572    /// Namecheap Private Email (Open-Xchange).
573    PrivateEmail,
574    /// Google Workspace / Gmail preset.
575    Gmail,
576}
577
578impl EmailType {
579    /// The value Namecheap expects for this setting in the `EmailType`
580    /// parameter.
581    #[must_use]
582    pub fn as_str(self) -> &'static str {
583        match self {
584            EmailType::Mx => "MX",
585            EmailType::Mxe => "MXE",
586            EmailType::Forward => "FWD",
587            EmailType::PrivateEmail => "OX",
588            EmailType::Gmail => "GMAIL",
589        }
590    }
591
592    /// Parses an email type from the string Namecheap reports (for example in a
593    /// [`Dns::get_hosts`] response). Case-insensitive; returns `None` for an
594    /// unrecognized value such as `"NONE"`.
595    #[must_use]
596    pub fn from_api_str(value: &str) -> Option<Self> {
597        Some(match value.trim().to_ascii_uppercase().as_str() {
598            "MX" => EmailType::Mx,
599            "MXE" => EmailType::Mxe,
600            "FWD" => EmailType::Forward,
601            "OX" => EmailType::PrivateEmail,
602            "GMAIL" => EmailType::Gmail,
603            _ => return None,
604        })
605    }
606}
607
608/// A single DNS host record to set via [`Dns::set_hosts`].
609#[derive(Debug, Clone)]
610pub struct HostRecord {
611    /// The host (subdomain), for example `"@"` for the apex or `"www"`.
612    pub host_name: String,
613    /// The record type.
614    pub record_type: RecordType,
615    /// The record value (an IP, hostname, or text payload).
616    pub address: String,
617    /// The `MX` preference. Required for `MX` records; ignored otherwise.
618    pub mx_pref: Option<u32>,
619    /// The record TTL in seconds (Namecheap allows 60 to 60000).
620    pub ttl: Option<u32>,
621}
622
623impl HostRecord {
624    /// Creates a host record with no explicit `MX` preference or TTL.
625    pub fn new(
626        host_name: impl Into<String>,
627        record_type: RecordType,
628        address: impl Into<String>,
629    ) -> Self {
630        Self {
631            host_name: host_name.into(),
632            record_type,
633            address: address.into(),
634            mx_pref: None,
635            ttl: None,
636        }
637    }
638
639    /// Creates an `A` (IPv4 address) record.
640    pub fn a(host_name: impl Into<String>, ipv4: impl Into<String>) -> Self {
641        Self::new(host_name, RecordType::A, ipv4)
642    }
643
644    /// Creates an `AAAA` (IPv6 address) record.
645    pub fn aaaa(host_name: impl Into<String>, ipv6: impl Into<String>) -> Self {
646        Self::new(host_name, RecordType::Aaaa, ipv6)
647    }
648
649    /// Creates a `CNAME` (alias) record.
650    pub fn cname(host_name: impl Into<String>, target: impl Into<String>) -> Self {
651        Self::new(host_name, RecordType::Cname, target)
652    }
653
654    /// Creates a `TXT` record (SPF, DKIM, DMARC, verification tokens, ...).
655    pub fn txt(host_name: impl Into<String>, value: impl Into<String>) -> Self {
656        Self::new(host_name, RecordType::Txt, value)
657    }
658
659    /// Creates an `MX` record with the given mail server and preference.
660    pub fn mx(
661        host_name: impl Into<String>,
662        mail_server: impl Into<String>,
663        preference: u32,
664    ) -> Self {
665        Self {
666            host_name: host_name.into(),
667            record_type: RecordType::Mx,
668            address: mail_server.into(),
669            mx_pref: Some(preference),
670            ttl: None,
671        }
672    }
673
674    /// Sets the TTL (in seconds) for this record.
675    #[must_use]
676    pub fn with_ttl(mut self, ttl: u32) -> Self {
677        self.ttl = Some(ttl);
678        self
679    }
680
681    /// Sets the `MX` preference for this record.
682    #[must_use]
683    pub fn with_mx_pref(mut self, preference: u32) -> Self {
684        self.mx_pref = Some(preference);
685        self
686    }
687}
688
689/// A request to replace a domain's DNS host records via [`Dns::set_hosts`].
690///
691/// Namecheap addresses the domain by its second-level and top-level parts
692/// separately, so a domain like `example.com` is `sld = "example"`,
693/// `tld = "com"`. Use [`SetHostsRequest::from_domain`] to split a registrable
694/// domain automatically.
695#[derive(Debug, Clone)]
696pub struct SetHostsRequest {
697    /// The second-level domain (the `example` of `example.com`).
698    pub sld: String,
699    /// The top-level domain (the `com` of `example.com`, or `co.uk`).
700    pub tld: String,
701    /// The complete set of host records the domain should have afterward.
702    pub records: Vec<HostRecord>,
703    /// The optional email routing mode to apply.
704    pub email_type: Option<EmailType>,
705}
706
707impl SetHostsRequest {
708    /// Builds a request from explicit second-level and top-level domain parts.
709    pub fn new(sld: impl Into<String>, tld: impl Into<String>, records: Vec<HostRecord>) -> Self {
710        Self {
711            sld: sld.into(),
712            tld: tld.into(),
713            records,
714            email_type: None,
715        }
716    }
717
718    /// Builds a request from a registrable domain, splitting it at the first
719    /// dot (so `example.com` and `example.co.uk` both work). Returns `None` if
720    /// the input has no dot or an empty part.
721    pub fn from_domain(domain: &str, records: Vec<HostRecord>) -> Option<Self> {
722        let (sld, tld) = domain.split_once('.')?;
723        if sld.is_empty() || tld.is_empty() {
724            return None;
725        }
726        Some(Self::new(sld, tld, records))
727    }
728
729    /// Sets the email routing mode (see [`EmailType`]).
730    #[must_use]
731    pub fn with_email_type(mut self, email_type: EmailType) -> Self {
732        self.email_type = Some(email_type);
733        self
734    }
735
736    fn to_params(&self) -> Vec<(String, String)> {
737        let mut params = Vec::new();
738        params.push(("SLD".to_owned(), self.sld.clone()));
739        params.push(("TLD".to_owned(), self.tld.clone()));
740        for (index, record) in self.records.iter().enumerate() {
741            let n = index + 1;
742            params.push((format!("HostName{n}"), record.host_name.clone()));
743            params.push((
744                format!("RecordType{n}"),
745                record.record_type.as_str().to_owned(),
746            ));
747            params.push((format!("Address{n}"), record.address.clone()));
748            let mx_pref = record.mx_pref.or(match record.record_type {
749                RecordType::Mx => Some(10),
750                _ => None,
751            });
752            if let Some(pref) = mx_pref {
753                params.push((format!("MXPref{n}"), pref.to_string()));
754            }
755            if let Some(ttl) = record.ttl {
756                params.push((format!("TTL{n}"), ttl.to_string()));
757            }
758        }
759        if let Some(email_type) = self.email_type {
760            params.push(("EmailType".to_owned(), email_type.as_str().to_owned()));
761        }
762        params
763    }
764}
765
766#[derive(Debug, Deserialize)]
767struct SetHostsPayload {
768    #[serde(rename = "DomainDNSSetHostsResult")]
769    result: SetHostsResult,
770}
771
772/// The outcome of a [`Dns::set_hosts`] call.
773#[derive(Debug, Clone, Deserialize)]
774#[non_exhaustive]
775pub struct SetHostsResult {
776    /// The domain whose records were updated.
777    #[serde(rename = "@Domain")]
778    pub domain: String,
779    /// Whether the update succeeded.
780    #[serde(rename = "@IsSuccess", deserialize_with = "de_bool")]
781    pub is_success: bool,
782}
783
784// --- domains.dns.getHosts --------------------------------------------------
785
786#[derive(Debug, Deserialize)]
787struct GetHostsPayload {
788    #[serde(rename = "DomainDNSGetHostsResult")]
789    result: GetHostsResult,
790}
791
792/// The current DNS configuration for a domain, returned by [`Dns::get_hosts`].
793#[derive(Debug, Clone, Deserialize)]
794#[non_exhaustive]
795pub struct GetHostsResult {
796    /// The domain these records belong to.
797    #[serde(rename = "@Domain")]
798    pub domain: String,
799    /// Whether the domain currently points at Namecheap's DNS. When `false`,
800    /// [`Dns::set_hosts`] will not take effect until the nameservers are pointed
801    /// back at Namecheap.
802    #[serde(rename = "@IsUsingOurDNS", default, deserialize_with = "de_bool")]
803    pub is_using_our_dns: bool,
804    /// The raw email routing mode reported by the API (for example `"MX"`,
805    /// `"MXE"`, `"FWD"`, `"OX"`, `"GMAIL"`, or `"NONE"`). Parse it with
806    /// [`EmailType::from_api_str`] if you need the typed form.
807    #[serde(rename = "@EmailType", default)]
808    pub email_type: Option<String>,
809    /// The current host records.
810    #[serde(rename = "host", default)]
811    pub records: Vec<HostInfo>,
812}
813
814impl GetHostsResult {
815    /// Converts the current records into [`HostRecord`]s ready to pass back to
816    /// [`Dns::set_hosts`], which is the basis of a read-modify-write update.
817    ///
818    /// Every record type Namecheap's DNS supports is recognized, so in practice
819    /// nothing is dropped; a record whose type is somehow unrecognized is
820    /// skipped (see [`HostInfo::to_host_record`]).
821    #[must_use]
822    pub fn to_host_records(&self) -> Vec<HostRecord> {
823        self.records
824            .iter()
825            .filter_map(HostInfo::to_host_record)
826            .collect()
827    }
828}
829
830/// A single DNS host record as returned by [`Dns::get_hosts`].
831///
832/// This carries the read-only fields the API reports (such as
833/// [`host_id`](Self::host_id) and [`is_active`](Self::is_active)). To write a
834/// record back, convert it with [`to_host_record`](Self::to_host_record).
835#[derive(Debug, Clone, Deserialize)]
836#[non_exhaustive]
837pub struct HostInfo {
838    /// Namecheap's internal identifier for this record.
839    #[serde(rename = "@HostId", default, deserialize_with = "de_opt_from_str")]
840    pub host_id: Option<u64>,
841    /// The host (subdomain), for example `"@"` or `"www"`.
842    #[serde(rename = "@Name")]
843    pub name: String,
844    /// The record type as reported by the API (for example `"A"` or `"MX"`).
845    #[serde(rename = "@Type")]
846    pub record_type: String,
847    /// The record value.
848    #[serde(rename = "@Address")]
849    pub address: String,
850    /// The `MX` preference, when applicable.
851    #[serde(rename = "@MXPref", default, deserialize_with = "de_opt_from_str")]
852    pub mx_pref: Option<u32>,
853    /// The record TTL in seconds.
854    #[serde(rename = "@TTL", default, deserialize_with = "de_opt_from_str")]
855    pub ttl: Option<u32>,
856    /// Whether the record is active.
857    #[serde(rename = "@IsActive", default, deserialize_with = "de_opt_bool")]
858    pub is_active: Option<bool>,
859}
860
861impl HostInfo {
862    /// Converts this record into a [`HostRecord`] for [`Dns::set_hosts`].
863    ///
864    /// Returns `None` only if [`record_type`](Self::record_type) is not a type
865    /// this crate recognizes (see [`RecordType::from_api_str`]); all standard
866    /// Namecheap record types are recognized.
867    #[must_use]
868    pub fn to_host_record(&self) -> Option<HostRecord> {
869        let record_type = RecordType::from_api_str(&self.record_type)?;
870        Some(HostRecord {
871            host_name: self.name.clone(),
872            record_type,
873            address: self.address.clone(),
874            mx_pref: self.mx_pref,
875            ttl: self.ttl,
876        })
877    }
878}
879
880// --- domains.getList -------------------------------------------------------
881
882#[derive(Debug, Deserialize)]
883struct GetListPayload {
884    #[serde(rename = "DomainGetListResult", default)]
885    result: GetListInner,
886    #[serde(rename = "Paging", default)]
887    paging: Paging,
888}
889
890#[derive(Debug, Default, Deserialize)]
891struct GetListInner {
892    #[serde(rename = "Domain", default)]
893    domains: Vec<DomainListItem>,
894}
895
896#[derive(Debug, Default, Deserialize)]
897struct Paging {
898    #[serde(rename = "TotalItems", default)]
899    total_items: u32,
900    #[serde(rename = "CurrentPage", default)]
901    current_page: u32,
902    #[serde(rename = "PageSize", default)]
903    page_size: u32,
904}
905
906/// The result of [`Domains::list`]: the account's domains plus paging totals.
907#[derive(Debug, Clone)]
908#[non_exhaustive]
909pub struct DomainListResult {
910    /// The domains on the returned page.
911    pub domains: Vec<DomainListItem>,
912    /// The total number of domains in the account, across all pages.
913    pub total_items: u32,
914    /// The page number that was returned.
915    pub current_page: u32,
916    /// The page size used for the request.
917    pub page_size: u32,
918}
919
920/// A single domain entry from [`Domains::list`].
921#[derive(Debug, Clone, Deserialize)]
922#[non_exhaustive]
923pub struct DomainListItem {
924    /// Namecheap's internal identifier for the domain.
925    #[serde(rename = "@ID", default, deserialize_with = "de_opt_from_str")]
926    pub id: Option<u64>,
927    /// The domain name.
928    #[serde(rename = "@Name")]
929    pub name: String,
930    /// The creation date as reported by the API (for example `"06/20/2026"`).
931    #[serde(rename = "@Created", default)]
932    pub created: Option<String>,
933    /// The expiry date as reported by the API.
934    #[serde(rename = "@Expires", default)]
935    pub expires: Option<String>,
936    /// Whether the domain has expired.
937    #[serde(rename = "@IsExpired", default, deserialize_with = "de_opt_bool")]
938    pub is_expired: Option<bool>,
939    /// Whether the domain is registrar-locked.
940    #[serde(rename = "@IsLocked", default, deserialize_with = "de_opt_bool")]
941    pub is_locked: Option<bool>,
942    /// Whether the domain is set to renew automatically.
943    #[serde(rename = "@AutoRenew", default, deserialize_with = "de_bool")]
944    pub auto_renew: bool,
945    /// The WhoisGuard status (for example `"ENABLED"` or `"NOTPRESENT"`).
946    #[serde(rename = "@WhoisGuard", default)]
947    pub whois_guard: Option<String>,
948    /// Whether the domain currently uses Namecheap's DNS.
949    #[serde(rename = "@IsOurDNS", default, deserialize_with = "de_opt_bool")]
950    pub is_our_dns: Option<bool>,
951}
952
953// --- domains.setAutoRenew --------------------------------------------------
954
955#[derive(Debug, Deserialize)]
956struct SetAutoRenewPayload {
957    // The live API returns `SetAutoRenewResult`; some references use the
958    // `DomainSetAutoRenewResult` spelling, so accept either.
959    #[serde(rename = "SetAutoRenewResult", default)]
960    result: Option<SetAutoRenewResult>,
961    #[serde(rename = "DomainSetAutoRenewResult", default)]
962    alt_result: Option<SetAutoRenewResult>,
963}
964
965impl SetAutoRenewPayload {
966    fn into_result(self) -> Option<SetAutoRenewResult> {
967        self.result.or(self.alt_result)
968    }
969}
970
971/// The result of [`Domains::set_auto_renew`].
972#[derive(Debug, Clone, Deserialize)]
973#[non_exhaustive]
974pub struct SetAutoRenewResult {
975    /// The domain whose auto-renew setting was changed.
976    #[serde(rename = "@Domain")]
977    pub domain: String,
978    /// Whether the change succeeded.
979    #[serde(rename = "@IsSuccess", deserialize_with = "de_bool")]
980    pub is_success: bool,
981}
982
983#[cfg(test)]
984mod tests {
985    use super::*;
986    use reqwest::StatusCode;
987
988    fn value<'a>(params: &'a [(String, String)], key: &str) -> Option<&'a str> {
989        params
990            .iter()
991            .find(|(name, _)| name == key)
992            .map(|(_, value)| value.as_str())
993    }
994
995    fn sample_contact() -> Contact {
996        Contact::new(
997            "John",
998            "Doe",
999            "123 Main St",
1000            "Los Angeles",
1001            "CA",
1002            "90001",
1003            "US",
1004            "+1.5555551234",
1005            "john@example.com",
1006        )
1007    }
1008
1009    #[test]
1010    fn parses_check_response() {
1011        let body = r#"<?xml version="1.0" encoding="utf-8"?>
1012        <ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1013          <Errors />
1014          <CommandResponse Type="namecheap.domains.check">
1015            <DomainCheckResult Domain="taken.com" Available="false" ErrorNo="0" Description="" IsPremiumName="false" PremiumRegistrationPrice="0.0000" />
1016            <DomainCheckResult Domain="free-domain-9876.com" Available="true" ErrorNo="0" Description="" IsPremiumName="false" />
1017            <DomainCheckResult Domain="fancy.io" Available="true" IsPremiumName="true" PremiumRegistrationPrice="2999.9900" />
1018          </CommandResponse>
1019        </ApiResponse>"#;
1020
1021        let payload: CheckPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1022        assert_eq!(payload.results.len(), 3);
1023
1024        assert_eq!(payload.results[0].domain, "taken.com");
1025        assert!(!payload.results[0].available);
1026
1027        assert!(payload.results[1].available);
1028        assert!(!payload.results[1].is_premium_name);
1029
1030        assert!(payload.results[2].is_premium_name);
1031        assert_eq!(payload.results[2].premium_registration_price, Some(2999.99));
1032    }
1033
1034    #[test]
1035    fn parses_create_response() {
1036        let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1037          <Errors />
1038          <CommandResponse Type="namecheap.domains.create">
1039            <DomainCreateResult Domain="example.com" Registered="true" ChargedAmount="10.8700" DomainID="123456" OrderID="654321" TransactionID="111222" WhoisguardEnable="true" NonRealTimeDomain="false" />
1040          </CommandResponse>
1041        </ApiResponse>"#;
1042
1043        let payload: CreatePayload = crate::response::parse(StatusCode::OK, body).unwrap();
1044        let result = payload.result;
1045        assert_eq!(result.domain, "example.com");
1046        assert!(result.registered);
1047        assert_eq!(result.charged_amount, Some(10.87));
1048        assert_eq!(result.domain_id, Some(123456));
1049        assert_eq!(result.order_id, Some(654321));
1050        assert_eq!(result.whoisguard_enabled, Some(true));
1051        assert_eq!(result.non_real_time_domain, Some(false));
1052    }
1053
1054    #[test]
1055    fn parses_set_hosts_response() {
1056        let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1057          <Errors />
1058          <CommandResponse Type="namecheap.domains.dns.setHosts">
1059            <DomainDNSSetHostsResult Domain="example.com" IsSuccess="true" />
1060          </CommandResponse>
1061        </ApiResponse>"#;
1062
1063        let payload: SetHostsPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1064        assert_eq!(payload.result.domain, "example.com");
1065        assert!(payload.result.is_success);
1066    }
1067
1068    #[test]
1069    fn create_request_builds_all_contact_roles() {
1070        let request = DomainCreateRequest::new("example.com", 2, sample_contact())
1071            .with_nameservers(["ns1.example.com", "ns2.example.com"]);
1072        let params = request.to_params();
1073
1074        assert_eq!(value(&params, "DomainName"), Some("example.com"));
1075        assert_eq!(value(&params, "Years"), Some("2"));
1076        assert_eq!(
1077            value(&params, "Nameservers"),
1078            Some("ns1.example.com,ns2.example.com")
1079        );
1080        assert_eq!(value(&params, "AddFreeWhoisguard"), Some("yes"));
1081        assert_eq!(value(&params, "WGEnabled"), Some("yes"));
1082
1083        for role in ["Registrant", "Tech", "Admin", "AuxBilling"] {
1084            assert_eq!(value(&params, &format!("{role}FirstName")), Some("John"));
1085            assert_eq!(value(&params, &format!("{role}Country")), Some("US"));
1086            assert_eq!(
1087                value(&params, &format!("{role}EmailAddress")),
1088                Some("john@example.com")
1089            );
1090        }
1091    }
1092
1093    #[test]
1094    fn create_request_can_disable_privacy() {
1095        let params = DomainCreateRequest::new("example.com", 1, sample_contact())
1096            .with_whois_privacy(false)
1097            .to_params();
1098        assert_eq!(value(&params, "AddFreeWhoisguard"), Some("no"));
1099        assert_eq!(value(&params, "WGEnabled"), Some("no"));
1100    }
1101
1102    #[test]
1103    fn set_hosts_request_indexes_records_and_defaults_mx_pref() {
1104        let request = SetHostsRequest::new(
1105            "example",
1106            "com",
1107            vec![
1108                HostRecord::mx("@", "mx1.example.com", 10),
1109                HostRecord::txt("@", "v=spf1 include:_spf.example.com ~all"),
1110                HostRecord::a("www", "203.0.113.10").with_ttl(300),
1111            ],
1112        )
1113        .with_email_type(EmailType::Mx);
1114        let params = request.to_params();
1115
1116        assert_eq!(value(&params, "SLD"), Some("example"));
1117        assert_eq!(value(&params, "TLD"), Some("com"));
1118
1119        assert_eq!(value(&params, "HostName1"), Some("@"));
1120        assert_eq!(value(&params, "RecordType1"), Some("MX"));
1121        assert_eq!(value(&params, "Address1"), Some("mx1.example.com"));
1122        assert_eq!(value(&params, "MXPref1"), Some("10"));
1123
1124        assert_eq!(value(&params, "RecordType2"), Some("TXT"));
1125        // Non-MX records carry no MX preference.
1126        assert_eq!(value(&params, "MXPref2"), None);
1127
1128        assert_eq!(value(&params, "RecordType3"), Some("A"));
1129        assert_eq!(value(&params, "TTL3"), Some("300"));
1130
1131        assert_eq!(value(&params, "EmailType"), Some("MX"));
1132    }
1133
1134    #[test]
1135    fn from_domain_splits_at_first_dot() {
1136        let records = vec![HostRecord::a("@", "203.0.113.10")];
1137
1138        let simple = SetHostsRequest::from_domain("example.com", records.clone()).unwrap();
1139        assert_eq!(simple.sld, "example");
1140        assert_eq!(simple.tld, "com");
1141
1142        let multi = SetHostsRequest::from_domain("example.co.uk", records.clone()).unwrap();
1143        assert_eq!(multi.sld, "example");
1144        assert_eq!(multi.tld, "co.uk");
1145
1146        assert!(SetHostsRequest::from_domain("nodot", records.clone()).is_none());
1147        assert!(SetHostsRequest::from_domain(".com", records.clone()).is_none());
1148        assert!(SetHostsRequest::from_domain("example.", records).is_none());
1149    }
1150
1151    #[test]
1152    fn parses_get_hosts_response_and_round_trips() {
1153        let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1154          <Errors />
1155          <CommandResponse Type="namecheap.domains.dns.getHosts">
1156            <DomainDNSGetHostsResult Domain="example.com" IsUsingOurDNS="true" EmailType="MX">
1157              <host HostId="12" Name="@" Type="A" Address="203.0.113.10" MXPref="10" TTL="1800" IsActive="true" IsDDNSEnabled="false" />
1158              <host HostId="13" Name="@" Type="MX" Address="mx.example.com" MXPref="10" TTL="1800" IsActive="true" />
1159              <host HostId="14" Name="www" Type="CNAME" Address="example.com." MXPref="10" TTL="60" IsActive="true" />
1160            </DomainDNSGetHostsResult>
1161          </CommandResponse>
1162        </ApiResponse>"#;
1163
1164        let payload: GetHostsPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1165        let result = payload.result;
1166        assert_eq!(result.domain, "example.com");
1167        assert!(result.is_using_our_dns);
1168        assert_eq!(result.email_type.as_deref(), Some("MX"));
1169        assert_eq!(result.records.len(), 3);
1170        assert_eq!(result.records[0].host_id, Some(12));
1171        assert_eq!(result.records[0].name, "@");
1172        assert_eq!(result.records[0].record_type, "A");
1173        assert_eq!(result.records[0].ttl, Some(1800));
1174        assert_eq!(result.records[0].is_active, Some(true));
1175
1176        // Read-modify-write: the records convert straight back to writable form.
1177        let writable = result.to_host_records();
1178        assert_eq!(writable.len(), 3);
1179        assert_eq!(writable[1].record_type, RecordType::Mx);
1180        assert_eq!(writable[1].mx_pref, Some(10));
1181        assert_eq!(writable[2].host_name, "www");
1182        assert_eq!(writable[2].record_type, RecordType::Cname);
1183    }
1184
1185    #[test]
1186    fn record_and_email_types_parse_from_api_strings() {
1187        for record_type in [
1188            RecordType::A,
1189            RecordType::Mx,
1190            RecordType::Txt,
1191            RecordType::Caa,
1192        ] {
1193            assert_eq!(
1194                RecordType::from_api_str(record_type.as_str()),
1195                Some(record_type)
1196            );
1197        }
1198        assert_eq!(RecordType::from_api_str("cname"), Some(RecordType::Cname));
1199        assert_eq!(RecordType::from_api_str("bogus"), None);
1200
1201        assert_eq!(EmailType::from_api_str("MX"), Some(EmailType::Mx));
1202        assert_eq!(EmailType::from_api_str("fwd"), Some(EmailType::Forward));
1203        assert_eq!(EmailType::from_api_str("NONE"), None);
1204    }
1205
1206    #[test]
1207    fn parses_get_list_response() {
1208        let body = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1209          <Errors />
1210          <CommandResponse Type="namecheap.domains.getList">
1211            <DomainGetListResult>
1212              <Domain ID="1120966" Name="alpha.com" User="someone" Created="06/19/2026" Expires="06/20/2027" IsExpired="false" IsLocked="false" AutoRenew="false" WhoisGuard="NOTPRESENT" IsPremium="false" IsOurDNS="true" />
1213              <Domain ID="1120967" Name="beta.com" User="someone" Created="06/19/2026" Expires="06/20/2027" IsExpired="false" IsLocked="false" AutoRenew="true" WhoisGuard="ENABLED" IsPremium="false" IsOurDNS="true" />
1214            </DomainGetListResult>
1215            <Paging>
1216              <TotalItems>2</TotalItems>
1217              <CurrentPage>1</CurrentPage>
1218              <PageSize>100</PageSize>
1219            </Paging>
1220          </CommandResponse>
1221        </ApiResponse>"#;
1222
1223        let payload: GetListPayload = crate::response::parse(StatusCode::OK, body).unwrap();
1224        assert_eq!(payload.paging.total_items, 2);
1225        assert_eq!(payload.paging.page_size, 100);
1226        assert_eq!(payload.result.domains.len(), 2);
1227
1228        let alpha = &payload.result.domains[0];
1229        assert_eq!(alpha.name, "alpha.com");
1230        assert_eq!(alpha.id, Some(1120966));
1231        assert!(!alpha.auto_renew);
1232        assert_eq!(alpha.expires.as_deref(), Some("06/20/2027"));
1233        assert_eq!(alpha.is_our_dns, Some(true));
1234
1235        assert!(payload.result.domains[1].auto_renew);
1236        assert_eq!(
1237            payload.result.domains[1].whois_guard.as_deref(),
1238            Some("ENABLED")
1239        );
1240    }
1241
1242    #[test]
1243    fn parses_set_auto_renew_response_both_spellings() {
1244        // The form the live API returned.
1245        let observed = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1246          <Errors />
1247          <CommandResponse Type="namecheap.domains.setAutoRenew">
1248            <SetAutoRenewResult Domain="example.com" IsSuccess="true" />
1249          </CommandResponse>
1250        </ApiResponse>"#;
1251        let payload: SetAutoRenewPayload =
1252            crate::response::parse(StatusCode::OK, observed).unwrap();
1253        let result = payload.into_result().unwrap();
1254        assert_eq!(result.domain, "example.com");
1255        assert!(result.is_success);
1256
1257        // The alternative element spelling some references use.
1258        let alternative = r#"<ApiResponse Status="OK" xmlns="http://api.namecheap.com/xml.response">
1259          <Errors />
1260          <CommandResponse Type="namecheap.domains.setAutoRenew">
1261            <DomainSetAutoRenewResult Domain="example.com" IsSuccess="true" />
1262          </CommandResponse>
1263        </ApiResponse>"#;
1264        let payload: SetAutoRenewPayload =
1265            crate::response::parse(StatusCode::OK, alternative).unwrap();
1266        assert!(payload.into_result().unwrap().is_success);
1267    }
1268}