Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- WordPress.com `/domains/{name}/is-available` endpoint for checking domain availability, pricing, and transfer status

## [0.2.0] - 2026-05-05

### Added
Expand Down
397 changes: 389 additions & 8 deletions wp_api/src/wp_com/domains.rs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions wp_api/src/wp_com/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ pub(crate) mod tests {
validate_endpoint(WpComNamespace::RestV1_1, endpoint_url, path);
}

pub fn validate_wp_com_rest_v1_3_endpoint(endpoint_url: ApiEndpointUrl, path: &str) {
validate_endpoint(WpComNamespace::RestV1_3, endpoint_url, path);
}

pub fn validate_wp_com_v2_endpoint(endpoint_url: ApiEndpointUrl, path: &str) {
validate_endpoint(WpComNamespace::V2, endpoint_url, path);
}
Expand Down
44 changes: 40 additions & 4 deletions wp_api/src/wp_com/endpoint/domains_endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::{
wp_com::{
WpComNamespace,
domains::{
CountryCode, DomainSuggestion, DomainSuggestionsParams, SupportedCountries,
SupportedState,
CountryCode, DomainAvailability, DomainAvailabilityParams, DomainName,
DomainSuggestion, DomainSuggestionsParams, SupportedCountries, SupportedState,
},
},
};
Expand All @@ -18,11 +18,16 @@ enum DomainsRequest {
SupportedCountries,
#[get(url = "/domains/supported-states/<country_code>", output = Vec<SupportedState>)]
SupportedStates,
#[get(url = "/domains/<domain_name>/is-available", params = &DomainAvailabilityParams, output = DomainAvailability)]
IsAvailable,
}

impl DerivedRequest for DomainsRequest {
fn namespace(&self) -> impl AsNamespace {
WpComNamespace::RestV1_1
match self {
Self::IsAvailable => WpComNamespace::RestV1_3,
_ => WpComNamespace::RestV1_1,
}
}
}

Expand All @@ -32,9 +37,11 @@ mod tests {
use crate::{
request::endpoint::ApiUrlResolver,
wp_com::{
domains::CountryCode,
WpComSiteId,
domains::{CountryCode, DomainAvailabilityParams, DomainName},
endpoint::tests::{
fixture_wp_com_api_url_resolver, validate_wp_com_rest_v1_1_endpoint,
validate_wp_com_rest_v1_3_endpoint,
},
segments::SegmentId,
},
Expand Down Expand Up @@ -106,6 +113,35 @@ mod tests {
validate_wp_com_rest_v1_1_endpoint(endpoint.supported_states(&country_code), expected_path);
}

#[rstest]
#[case::com(DomainName("example.com".to_string()), "/domains/example.com/is-available?")]
#[case::org(DomainName("myblog.org".to_string()), "/domains/myblog.org/is-available?")]
fn is_available(
endpoint: DomainsRequestEndpoint,
#[case] domain_name: DomainName,
#[case] expected_path: &str,
) {
validate_wp_com_rest_v1_3_endpoint(
endpoint.is_available(&domain_name, &DomainAvailabilityParams::default()),
expected_path,
);
}

#[rstest]
fn is_available_with_params(endpoint: DomainsRequestEndpoint) {
validate_wp_com_rest_v1_3_endpoint(
endpoint.is_available(
&DomainName("test.com".to_string()),
&DomainAvailabilityParams {
blog_id: Some(WpComSiteId(12345)),
is_cart_pre_check: Some(true),
vendor: Some("100-year-domains".to_string()),
},
),
"/domains/test.com/is-available?blog_id=12345&is_cart_pre_check=true&vendor=100-year-domains",
);
}

fn base_domain_suggestions_params() -> DomainSuggestionsParams {
DomainSuggestionsParams {
query: "coolsite".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions wp_api/src/wp_com/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub(crate) enum WpComNamespace {
Oauth2,
RestV1_1,
RestV1_2,
RestV1_3,
V2,
}

Expand All @@ -99,6 +100,7 @@ impl AsNamespace for WpComNamespace {
WpComNamespace::Oauth2 => "/oauth2",
WpComNamespace::RestV1_1 => "/rest/v1.1",
WpComNamespace::RestV1_2 => "/rest/v1.2",
WpComNamespace::RestV1_3 => "/rest/v1.3",
WpComNamespace::V2 => "/wpcom/v2",
}
}
Expand Down
20 changes: 20 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/available-premium.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"domain_name": "luxury.com",
"tld": "com",
"status": "available_premium",
"mappable": "mappable",
"supports_privacy": true,
"root_domain_provider": "unknown",
"product_id": 6,
"product_slug": "domain_reg",
"cost": "$5,000.00",
"renew_cost": "$5,000.00",
"renew_raw_price": 5000,
"raw_price": 5000,
"currency_code": "USD",
"is_supported_premium_domain": true,
"is_price_limit_exceeded": false,
"match_reasons": ["exact-match", "tld-exact", "tld-common"],
"vendor": "availability",
"ownership_verification_type": "no_verification_required"
}
22 changes: 22 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/available.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"domain_name": "freshsite2025.com",
"tld": "com",
"status": "available",
"mappable": "mappable",
"supports_privacy": true,
"root_domain_provider": "unknown",
"product_id": 6,
"product_slug": "domain_reg",
"cost": "$18.00",
"renew_cost": "$18.00",
"renew_raw_price": 18,
"raw_price": 18,
"currency_code": "USD",
"match_reasons": [
"exact-match",
"tld-exact",
"tld-common"
],
"vendor": "availability",
"ownership_verification_type": "no_verification_required"
}
17 changes: 17 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/dot-gay-notice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"domain_name": "testsite2025.gay",
"tld": "gay",
"status": "mappable",
"mappable": "mappable",
"supports_privacy": true,
"root_domain_provider": "unknown",
"is_dot_gay_notice_required": true,
"ownership_verification_type": "no_verification_required",
"policy_notices": [
{
"type": "gay_accept_requirements",
"label": "Special requirements",
"message": "Any anti-LGBTQ content is prohibited and can result in registration termination. The registry will donate 20% of all registration revenue to LGBTQ non-profit organizations."
}
]
}
22 changes: 22 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/hsts-required.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"domain_name": "myproject.dev",
"tld": "dev",
"status": "recent_registration_lock_not_transferrable",
"mappable": "mappable",
"supports_privacy": false,
"root_domain_provider": "unknown",
"hsts_required": true,
"product_id": 1337,
"product_slug": "domain_transfer",
"cost": "$120.00",
"raw_price": 120,
"currency_code": "USD",
"ownership_verification_type": "no_verification_required",
"policy_notices": [
{
"type": "hsts",
"label": "HSTS required",
"message": "All domains with this ending require an SSL certificate to host a website."
}
]
}
9 changes: 9 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/maintenance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain_name": "mysite.example",
"tld": "example",
"status": "tld_in_maintenance",
"mappable": "mappable",
"supports_privacy": false,
"root_domain_provider": "unknown",
"maintenance_end_time": "2026-05-01 12:00:00"
}
11 changes: 11 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/mapped-same-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain_name": "myblog.com",
"tld": "com",
"status": "mapped_to_other_site_same_user",
"mappable": "mappable",
"supports_privacy": true,
"root_domain_provider": "unknown",
"other_site_domain": "myothersite.wordpress.com",
"transferrability": "transferrable",
"ownership_verification_type": "no_verification_required"
}
8 changes: 8 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/not-available.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"domain_name": "example.com",
"tld": "com",
"status": "blacklisted_domain",
"mappable": "blacklisted_domain",
"supports_privacy": true,
"root_domain_provider": "unknown"
}
22 changes: 22 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/sale-coupon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"domain_name": "freshblog2025.online",
"tld": "online",
"status": "available",
"mappable": "mappable",
"supports_privacy": true,
"root_domain_provider": "unknown",
"product_id": 271,
"product_slug": "dotonline_domain",
"cost": "$25.00",
"renew_cost": "$25.00",
"renew_raw_price": 25,
"raw_price": 25,
"currency_code": "USD",
"sale_cost": 10.00,
"match_reasons": [
"exact-match",
"tld-exact"
],
"vendor": "availability",
"ownership_verification_type": "no_verification_required"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain_name": "mysite.ai",
"tld": "ai",
"status": "tld_not_supported",
"mappable": "mappable",
"supports_privacy": false,
"root_domain_provider": "unknown",
"ownership_verification_type": "no_verification_required"
}
14 changes: 14 additions & 0 deletions wp_api/tests/wpcom/domains/is_available/transferrable.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"domain_name": "taken-domain.io",
"tld": "io",
"status": "transferrable",
"mappable": "mappable",
"supports_privacy": true,
"root_domain_provider": "unknown",
"product_id": 1337,
"product_slug": "domain_transfer",
"cost": "$48.00",
"raw_price": 48,
"currency_code": "USD",
"ownership_verification_type": "no_verification_required"
}
72 changes: 71 additions & 1 deletion wp_com_e2e/src/domains_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::context::TestContext;
use libtest_mimic::Trial;
use std::sync::Arc;
use wp_api::wp_com::domains::CountryCode;
use wp_api::wp_com::domains::{
CountryCode, DomainAvailabilityParams, DomainAvailabilityStatus, DomainName,
};

pub fn tests(ctx: Arc<TestContext>) -> Vec<Trial> {
let mut trials = vec![];
Expand Down Expand Up @@ -83,5 +85,73 @@ pub fn tests(ctx: Arc<TestContext>) -> Vec<Trial> {
}
}));

trials.push(Trial::test("domains::is_available_taken", {
let ctx = Arc::clone(&ctx);
move || {
ctx.runtime.block_on(async {
let availability = ctx
.client
.domains()
.is_available(
&DomainName("google.com".to_string()),
&DomainAvailabilityParams::default(),
)
.await
.map_err(|e| e.to_string())?
.data;

if availability.domain_name != "google.com" {
return Err(format!(
"expected domain_name 'google.com', got '{}'",
availability.domain_name
)
.into());
}

if availability.status == DomainAvailabilityStatus::Available {
return Err("expected google.com to not be available".into());
}

Ok(())
})
}
}));

trials.push(Trial::test("domains::is_available_likely_available", {
let ctx = Arc::clone(&ctx);
move || {
ctx.runtime.block_on(async {
let availability = ctx
.client
.domains()
.is_available(
&DomainName("xyzzy-test-unlikely-taken-2025.com".to_string()),
&DomainAvailabilityParams::default(),
)
.await
.map_err(|e| e.to_string())?
.data;

if availability.status != DomainAvailabilityStatus::Available {
return Err(format!(
"expected status Available, got {:?}",
availability.status
)
.into());
}

if !availability.supports_privacy {
return Err("expected .com domain to support privacy".into());
}

if availability.product_id.is_none() {
return Err("expected product_id for available domain".into());
}

Ok(())
})
}
}));

trials
}