From 9bc5a02f8afe3080f9fb2bfe3193d55dcd41cac6 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 10:32:56 -0700 Subject: [PATCH 01/14] feat: domain availability + suggest via spec-generated client (DEVEX) Adds a `gddy domain` command group with `available` and `suggest`, built on two patterns: - Spec-generated HTTP client: new `domains-client` workspace crate generates a typed reqwest client from a vendored, trimmed OpenAPI 3.0 spec via progenitor at build time (scripts/regenerate-spec.sh converts the upstream Swagger 2.0 Domains spec). Pinned to progenitor 0.11 to keep the workspace on one reqwest 0.12 + ring rustls stack (clean Windows cross-build). - Composite auth: `CompositeAuthProvider` wraps the OAuth provider and, for `domain:*` commands with a configured sso-key, authenticates with `Authorization: sso-key KEY:SECRET`; otherwise it falls back to OAuth (the domain endpoints are OAuth-enabled). Commands declare the `domains.domain:read` scope (per the api-domain-data scope whitelist) so OAuth scope step-up mints a token that carries it. Environment config gains optional per-env `domains_api_url`, `api_key`, `api_secret` (sso-key), resolved with the existing env-var/flag precedence; adds `--api-key`/`--api-secret` global flags and a `rust/examples/environments.toml` template for internal dev/test. Built-in ote/prod OAuth client ids updated; adds RUST_LOG=debug visibility into the resolved OAuth client/endpoints in auth.rs. Co-Authored-By: Claude Opus 4.8 --- rust/Cargo.lock | 278 +- rust/Cargo.toml | 3 +- rust/domains-client/Cargo.toml | 26 + rust/domains-client/build.rs | 25 + rust/domains-client/openapi/domains.oas3.json | 511 + .../openapi/swagger_domains.v2.json | 9691 +++++++++++++++++ .../domains-client/scripts/regenerate-spec.sh | 107 + rust/domains-client/src/lib.rs | 52 + rust/examples/environments.toml | 35 + rust/src/auth.rs | 209 + rust/src/domain/mod.rs | 265 + rust/src/environments/mod.rs | 156 +- rust/src/main.rs | 27 +- 13 files changed, 11373 insertions(+), 12 deletions(-) create mode 100644 rust/domains-client/Cargo.toml create mode 100644 rust/domains-client/build.rs create mode 100644 rust/domains-client/openapi/domains.oas3.json create mode 100644 rust/domains-client/openapi/swagger_domains.v2.json create mode 100755 rust/domains-client/scripts/regenerate-spec.sh create mode 100644 rust/domains-client/src/lib.rs create mode 100644 rust/examples/environments.toml create mode 100644 rust/src/domain/mod.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 2464036..73e73c4 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -22,6 +22,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -550,7 +556,7 @@ dependencies = [ "rand 0.9.4", "regex", "reqwest", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "sha2", @@ -742,6 +748,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "domains-client" +version = "0.1.0" +dependencies = [ + "bytes", + "futures", + "openapiv3", + "prettyplease", + "progenitor", + "progenitor-client", + "reqwest", + "serde", + "serde_json", + "syn 2.0.117", + "thiserror 2.0.18", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -874,6 +897,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -883,6 +912,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -899,6 +943,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -947,6 +1002,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -1043,6 +1099,7 @@ dependencies = [ "clap", "cli-engine", "dirs", + "domains-client", "fancy-regex", "httpmock", "regex", @@ -1074,7 +1131,18 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", ] [[package]] @@ -1830,6 +1898,17 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openapiv3" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8d427828b22ae1fff2833a03d8486c2c881367f1c336349f307f321e7f4d05" +dependencies = [ + "indexmap", + "serde", + "serde_json", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2007,6 +2086,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "progenitor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2326f73d5326257514712436680ef8da4543ee47c0e9e0d501545c8909ee12e4" +dependencies = [ + "progenitor-client", + "progenitor-impl", + "progenitor-macro", +] + +[[package]] +name = "progenitor-client" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71a0beb939758f229cbae70a4889c7c76a4ac0e90f0b1e7ae9b4636a927d1018" +dependencies = [ + "bytes", + "futures-core", + "percent-encoding", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", +] + +[[package]] +name = "progenitor-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f6d9109b04e005bbdec84cacec7e81cc15533f2b5dc505f0defc212d270c15" +dependencies = [ + "heck", + "http 1.4.1", + "indexmap", + "openapiv3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "syn 2.0.117", + "thiserror 2.0.18", + "typify", + "unicode-ident", +] + +[[package]] +name = "progenitor-macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46596c574831739c661f22923fe587399c61f5e3e79b73cc9a93644c72248d84" +dependencies = [ + "openapiv3", + "proc-macro2", + "progenitor-impl", + "quote", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_tokenstream", + "serde_yaml", + "syn 2.0.117", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2222,6 +2367,16 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "regress" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" +dependencies = [ + "hashbrown 0.16.1", + "memchr", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -2253,12 +2408,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "webpki-roots", ] @@ -2352,6 +2509,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "chrono", + "dyn-clone", + "schemars_derive 0.8.22", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "schemars" version = "1.2.1" @@ -2360,11 +2531,23 @@ checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", - "schemars_derive", + "schemars_derive 1.2.1", "serde", "serde_json", ] +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + [[package]] name = "schemars_derive" version = "1.2.1" @@ -2443,6 +2626,10 @@ name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "serde" @@ -2528,6 +2715,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_tokenstream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c49585c52c01f13c5c2ebb333f14f6885d76daa768d8a037d28017ec538c69" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2878,6 +3077,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" @@ -3067,6 +3279,53 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +[[package]] +name = "typify" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7144144e97e987c94758a3017c920a027feac0799df325d6df4fc8f08d02068e" +dependencies = [ + "typify-impl", + "typify-macro", +] + +[[package]] +name = "typify-impl" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062879d46aa4c9dfe0d33b035bbaf512da192131645d05deacb7033ec8581a09" +dependencies = [ + "heck", + "log", + "proc-macro2", + "quote", + "regress", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "syn 2.0.117", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "typify-macro" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9708a3ceb6660ba3f8d2b8f0567e7d4b8b198e2b94d093b8a6077a751425de9e" +dependencies = [ + "proc-macro2", + "quote", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "serde_tokenstream", + "syn 2.0.117", + "typify-impl", +] + [[package]] name = "uds_windows" version = "1.2.1" @@ -3281,6 +3540,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index d40bd03..6c6a425 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -6,7 +6,7 @@ description = "GoDaddy developer CLI" license = "Proprietary" [workspace] -members = [".", "tools/generate-api-catalog"] +members = [".", "tools/generate-api-catalog", "domains-client"] [[bin]] name = "gddy" @@ -18,6 +18,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "serd clap = { version = "4.5", features = ["std", "string"] } cli-engine = { features = ["pkce-auth"], version = "0.2.1" } dirs = "6" +domains-client = { path = "domains-client" } fancy-regex = "0.14" regex = { version = "1", features = ["std"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } diff --git a/rust/domains-client/Cargo.toml b/rust/domains-client/Cargo.toml new file mode 100644 index 0000000..2240f91 --- /dev/null +++ b/rust/domains-client/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "domains-client" +version = "0.1.0" +edition = "2024" +license = "Proprietary" +description = "Generated GoDaddy Domains API client (availability + suggest), produced from the vendored OpenAPI 3.0 spec by progenitor at build time." + +[dependencies] +# Pinned to 0.11 (the latest progenitor on reqwest 0.12): keeps the whole +# workspace on one reqwest + ring-based rustls-tls stack as cli-engine/the main +# crate, which avoids aws-lc-rs (reqwest 0.13's only rustls provider) and keeps +# the Windows-msvc cross-build clean. +progenitor-client = "0.11" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +futures = "0.3" +bytes = "1" +thiserror = "2" + +[build-dependencies] +progenitor = "0.11" +openapiv3 = "2" +serde_json = "1" +prettyplease = "0.2" +syn = "2" diff --git a/rust/domains-client/build.rs b/rust/domains-client/build.rs new file mode 100644 index 0000000..341aba9 --- /dev/null +++ b/rust/domains-client/build.rs @@ -0,0 +1,25 @@ +//! Generates the typed Domains API client from the vendored OpenAPI 3.0 spec. +//! +//! The spec (`openapi/domains.oas3.json`) is committed and trimmed to the +//! availability + suggest operations (see `scripts/regenerate-spec.sh`); this +//! build step is hermetic and never touches the network. + +use std::{env, fs, path::Path}; + +fn main() -> Result<(), Box> { + let spec_path = "openapi/domains.oas3.json"; + println!("cargo:rerun-if-changed={spec_path}"); + println!("cargo:rerun-if-changed=build.rs"); + + let spec_text = fs::read_to_string(spec_path)?; + let spec: openapiv3::OpenAPI = serde_json::from_str(&spec_text)?; + + let mut generator = progenitor::Generator::default(); + let tokens = generator.generate_tokens(&spec)?; + let ast = syn::parse2(tokens)?; + let formatted = prettyplease::unparse(&ast); + + let out_dir = env::var("OUT_DIR")?; + fs::write(Path::new(&out_dir).join("codegen.rs"), formatted)?; + Ok(()) +} diff --git a/rust/domains-client/openapi/domains.oas3.json b/rust/domains-client/openapi/domains.oas3.json new file mode 100644 index 0000000..4835b16 --- /dev/null +++ b/rust/domains-client/openapi/domains.oas3.json @@ -0,0 +1,511 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "GoDaddy Domains API (availability subset)", + "version": "1.0.0" + }, + "paths": { + "/v1/domains/available": { + "get": { + "tags": [ + "v1" + ], + "parameters": [ + { + "description": "Domain name whose availability is to be checked", + "in": "query", + "name": "domain", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Optimize for time ('FAST') or accuracy ('FULL')", + "in": "query", + "name": "checkType", + "required": false, + "schema": { + "type": "string", + "enum": [ + "FAST", + "FULL" + ], + "default": "FAST" + } + }, + { + "description": "Whether or not to include domains available for transfer. If set to True, checkType is ignored", + "in": "query", + "name": "forTransfer", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Request was successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DomainAvailableResponse" + } + } + } + } + }, + "operationId": "available", + "summary": "Determine whether or not the specified domain is available for purchase" + } + }, + "/v1/domains/suggest": { + "get": { + "tags": [ + "v1" + ], + "parameters": [ + { + "description": "Shopper ID for which the suggestions are being generated", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Domain name or set of keywords for which alternative domain names will be suggested", + "in": "query", + "name": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "description": "Two-letter ISO country code to be used as a hint for target region

\nNOTE: These are sample values, there are many\nmore", + "in": "query", + "name": "country", + "required": false, + "schema": { + "type": "string", + "format": "iso-country-code", + "enum": [ + "AC", + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KR", + "KV", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "ST", + "SV", + "SX", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TP", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW" + ] + } + }, + { + "description": "Name of city to be used as a hint for target region", + "in": "query", + "name": "city", + "required": false, + "schema": { + "type": "string", + "format": "city-name" + } + }, + { + "description": "Sources to be queried

    \n
  • CC_TLD - Varies the TLD using Country Codes
  • \n
  • EXTENSION - Varies the TLD
  • \n
  • KEYWORD_SPIN - Identifies keywords and then rotates each one
  • \n
  • PREMIUM - Includes variations with premium prices
", + "in": "query", + "name": "sources", + "required": false, + "style": "form", + "explode": false, + "schema": { + "type": "array", + "items": { + "enum": [ + "CC_TLD", + "EXTENSION", + "KEYWORD_SPIN", + "PREMIUM", + "cctld", + "keywordspin" + ], + "type": "string" + } + } + }, + { + "description": "Top-level domains to be included in suggestions

\nNOTE: These are sample values, there are many\nmore", + "in": "query", + "name": "tlds", + "required": false, + "style": "form", + "explode": false, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "description": "Maximum length of second-level domain", + "in": "query", + "name": "lengthMax", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Minimum length of second-level domain", + "in": "query", + "name": "lengthMin", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Maximum number of suggestions to return", + "in": "query", + "name": "limit", + "required": false, + "schema": { + "type": "integer" + } + }, + { + "description": "Maximum amount of time, in milliseconds, to wait for responses\nIf elapses, return the results compiled up to that point", + "in": "query", + "name": "waitMs", + "required": false, + "schema": { + "type": "integer", + "format": "integer-positive", + "default": 1000 + } + } + ], + "responses": { + "200": { + "description": "Request was successful", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/DomainSuggestion" + }, + "type": "array" + } + } + } + } + }, + "operationId": "suggest", + "summary": "Suggest alternate Domain names based on a seed Domain, a set of keywords, or the shopper's purchase history" + } + } + }, + "servers": [ + { + "url": "https://api.ote-godaddy.com" + } + ], + "components": { + "schemas": { + "DomainAvailableResponse": { + "properties": { + "available": { + "description": "Whether or not the domain name is available", + "type": "boolean" + }, + "currency": { + "default": "USD", + "description": "Currency in which the `price` is listed. Only returned if tld is offered", + "format": "iso-currency-code", + "type": "string" + }, + "definitive": { + "description": "Whether or not the `available` answer has been definitively verified with the registry", + "type": "boolean" + }, + "domain": { + "description": "Domain name", + "type": "string" + }, + "period": { + "description": "Number of years included in the price. Only returned if tld is offered", + "format": "integer-positive", + "type": "integer" + }, + "price": { + "description": "Price of the domain excluding taxes or fees. Only returned if tld is offered", + "format": "currency-micro-unit", + "type": "integer" + }, + "renewalPrice": { + "description": "Price for renewing the domain excluding taxes or fees. Only returned if tld is offered", + "format": "currency-micro-unit", + "type": "integer" + } + }, + "required": [ + "domain", + "available", + "definitive" + ] + }, + "DomainSuggestion": { + "properties": { + "domain": { + "description": "Suggested domain name", + "type": "string" + } + }, + "required": [ + "domain" + ] + } + } + } +} \ No newline at end of file diff --git a/rust/domains-client/openapi/swagger_domains.v2.json b/rust/domains-client/openapi/swagger_domains.v2.json new file mode 100644 index 0000000..2839209 --- /dev/null +++ b/rust/domains-client/openapi/swagger_domains.v2.json @@ -0,0 +1,9691 @@ +{ + "swagger": "2.0", + "info": { + "title": "Domains API", + "description": "

The Domains API is for domain-related actions such as purchasing, renewing, or managing domains.

Updates to domains generally require the domain to be in an `ACTIVE` status. Some update actions (such as updating nameservers) on protected and high-value domains requires 2FA which is currently not supported via the api.

" + }, + "tags": [ + { + "name": "v1", + "description": "" + }, + { + "name": "Domains", + "description": "" + }, + { + "name": "Actions", + "description": "" + }, + { + "name": "Notifications", + "description": "" + } + ], + "host": "api.ote-godaddy.com", + "paths": { + "/v1/domains": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID whose domains are to be retrieved", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Only include results with `status` value in the specified set", + "in": "query", + "items": { + "enum": [ + "ACTIVE", + "AWAITING_CLAIM_ACK", + "AWAITING_DOCUMENT_AFTER_TRANSFER", + "AWAITING_DOCUMENT_AFTER_UPDATE_ACCOUNT", + "AWAITING_DOCUMENT_UPLOAD", + "AWAITING_FAILED_TRANSFER_WHOIS_PRIVACY", + "AWAITING_PAYMENT", + "AWAITING_RENEWAL_TRANSFER_IN_COMPLETE", + "AWAITING_TRANSFER_IN_ACK", + "AWAITING_TRANSFER_IN_AUTH", + "AWAITING_TRANSFER_IN_AUTO", + "AWAITING_TRANSFER_IN_WHOIS", + "AWAITING_TRANSFER_IN_WHOIS_FIX", + "AWAITING_VERIFICATION_ICANN", + "AWAITING_VERIFICATION_ICANN_MANUAL", + "CANCELLED", + "CANCELLED_HELD", + "CANCELLED_REDEEMABLE", + "CANCELLED_TRANSFER", + "CONFISCATED", + "DISABLED_SPECIAL", + "EXCLUDED_INVALID_CLAIM_FIREHOSE", + "EXPIRED_REASSIGNED", + "FAILED_BACKORDER_CAPTURE", + "FAILED_DROP_IMMEDIATE_THEN_ADD", + "FAILED_PRE_REGISTRATION", + "FAILED_REDEMPTION", + "FAILED_REDEMPTION_REPORT", + "FAILED_REGISTRATION", + "FAILED_REGISTRATION_FIREHOSE", + "FAILED_RESTORATION_REDEMPTION_MOCK", + "FAILED_SETUP", + "FAILED_TRANSFER_IN", + "FAILED_TRANSFER_IN_BAD_STATUS", + "FAILED_TRANSFER_IN_REGISTRY", + "HELD_COURT_ORDERED", + "HELD_DISPUTED", + "HELD_EXPIRATION_PROTECTION", + "HELD_EXPIRED_REDEMPTION_MOCK", + "HELD_REGISTRAR_ADD", + "HELD_REGISTRAR_REMOVE", + "HELD_SHOPPER", + "HELD_TEMPORARY", + "LOCKED_ABUSE", + "LOCKED_COPYRIGHT", + "LOCKED_REGISTRY", + "LOCKED_SUPER", + "PARKED_AND_HELD", + "PARKED_EXPIRED", + "PARKED_VERIFICATION_ICANN", + "PENDING_ABORT_CANCEL_SETUP", + "PENDING_AGREEMENT_PRE_REGISTRATION", + "PENDING_APPLY_RENEWAL_CREDITS", + "PENDING_BACKORDER_CAPTURE", + "PENDING_BLOCKED_REGISTRY", + "PENDING_CANCEL_REGISTRANT_PROFILE", + "PENDING_COMPLETE_REDEMPTION_WITHOUT_RECEIPT", + "PENDING_COMPLETE_REGISTRANT_PROFILE", + "PENDING_COO", + "PENDING_COO_COMPLETE", + "PENDING_DNS", + "PENDING_DNS_ACTIVE", + "PENDING_DNS_INACTIVE", + "PENDING_DOCUMENT_VALIDATION", + "PENDING_DOCUMENT_VERIFICATION", + "PENDING_DROP_IMMEDIATE", + "PENDING_DROP_IMMEDIATE_THEN_ADD", + "PENDING_EPP_CREATE", + "PENDING_EPP_DELETE", + "PENDING_EPP_UPDATE", + "PENDING_ESCALATION_REGISTRY", + "PENDING_EXPIRATION", + "PENDING_EXPIRATION_RESPONSE", + "PENDING_EXPIRATION_SYNC", + "PENDING_EXPIRED_REASSIGNMENT", + "PENDING_EXPIRE_AUTO_ADD", + "PENDING_EXTEND_REGISTRANT_PROFILE", + "PENDING_FAILED_COO", + "PENDING_FAILED_EPP_CREATE", + "PENDING_FAILED_HELD", + "PENDING_FAILED_PURCHASE_PREMIUM", + "PENDING_FAILED_RECONCILE_FIREHOSE", + "PENDING_FAILED_REDEMPTION_WITHOUT_RECEIPT", + "PENDING_FAILED_RELEASE_PREMIUM", + "PENDING_FAILED_RENEW_EXPIRATION_PROTECTION", + "PENDING_FAILED_RESERVE_PREMIUM", + "PENDING_FAILED_SUBMIT_FIREHOSE", + "PENDING_FAILED_TRANSFER_ACK_PREMIUM", + "PENDING_FAILED_TRANSFER_IN_ACK_PREMIUM", + "PENDING_FAILED_TRANSFER_IN_PREMIUM", + "PENDING_FAILED_TRANSFER_PREMIUM", + "PENDING_FAILED_TRANSFER_SUBMIT_PREMIUM", + "PENDING_FAILED_UNLOCK_PREMIUM", + "PENDING_FAILED_UPDATE_API", + "PENDING_FRAUD_VERIFICATION", + "PENDING_FRAUD_VERIFIED", + "PENDING_GET_CONTACTS", + "PENDING_GET_HOSTS", + "PENDING_GET_NAME_SERVERS", + "PENDING_GET_STATUS", + "PENDING_HOLD_ESCROW", + "PENDING_HOLD_REDEMPTION", + "PENDING_LOCK_CLIENT_REMOVE", + "PENDING_LOCK_DATA_QUALITY", + "PENDING_LOCK_THEN_HOLD_REDEMPTION", + "PENDING_PARKING_DETERMINATION", + "PENDING_PARK_INVALID_WHOIS", + "PENDING_PARK_INVALID_WHOIS_REMOVAL", + "PENDING_PURCHASE_PREMIUM", + "PENDING_RECONCILE", + "PENDING_RECONCILE_FIREHOSE", + "PENDING_REDEMPTION", + "PENDING_REDEMPTION_REPORT", + "PENDING_REDEMPTION_REPORT_COMPLETE", + "PENDING_REDEMPTION_REPORT_SUBMITTED", + "PENDING_REDEMPTION_WITHOUT_RECEIPT", + "PENDING_REDEMPTION_WITHOUT_RECEIPT_MOCK", + "PENDING_RELEASE_PREMIUM", + "PENDING_REMOVAL", + "PENDING_REMOVAL_HELD", + "PENDING_REMOVAL_PARKED", + "PENDING_REMOVAL_UNPARK", + "PENDING_RENEWAL", + "PENDING_RENEW_EXPIRATION_PROTECTION", + "PENDING_RENEW_INFINITE", + "PENDING_RENEW_LOCKED", + "PENDING_RENEW_WITHOUT_RECEIPT", + "PENDING_REPORT_REDEMPTION_WITHOUT_RECEIPT", + "PENDING_RESERVE_PREMIUM", + "PENDING_RESET_VERIFICATION_ICANN", + "PENDING_RESPONSE_FIREHOSE", + "PENDING_RESTORATION", + "PENDING_RESTORATION_INACTIVE", + "PENDING_RESTORATION_REDEMPTION_MOCK", + "PENDING_RETRY_EPP_CREATE", + "PENDING_RETRY_HELD", + "PENDING_SEND_AUTH_CODE", + "PENDING_SETUP", + "PENDING_SETUP_ABANDON", + "PENDING_SETUP_AGREEMENT_LANDRUSH", + "PENDING_SETUP_AGREEMENT_SUNRISE2_A", + "PENDING_SETUP_AGREEMENT_SUNRISE2_B", + "PENDING_SETUP_AGREEMENT_SUNRISE2_C", + "PENDING_SETUP_AUTH", + "PENDING_SETUP_DNS", + "PENDING_SETUP_FAILED", + "PENDING_SETUP_REVIEW", + "PENDING_SETUP_SUNRISE", + "PENDING_SETUP_SUNRISE_PRE", + "PENDING_SETUP_SUNRISE_RESPONSE", + "PENDING_SUBMIT_FAILURE", + "PENDING_SUBMIT_FIREHOSE", + "PENDING_SUBMIT_HOLD_FIREHOSE", + "PENDING_SUBMIT_HOLD_LANDRUSH", + "PENDING_SUBMIT_HOLD_SUNRISE", + "PENDING_SUBMIT_LANDRUSH", + "PENDING_SUBMIT_RESPONSE_FIREHOSE", + "PENDING_SUBMIT_RESPONSE_LANDRUSH", + "PENDING_SUBMIT_RESPONSE_SUNRISE", + "PENDING_SUBMIT_SUCCESS_FIREHOSE", + "PENDING_SUBMIT_SUCCESS_LANDRUSH", + "PENDING_SUBMIT_SUCCESS_SUNRISE", + "PENDING_SUBMIT_SUNRISE", + "PENDING_SUBMIT_WAITING_LANDRUSH", + "PENDING_SUCCESS_PRE_REGISTRATION", + "PENDING_SUSPENDED_DATA_QUALITY", + "PENDING_TRANSFER_ACK_PREMIUM", + "PENDING_TRANSFER_IN", + "PENDING_TRANSFER_IN_ACK", + "PENDING_TRANSFER_IN_ACK_PREMIUM", + "PENDING_TRANSFER_IN_BAD_REGISTRANT", + "PENDING_TRANSFER_IN_CANCEL", + "PENDING_TRANSFER_IN_CANCEL_REGISTRY", + "PENDING_TRANSFER_IN_COMPLETE_ACK", + "PENDING_TRANSFER_IN_DELETE", + "PENDING_TRANSFER_IN_LOCK", + "PENDING_TRANSFER_IN_NACK", + "PENDING_TRANSFER_IN_NOTIFICATION", + "PENDING_TRANSFER_IN_PREMIUM", + "PENDING_TRANSFER_IN_RELEASE", + "PENDING_TRANSFER_IN_RESPONSE", + "PENDING_TRANSFER_IN_UNDERAGE", + "PENDING_TRANSFER_OUT", + "PENDING_TRANSFER_OUT_ACK", + "PENDING_TRANSFER_OUT_NACK", + "PENDING_TRANSFER_OUT_PREMIUM", + "PENDING_TRANSFER_OUT_UNDERAGE", + "PENDING_TRANSFER_OUT_VALIDATION", + "PENDING_TRANSFER_PREMIUM", + "PENDING_TRANSFER_PREMUIM", + "PENDING_TRANSFER_SUBMIT_PREMIUM", + "PENDING_UNLOCK_DATA_QUALITY", + "PENDING_UNLOCK_PREMIUM", + "PENDING_UPDATE", + "PENDING_UPDATED_REGISTRANT_DATA_QUALITY", + "PENDING_UPDATE_ACCOUNT", + "PENDING_UPDATE_API", + "PENDING_UPDATE_API_RESPONSE", + "PENDING_UPDATE_AUTH", + "PENDING_UPDATE_CONTACTS", + "PENDING_UPDATE_CONTACTS_PRIVACY", + "PENDING_UPDATE_DNS", + "PENDING_UPDATE_DNS_SECURITY", + "PENDING_UPDATE_ELIGIBILITY", + "PENDING_UPDATE_EPP_CONTACTS", + "PENDING_UPDATE_MEMBERSHIP", + "PENDING_UPDATE_OWNERSHIP", + "PENDING_UPDATE_OWNERSHIP_AUTH_AUCTION", + "PENDING_UPDATE_OWNERSHIP_HELD", + "PENDING_UPDATE_REGISTRANT", + "PENDING_UPDATE_REPO", + "PENDING_VALIDATION_DATA_QUALITY", + "PENDING_VERIFICATION_FRAUD", + "PENDING_VERIFICATION_STATUS", + "PENDING_VERIFY_REGISTRANT_DATA_QUALITY", + "RESERVED", + "RESERVED_PREMIUM", + "REVERTED", + "SUSPENDED_VERIFICATION_ICANN", + "TRANSFERRED_OUT", + "UNLOCKED_ABUSE", + "UNLOCKED_SUPER", + "UNPARKED_AND_UNHELD", + "UPDATED_OWNERSHIP", + "UPDATED_OWNERSHIP_HELD" + ], + "type": "string" + }, + "name": "statuses", + "required": false, + "type": "array" + }, + { + "description": "Only include results with `status` value in any of the specified groups", + "in": "query", + "items": { + "enum": [ + "INACTIVE", + "PRE_REGISTRATION", + "REDEMPTION", + "RENEWABLE", + "VERIFICATION_ICANN", + "VISIBLE" + ], + "type": "string" + }, + "name": "statusGroups", + "required": false, + "type": "array" + }, + { + "description": "Maximum number of domains to return", + "in": "query", + "maximum": 1000, + "minimum": 1, + "name": "limit", + "required": false, + "type": "integer" + }, + { + "description": "Marker Domain to use as the offset in results", + "in": "query", + "name": "marker", + "required": false, + "type": "string" + }, + { + "description": "Optional details to be included in the response", + "in": "query", + "items": { + "enum": [ + "authCode", + "contacts", + "nameServers" + ], + "type": "string" + }, + "name": "includes", + "required": false, + "type": "array" + }, + { + "description": "Only include results that have been modified since the specified date", + "format": "iso-datetime", + "in": "query", + "name": "modifiedDate", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/DomainSummary" + }, + "type": "array" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Limit must have a value no greater than 1000", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "list", + "summary": "Retrieve a list of Domains for the specified Shopper" + } + }, + "/v1/domains/agreements": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "default": "en-US", + "description": "Unique identifier of the Market used to retrieve/translate Legal Agreements", + "format": "bcp-47", + "in": "header", + "name": "X-Market-Id", + "required": false, + "type": "string" + }, + { + "description": "list of TLDs whose legal agreements are to be retrieved", + "in": "query", + "items": { + "type": "string" + }, + "name": "tlds", + "required": true, + "type": "array" + }, + { + "description": "Whether or not privacy has been requested", + "in": "query", + "name": "privacy", + "required": true, + "type": "boolean" + }, + { + "description": "Whether or not domain tranfer has been requested", + "in": "query", + "name": "forTransfer", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/LegalAgreement" + }, + "type": "array" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "getAgreement", + "summary": "Retrieve the legal agreement(s) required to purchase the specified TLD and add-ons" + } + }, + "/v1/domains/available": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Domain name whose availability is to be checked", + "in": "query", + "name": "domain", + "required": true, + "type": "string" + }, + { + "default": "FAST", + "description": "Optimize for time ('FAST') or accuracy ('FULL')", + "enum": [ + "FAST", + "FULL", + "fast", + "full" + ], + "in": "query", + "name": "checkType", + "required": false, + "type": "string" + }, + { + "default": false, + "description": "Whether or not to include domains available for transfer. If set to True, checkType is ignored", + "in": "query", + "name": "forTransfer", + "required": false, + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainAvailableResponse" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Cannot convert domain label error
Domain is missing IDN script
Domain segment ends with dash
Domain starts with dashbr>Domain uses unsupported IDN script
FQDN fails generic validity regex
Invalid character(s) error
Invalid tld error
Non-IDN domain name must not have dashes at the third and fourth position
Reserved name error
domain must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "available", + "summary": "Determine whether or not the specified domain is available for purchase" + }, + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Domain names for which to check availability", + "in": "body", + "name": "domains", + "required": true, + "schema": { + "items": { + "type": "string" + }, + "maximum": 500, + "type": "array" + } + }, + { + "default": "FAST", + "description": "Optimize for time ('FAST') or accuracy ('FULL')", + "enum": [ + "FAST", + "FULL", + "fast", + "full" + ], + "in": "query", + "name": "checkType", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainAvailableBulk" + } + }, + "203": { + "description": "Request was partially successful", + "schema": { + "$ref": "#/definitions/DomainAvailableBulkMixed" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Cannot convert domain label error
Domain is missing IDN script
Domain segment ends with dash
Domain starts with dash
Domain uses unsupported IDN script
FQDN fails generic validity regex
Invalid character(s) error
Invalid tld error
Non-IDN domain name must not have dashes at the third and fourth position
Reserved name error
Reserved name error
domain must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "availableBulk", + "summary": "Determine whether or not the specified domains are available for purchase" + } + }, + "/v1/domains/contacts/validate": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "default": 1, + "description": "PrivateLabelId to operate as, if different from JWT", + "in": "header", + "name": "X-Private-Label-Id", + "required": false, + "type": "integer" + }, + { + "default": "en-US", + "description": "MarketId in which the request is being made, and for which responses should be localized", + "format": "bcp-47", + "in": "query", + "name": "marketId", + "required": false, + "type": "string" + }, + { + "description": "An instance document expected for domains contacts validation", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainsContactsBulk" + } + } + ], + "responses": { + "200": { + "description": "No response was specified" + }, + "204": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/ErrorDomainContactsValidate" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "description": "All contacts specified in request will be validated against all domains specifed in \"domains\". As an alternative, you can also pass in tlds, with the exception of `uk`, which requires full domain names", + "operationId": "ContactsValidate", + "summary": "Validate the request body using the Domain Contact Validation Schema for specified domains." + } + }, + "/v1/domains/purchase": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "The Shopper for whom the domain should be purchased", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "An instance document expected to match the JSON schema returned by `./schema/{tld}`", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainPurchase" + } + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainPurchaseResponse" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "domain must be specified
Based on restrictions declared in JSON schema returned by `./schema/{tld}`
Cannot convert domain label error
Domain is missing IDN script
Domain segment ends with dash
Domain starts with dash
Domain uses unsupported IDN script
FQDN fails generic validity regex
Invalid character(s) error
Invalid tld error
Non-IDN domain name must not have dashes at the third and fourth position
Reserved name error
`body` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "purchase", + "summary": "Purchase and register the specified Domain" + } + }, + "/v1/domains/purchase/schema/{tld}": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "The Top-Level Domain whose schema should be retrieved", + "in": "path", + "name": "tld", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/JsonSchema" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`tld` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "schema", + "summary": "Retrieve the schema to be submitted when registering a Domain for the specified TLD" + } + }, + "/v1/domains/purchase/validate": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "An instance document expected to match the JSON schema returned by `./schema/{tld}`", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainPurchase" + } + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Based on restrictions declared in JSON schema returned by `./schema/{tld}`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "validate", + "summary": "Validate the request body using the Domain Purchase Schema for the specified TLD" + } + }, + "/v1/domains/suggest": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID for which the suggestions are being generated", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain name or set of keywords for which alternative domain names will be suggested", + "in": "query", + "name": "query", + "required": false, + "type": "string" + }, + { + "description": "Two-letter ISO country code to be used as a hint for target region

\nNOTE: These are sample values, there are many\nmore", + "enum": [ + "AC", + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KR", + "KV", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "ST", + "SV", + "SX", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TP", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW" + ], + "format": "iso-country-code", + "in": "query", + "name": "country", + "required": false, + "type": "string" + }, + { + "description": "Name of city to be used as a hint for target region", + "format": "city-name", + "in": "query", + "name": "city", + "required": false, + "type": "string" + }, + { + "description": "Sources to be queried

    \n
  • CC_TLD - Varies the TLD using Country Codes
  • \n
  • EXTENSION - Varies the TLD
  • \n
  • KEYWORD_SPIN - Identifies keywords and then rotates each one
  • \n
  • PREMIUM - Includes variations with premium prices
", + "in": "query", + "items": { + "enum": [ + "CC_TLD", + "EXTENSION", + "KEYWORD_SPIN", + "PREMIUM", + "cctld", + "extension", + "keywordspin", + "premium" + ], + "type": "string" + }, + "name": "sources", + "required": false, + "type": "array" + }, + { + "description": "Top-level domains to be included in suggestions

\nNOTE: These are sample values, there are many\nmore", + "in": "query", + "items": { + "type": "string" + }, + "name": "tlds", + "required": false, + "type": "array" + }, + { + "description": "Maximum length of second-level domain", + "in": "query", + "name": "lengthMax", + "required": false, + "type": "integer" + }, + { + "description": "Minimum length of second-level domain", + "in": "query", + "name": "lengthMin", + "required": false, + "type": "integer" + }, + { + "description": "Maximum number of suggestions to return", + "in": "query", + "name": "limit", + "required": false, + "type": "integer" + }, + { + "default": 1000, + "description": "Maximum amount of time, in milliseconds, to wait for responses\nIf elapses, return the results compiled up to that point", + "format": "integer-positive", + "in": "query", + "name": "waitMs", + "required": false, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/DomainSuggestion" + }, + "type": "array" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`query` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "suggest", + "summary": "Suggest alternate Domain names based on a seed Domain, a set of keywords, or the shopper's purchase history" + } + }, + "/v1/domains/tlds": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/TldSummary" + }, + "type": "array" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "tlds", + "summary": "Retrieves a list of TLDs supported and enabled for sale" + } + }, + "/v1/domains/{domain}": { + "delete": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Domain to cancel", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Unknown domain error
At least two apex (aka @) `nameServers` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "cancel", + "summary": "Cancel a purchased domain" + }, + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID expected to own the specified domain", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain name whose details are to be retrieved", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainDetail" + } + }, + "203": { + "description": "Request was partially successful, see verifications.status for further detail", + "schema": { + "$ref": "#/definitions/DomainDetail" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "get", + "summary": "Retrieve details for the specified Domain" + }, + "patch": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Domain whose details are to be updated", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "Shopper for whom Domain is to be updated. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Changes to apply to existing Domain", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainUpdate" + } + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Specified Subaccount not owned by authenticated Shopper", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The given domain is not eligible to have its nameservers changed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "At least two apex (aka @) `nameServers` must be specified
Failed to update nameservers", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "update", + "summary": "Update details for the specified Domain" + } + }, + "/v1/domains/{domain}/contacts": { + "patch": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper for whom domain contacts are to be updated. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose Contacts are to be updated.", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "Changes to apply to existing Contacts", + "in": "body", + "name": "contacts", + "required": true, + "schema": { + "$ref": "#/definitions/DomainContacts" + } + } + ], + "responses": { + "200": { + "description": "No response was specified" + }, + "204": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Domain not found
Identity document not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` is not a valid Domain name", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "updateContacts", + "summary": "Update domain" + } + }, + "/v1/domains/{domain}/privacy": { + "delete": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID of the owner of the domain", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose privacy is to be cancelled", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Customer has purchased Domain Ownership Protection and the domain has expired
The domain status does not allow performing the operation
Unknown domain error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "cancelPrivacy", + "summary": "Submit a privacy cancellation request for the given domain" + } + }, + "/v1/domains/{domain}/privacy/purchase": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID of the owner of the domain", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain for which to purchase privacy", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "Options for purchasing privacy", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PrivacyPurchase" + } + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainPurchaseResponse" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "End-user must read and consent to all of the following legal agreements
`domain` must match `sld.tld`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "purchasePrivacy", + "summary": "Purchase privacy for a specified domain" + } + }, + "/v1/domains/{domain}/records": { + "patch": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID which owns the domain. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose DNS Records are to be augmented", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "DNS Records to add to whatever currently exists", + "in": "body", + "name": "records", + "required": true, + "schema": { + "$ref": "#/definitions/ArrayOfDNSRecord" + } + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` is not a valid Domain name", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "recordAdd", + "summary": "Add the specified DNS Records to the specified Domain" + }, + "put": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID which owns the domain. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose DNS Records are to be replaced", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "DNS Records to replace whatever currently exists", + "in": "body", + "name": "records", + "required": true, + "schema": { + "items": { + "$ref": "#/definitions/DNSRecord" + }, + "type": "array" + } + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` is not a valid Domain name
`record` does not fulfill the schema", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "recordReplace", + "summary": "Replace all DNS Records for the specified Domain" + } + }, + "/v1/domains/{domain}/records/{type}/{name}": { + "get": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID which owns the domain. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose DNS Records are to be retrieved", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Type for which DNS Records are to be retrieved", + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "SOA", + "SRV", + "TXT" + ], + "in": "path", + "name": "type", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Name for which DNS Records are to be retrieved", + "in": "path", + "name": "name", + "required": true, + "type": "string" + }, + { + "description": "Number of results to skip for pagination", + "in": "query", + "name": "offset", + "required": false, + "type": "integer" + }, + { + "description": "Maximum number of items to return", + "in": "query", + "name": "limit", + "required": false, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/DNSRecord" + }, + "type": "array" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`record` does not fulfill the schema
`domain` is not a valid Domain name", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "recordGet", + "summary": "Retrieve DNS Records for the specified Domain, optionally with the specified Type and/or Name" + }, + "put": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID which owns the domain. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose DNS Records are to be replaced", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Type for which DNS Records are to be replaced", + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "SOA", + "SRV", + "TXT" + ], + "in": "path", + "name": "type", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Name for which DNS Records are to be replaced", + "in": "path", + "name": "name", + "required": true, + "type": "string" + }, + { + "description": "DNS Records to replace whatever currently exists", + "in": "body", + "name": "records", + "required": true, + "schema": { + "items": { + "$ref": "#/definitions/DNSRecordCreateTypeName" + }, + "type": "array" + } + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`record` does not fulfill the schema", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "recordReplaceTypeName", + "summary": "Replace all DNS Records for the specified Domain with the specified Type and Name" + }, + "delete": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID which owns the domain. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose DNS Records are to be deleted", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Type for which DNS Records are to be deleted", + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "SRV", + "TXT" + ], + "in": "path", + "name": "type", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Name for which DNS Records are to be deleted", + "in": "path", + "name": "name", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Domain not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The given domain is not eligible to have its records changed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` is not a valid Domain name", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "recordDeleteTypeName", + "summary": "Delete all DNS Records for the specified Domain with the specified Type and Name" + } + }, + "/v1/domains/{domain}/records/{type}": { + "put": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper ID which owns the domain. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose DNS Records are to be replaced", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "DNS Record Type for which DNS Records are to be replaced", + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "SOA", + "SRV", + "TXT" + ], + "in": "path", + "name": "type", + "required": true, + "type": "string" + }, + { + "description": "DNS Records to replace whatever currently exists", + "in": "body", + "name": "records", + "required": true, + "schema": { + "items": { + "$ref": "#/definitions/DNSRecordCreateType" + }, + "type": "array" + } + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`record` does not fulfill the schema", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "recordReplaceType", + "summary": "Replace all DNS Records for the specified Domain with the specified Type" + } + }, + "/v1/domains/{domain}/renew": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper for whom Domain is to be renewed. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain to renew", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "Options for renewing existing Domain", + "in": "body", + "name": "body", + "required": false, + "schema": { + "$ref": "#/definitions/DomainRenew" + } + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainPurchaseResponse" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "End-user must read and consent to all of the following legal agreements
`domain` must match `sld.tld`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "renew", + "summary": "Renew the specified Domain" + } + }, + "/v1/domains/{domain}/transfer": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "The Shopper to whom the domain should be transfered", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain to transfer in", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + }, + { + "description": "Details for domain transfer purchase", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainTransferIn" + } + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainPurchaseResponse" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "`domain` (domain) isn't available for transfer", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Based on restrictions declared in JSON schema returned by `./schema/{tld}`
Cannot convert domain label error
Domain is missing IDN script
Domain segment ends with dash
Domain starts with dash
Domain uses unsupported IDN script
End-user must read and consent to all of the following legal agreements
FQDN fails generic validity regex
Invalid character(s) error
Invalid period range
Invalid tld error
Non-IDN domain name must not have dashes at the third and fourth position
Reserved name error
`authCode` cannot be empty
`domain` must match `sld.tld`
domain must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "transferIn", + "summary": "Purchase and start or restart transfer process" + } + }, + "/v1/domains/{domain}/verifyRegistrantEmail": { + "post": { + "tags": [ + "v1" + ], + "consumes": [ + "application/json", + "application/xml", + "text/xml" + ], + "produces": [ + "application/json", + "application/javascript", + "application/xml", + "text/javascript", + "text/xml" + ], + "parameters": [ + { + "description": "Shopper for whom domain contact e-mail should be verified. NOTE: This is only required if you are a Reseller managing a domain purchased outside the scope of your reseller account. For instance, if you're a Reseller, but purchased a Domain via http://www.godaddy.com", + "in": "header", + "name": "X-Shopper-Id", + "required": false, + "type": "string" + }, + { + "description": "Domain whose Contact E-mail should be verified.", + "in": "path", + "name": "domain", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` is not a valid Domain name", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "504": { + "description": "Gateway timeout", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "verifyEmail", + "summary": "Re-send Contact E-mail Verification for specified Domain" + } + }, + "/v2/customers/{customerId}/domains/{domain}": { + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Retrieve details for the specified Domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain name whose details are to be retrieved", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "name": "includes", + "required": false, + "in": "query", + "type": "array", + "items": { + "enum": [ + "actions", + "contacts", + "dnssecRecords", + "registryStatusCodes" + ], + "type": "string" + }, + "description": "Optional details to be included in the response" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainDetailV2" + } + }, + "203": { + "description": "Request was partially successful, but actions, contacts, and/or verifications may not be included.", + "schema": { + "$ref": "#/definitions/DomainDetailV2" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The contact does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/changeOfRegistrant": { + "delete": { + "tags": [ + "Domains" + ], + "summary": "Cancels a pending change of registrant request for a given domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose change of registrant is to be cancelled", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/CHANGE_OF_REGISTRANT_DELETE to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The contact does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "502": { + "description": "Dependent service unavailable", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Retrieve change of registrant information", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose change of registrant information is to be retrieved", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainChangeOfRegistrant" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The contact does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "502": { + "description": "Dependent service unavailable", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/dnssecRecords": { + "patch": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add the specifed DNSSEC records to the domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to add the DNSSEC record for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "DNSSEC records to add", + "name": "body", + "in": "body", + "required": true, + "schema": { + "items": { + "$ref": "#/definitions/DomainDnssec" + }, + "type": "array" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/DNSSEC_CREATE to poll status" + }, + "400": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Remove the specifed DNSSEC record from the domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to delete the DNSSEC record for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "DNSSEC records to remove", + "name": "body", + "in": "body", + "required": true, + "schema": { + "items": { + "$ref": "#/definitions/DomainDnssec" + }, + "type": "array" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/DNSSEC_DELETE to poll status" + }, + "400": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/nameServers": { + "put": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Replaces the existing name servers on the domain.", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose name servers are to be replaced", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Name server records to replace on the domain", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainNameServerUpdateV2" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/DOMAIN_UPDATE_NAME_SERVERS to poll status" + }, + "400": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/privacy/forwarding": { + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Retrieve privacy email forwarding settings showing where emails are delivered", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain name whose details are to be retrieved", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainPrivacyForwarding" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`domain` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "patch": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Update privacy email forwarding settings to determine how emails are delivered", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain name whose details are to be retrieved", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Update privacy email forwarding settings", + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DomainPrivacyForwardingUpdate" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/PRIVACY_FORWARDING_UPDATE to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/redeem": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Purchase a restore for the given domain to bring it out of redemption", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to request redeem for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Options for redeeming existing Domain", + "name" : "body", + "in" : "body", + "required" : false, + "schema": { + "$ref": "#/definitions/DomainRedeemV2" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/REDEEM to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Domain invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/renew": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Renew the specified Domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to be renewed", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Options for renewing existing Domain", + "name" : "body", + "in" : "body", + "required" : true, + "schema": { + "$ref": "#/definitions/DomainRenewV2" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/RENEW to poll status" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transfer": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Purchase and start or restart transfer process", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to transfer in", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Details for domain transfer purchase", + "name" : "body", + "in" : "body", + "required" : true, + "schema": { + "$ref": "#/definitions/DomainTransferInV2" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER to poll status" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Based on restrictions declared in JSON schema returned by `./schema/{tld}`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Query the current transfer status", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain Name", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainTransferStatus" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transfer/validate": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Validate the request body using the Domain Transfer Schema for the specified TLD", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to transfer in", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Details for domain transfer purchase", + "name" : "body", + "in" : "body", + "required" : true, + "schema": { + "$ref": "#/definitions/DomainTransferInV2" + } + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Based on restrictions declared in JSON schema returned by `./schema/{tld}`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferInAccept":{ + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Accepts the transfer in", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "name": "customerId", + "required": true, + "in": "path", + "type": "string", + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id." + }, + { + "name" : "domain", + "required" : true, + "in" : "path", + "type" : "string", + "description" : "Domain to accept the transfer in for" + }, + { + "name" : "body", + "in" : "body", + "description" : "An Authorization code for transferring the Domain", + "required" : true, + "schema" : { + "$ref" : "#/definitions/DomainTransferAuthCode" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_IN_ACCEPT to poll status" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferInCancel": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Cancels the transfer in", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to cancel the transfer in for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_IN_CANCEL to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferInRestart": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Restarts transfer in request from the beginning", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "name": "customerId", + "required": true, + "in": "path", + "type": "string", + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id." + }, + { + "name" : "domain", + "required" : true, + "in" : "path", + "type" : "string", + "description" : "Domain to restart the transfer in" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_IN_RESTART to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferInRetry": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Retries the current transfer in request with supplied Authorization code", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "name": "customerId", + "required": true, + "in": "path", + "type": "string", + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id." + }, + { + "name" : "domain", + "required" : true, + "in" : "path", + "type" : "string", + "description" : "Domain to retry the transfer in" + }, + { + "name" : "body", + "in" : "body", + "description" : "An Authorization code for transferring the Domain", + "required" : true, + "schema" : { + "$ref" : "#/definitions/DomainTransferAuthCode" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_IN_RETRY to poll status" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferOut": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Initiate transfer out to another registrar for a .uk domain.", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to initiate the transfer out for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Registrar tag to push transfer to", + "name" : "registrar", + "in" : "query", + "required" : true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_OUT_REQUESTED to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Domain invalid. TLD must be .uk", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferOutAccept": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Accept transfer out", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to accept the transfer out for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_OUT_ACCEPT to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/transferOutReject": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Reject transfer out", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain to reject the transfer out for", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "name": "reason", + "description": "Transfer out reject reason", + "in": "query", + "required": false, + "enum": [ + "EVIDENCE_OF_FRAUD", + "URDP_ACTION", + "COURT_ORDER", + "DISPUTE_OVER_IDENTITY", + "NO_PAYMENT_FOR_PREVIOUS_REGISTRATION_PERIOD", + "WRITTEN_OBJECTION", + "TRANSFERRED_WITHIN_SIXTY_DAYS" + ], + "type": "string" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/TRANSFER_OUT_REJECT to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/forwards/{fqdn}": { + "delete": { + "description": "Notes:
  • **shopperId** is **not the same** as **customerId**. **shopperId** is a number of max length 10 digits (*ex:* 1234567890) whereas **customerId** is a UUIDv4 (*ex:* 295e3bc3-b3b9-4d95-aae5-ede41a994d13)
", + "tags": [ + "Domains" + ], + "consumes": [ + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description": "The fully qualified domain name whose forwarding details are to be deleted.", + "in": "path", + "name": "fqdn", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "A valid `fqdn` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "domainsForwardsDelete", + "summary": "Submit a forwarding cancellation request for the given fqdn" + }, + "get": { + "description": "Notes:
  • **shopperId** is **not the same** as **customerId**. **shopperId** is a number of max length 10 digits (*ex:* 1234567890) whereas **customerId** is a UUIDv4 (*ex:* 295e3bc3-b3b9-4d95-aae5-ede41a994d13)
", + "tags": [ + "Domains" + ], + "consumes": [ + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description": "The fully qualified domain name whose forwarding details are to be retrieved.", + "in": "path", + "name": "fqdn", + "required": true, + "type": "string" + }, + { + "description": "Optionally include all sub domains if the fqdn specified is a domain and not a sub domain.", + "name": "includeSubs", + "required": false, + "in": "query", + "type": "boolean" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/DomainForwarding" + }, + "type": "array" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "A valid `fqdn` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "domainsForwardsGet", + "summary": "Retrieve the forwarding information for the given fqdn" + }, + "put": { + "description": "Notes:
  • **shopperId** is **not the same** as **customerId**. **shopperId** is a number of max length 10 digits (*ex:* 1234567890) whereas **customerId** is a UUIDv4 (*ex:* 295e3bc3-b3b9-4d95-aae5-ede41a994d13)
", + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description": "The fully qualified domain name whose forwarding details are to be modified.", + "in": "path", + "name": "fqdn", + "required": true, + "type": "string" + }, + { + "description": "Domain forwarding rule to create or replace on the fqdn", + "name": "body", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/DomainForwardingCreate" + } + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "domainsForwardsPut", + "summary": "Modify the forwarding information for the given fqdn" + }, + "post": { + "description": "Notes:
  • **shopperId** is **not the same** as **customerId**. **shopperId** is a number of max length 10 digits (*ex:* 1234567890) whereas **customerId** is a UUIDv4 (*ex:* 295e3bc3-b3b9-4d95-aae5-ede41a994d13)
", + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "parameters": [ + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your own customer id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description": "The fully qualified domain name whose forwarding details are to be modified.", + "in": "path", + "name": "fqdn", + "required": true, + "type": "string" + }, + { + "description": "Domain forwarding rule to create for the specified fqdn", + "name": "body", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/DomainForwardingCreate" + } + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "Resource not found", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "Provided `fqdn` already has forwarding setup", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + }, + "operationId": "domainsForwardsPost", + "summary": "Create a new forwarding configuration for the given FQDN" + } + }, + "/v2/customers/{customerId}/domains/{domain}/actions": { + "get": { + "tags": [ + "Actions" + ], + "produces": [ "application/json" ], + "summary": "Retrieves a list of the most recent actions for the specified domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose actions are to be retrieved", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "items": { + "$ref": "#/definitions/Action" + }, + "type": "array" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/actions/{type}": { + "delete": { + "tags": [ + "Actions" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Cancel the most recent user action for the specified domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose action is to be cancelled", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "name" : "type", + "description": "The type of action to cancel", + "in" : "path", + "required" : true, + "type" : "string", + "enum": [ + "AUTH_CODE_PURCHASE", + "AUTH_CODE_REGENERATE", + "BACKORDER_PURCHASE", + "BACKORDER_DELETE", + "BACKORDER_UPDATE", + "CHANGE_OF_REGISTRANT_DELETE", + "DNSSEC_CREATE", + "DNSSEC_DELETE", + "DOMAIN_DELETE", + "DOMAIN_UPDATE", + "DOMAIN_UPDATE_CONTACTS", + "DOMAIN_UPDATE_NAME_SERVERS", + "MIGRATE", + "PRIVACY_FORWARDING_UPDATE", + "PRIVACY_PURCHASE", + "PRIVACY_DELETE", + "REDEEM", + "REGISTER", + "RENEW", + "RENEW_UNDO", + "TRADE", + "TRADE_CANCEL", + "TRADE_PURCHASE", + "TRADE_PURCHASE_AUTH_TEXT_MESSAGE", + "TRADE_RESEND_AUTH_EMAIL", + "TRANSFER", + "TRANSFER_IN_ACCEPT", + "TRANSFER_IN_CANCEL", + "TRANSFER_IN_RESTART", + "TRANSFER_IN_RETRY", + "TRANSFER_OUT_ACCEPT", + "TRANSFER_OUT_REJECT", + "TRANSFER_OUT_REQUESTED", + "TRANSIT" + ] + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The action status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "get": { + "tags": [ + "Actions" + ], + "produces": [ "application/json" ], + "summary": "Retrieves the most recent action for the specified domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose action is to be retrieved", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "name" : "type", + "description": "The type of action to retrieve", + "in" : "path", + "required" : true, + "type" : "string", + "enum": [ + "AUTH_CODE_PURCHASE", + "AUTH_CODE_REGENERATE", + "AUTO_RENEWAL", + "BACKORDER_PURCHASE", + "BACKORDER_DELETE", + "BACKORDER_UPDATE", + "CHANGE_OF_REGISTRANT_DELETE", + "DNS_VERIFICATION", + "DNSSEC_CREATE", + "DNSSEC_DELETE", + "DOMAIN_DELETE", + "DOMAIN_UPDATE", + "DOMAIN_UPDATE_CONTACTS", + "DOMAIN_UPDATE_NAME_SERVERS", + "EXPIRY", + "ICANN_VERIFICATION", + "MIGRATE", + "MIGRATE_IN", + "PREMIUM", + "PRIVACY_FORWARDING_UPDATE", + "PRIVACY_PURCHASE", + "PRIVACY_DELETE", + "REDEEM", + "REGISTER", + "RENEW", + "RENEW_UNDO", + "TRADE", + "TRADE_CANCEL", + "TRADE_PURCHASE", + "TRADE_PURCHASE_AUTH_TEXT_MESSAGE", + "TRADE_RESEND_AUTH_EMAIL", + "TRANSFER", + "TRANSFER_IN", + "TRANSFER_IN_ACCEPT", + "TRANSFER_IN_CANCEL", + "TRANSFER_IN_RESTART", + "TRANSFER_IN_RETRY", + "TRANSFER_OUT", + "TRANSFER_OUT_ACCEPT", + "TRANSFER_OUT_REJECT", + "TRANSFER_OUT_REQUESTED", + "TRANSIT" + ] + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/Action" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "The domain status does not allow performing the operation", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/notifications": { + "get": { + "tags": [ + "Notifications" + ], + "produces": [ "application/json" ], + "summary": "Retrieve the next domain notification", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/DomainNotification" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The customer does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/notifications/optIn": { + "get": { + "tags": [ + "Notifications" + ], + "produces": [ "application/json" ], + "summary": "Retrieve a list of notification types that are opted in", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/DomainNotification" + } + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The customer does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "put": { + "tags": [ + "Notifications" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Opt in to recieve notifications for the submitted notification types", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "name": "types", + "description": "The notification types that should be opted in", + "in": "query", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "AUTH_CODE_PURCHASE", + "AUTH_CODE_REGENERATE", + "AUTO_RENEWAL", + "BACKORDER", + "BACKORDER_PURCHASE", + "BACKORDER_DELETE", + "BACKORDER_UPDATE", + "CHANGE_OF_REGISTRANT_DELETE", + "CONTACT_CREATE", + "CONTACT_DELETE", + "CONTACT_UPDATE", + "DNS_VERIFICATION", + "DNSSEC_CREATE", + "DNSSEC_DELETE", + "DOMAIN_DELETE", + "DOMAIN_UPDATE", + "DOMAIN_UPDATE_CONTACTS", + "DOMAIN_UPDATE_NAME_SERVERS", + "EXPIRY", + "HOST_CREATE", + "HOST_DELETE", + "ICANN_VERIFICATION", + "MIGRATE", + "MIGRATE_IN", + "PREMIUM", + "PRIVACY_FORWARDING_UPDATE", + "PRIVACY_PURCHASE", + "PRIVACY_DELETE", + "REDEEM", + "REGISTER", + "RENEW", + "RENEW_UNDO", + "TRADE", + "TRADE_CANCEL", + "TRADE_PURCHASE", + "TRADE_PURCHASE_AUTH_TEXT_MESSAGE", + "TRADE_RESEND_AUTH_EMAIL", + "TRANSFER", + "TRANSFER_IN", + "TRANSFER_IN_ACCEPT", + "TRANSFER_IN_CANCEL", + "TRANSFER_IN_RESTART", + "TRANSFER_IN_RETRY", + "TRANSFER_OUT", + "TRANSFER_OUT_ACCEPT", + "TRANSFER_OUT_REJECT", + "TRANSFER_OUT_REQUESTED", + "TRANSIT" + ] + } + } + ], + "responses": { + "204": { + "description": "Command successful" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The customer does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`type` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/notifications/schemas/{type}": { + "get": { + "tags": [ + "Notifications" + ], + "produces": [ "application/json" ], + "summary": "Retrieve the schema for the notification data for the specified notification type", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "The notification type whose schema should be retrieved", + "name" : "type", + "in" : "path", + "required" : true, + "type" : "string", + "enum": [ + "AUTO_RENEWAL", + "BACKORDER", + "BACKORDER_PURCHASE", + "EXPIRY", + "PREMIUM", + "PRIVACY_PURCHASE", + "REDEEM", + "REGISTER", + "RENEW", + "TRADE", + "TRANSFER" + ] + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/JsonSchema" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The schema type does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`type` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/notifications/{notificationId}/acknowledge": { + "post": { + "tags": [ + "Notifications" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Acknowledge a domain notification", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "name": "notificationId", + "description": "The notification ID to acknowledge", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": "Message acknowledged" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/register": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Purchase and register the specified Domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "name": "body", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/DomainPurchaseV2" + }, + "description": "An instance document expected to match the JSON schema returned by `./schema/{tld}`" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/REGISTER to poll status" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Based on restrictions declared in JSON schema returned by `./schema/{tld}`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/register/schema/{tld}": { + "get": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Retrieve the schema to be submitted when registering a Domain for the specified TLD", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "name": "tld", + "required": true, + "in": "path", + "type": "string", + "description": "The Top-Level Domain whose schema should be retrieved" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/JsonSchema" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The tld does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "`tld` must be specified", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/register/validate": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Validate the request body using the Domain Registration Schema for the specified TLD", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "name": "body", + "required": true, + "in": "body", + "schema": { + "$ref": "#/definitions/DomainPurchaseV2" + }, + "description": "An instance document expected to match the JSON schema returned by `./schema/{tld}`" + } + ], + "responses": { + "204": { + "description": "Request was successful" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The customer does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Based on restrictions declared in JSON schema returned by `./schema/{tld}`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/domains/maintenances": { + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Retrieve a list of upcoming system Maintenances", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "name" : "status", + "in" : "query", + "required" : false, + "description": "Only include results with the selected `status` value. Returns all results if omitted
  • ACTIVE - The upcoming maintenance is active.
  • CANCELLED - The upcoming maintenance has been cancelled.
", + "type": "string", + "enum": [ + "ACTIVE", + "CANCELLED" + ] + }, + { + "name": "modifiedAtAfter", + "required": false, + "in": "query", + "type": "string", + "format": "iso-datetime", + "description": "Only include results with `modifiedAt` after the supplied date" + }, + { + "name": "startsAtAfter", + "required": false, + "in": "query", + "type": "string", + "format": "iso-datetime", + "description": "Only include results with `startsAt` after the supplied date" + }, + { + "name": "limit", + "required": false, + "in": "query", + "type": "integer", + "default": 100, + "minimum": 1, + "maximum": 100, + "description": "Maximum number of results to return" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/Maintenance" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Filter parameters don't match schema and/or restrictions", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/domains/maintenances/{maintenanceId}": { + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Retrieve the details for an upcoming system Maintenances", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "name" : "maintenanceId", + "in" : "path", + "required" : true, + "description": "The identifier for the system maintenance", + "type": "string" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/MaintenanceDetail" + } + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The maintenance does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/domains/usage/{yyyymm}": { + "get": { + "tags": [ + "Domains" + ], + "produces": [ "application/json" ], + "summary": "Retrieve api usage request counts for a specific year/month. The data is retained for a period of three months.", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "name": "yyyymm", + "description": "The year/month timeframe for the request counts (in the format yyyy-mm)", + "in": "path", + "required": true, + "type": "string", + "pattern": "^\\d{4}-\\d{2}$" + }, + { + "name": "includes", + "required": false, + "in": "query", + "type": "array", + "items": { + "enum": [ + "details" + ], + "type": "string" + }, + "description": "Determines if the detail records (grouped by request path) are included in the response" + } + ], + "responses": { + "200": { + "description": "Request was successful", + "schema": { + "$ref": "#/definitions/UsageMonthly" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/contacts": { + "patch": { + "tags": [ + "Contacts" + ], + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "summary": "Update domain contacts", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description" : "Domain whose Contacts are to be updated.", + "name" : "domain", + "in" : "path", + "required" : true, + "type" : "string" + }, + { + "description": "Changes to apply to existing Contacts", + "name" : "body", + "in" : "body", + "required" : true, + "schema": { + "$ref": "#/definitions/DomainContactsUpdateV2" + } + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/DOMAIN_UPDATE_CONTACTS to poll status" + }, + "400": { + "description": "Request was malformed", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "422": { + "description": "Request body doesn't fulfill schema, see details in `fields`", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/v2/customers/{customerId}/domains/{domain}/regenerateAuthCode": { + "post": { + "tags": [ + "Domains" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Regenerate the auth code for the given domain", + "parameters": [ + { + "name": "X-Request-Id", + "required": false, + "in": "header", + "type": "string", + "description": "A client provided identifier for tracking this request." + }, + { + "description": "The Customer identifier
Note: For API Resellers, performing actions on behalf of your customers, you need to specify the Subaccount you're operating on behalf of; otherwise use your shopper id.", + "in": "path", + "name": "customerId", + "required": true, + "type": "string" + }, + { + "description": "Domain to update authcode for", + "name": "domain", + "in": "path", + "required": true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "Request Accepted. You may use GET /v2/customers/{customerId}/domains/{domain}/actions/AUTH_CODE_REGENERATE to poll status" + }, + "401": { + "description": "Authentication info not sent or invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "403": { + "description": "Authenticated user is not allowed access", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "404": { + "description": "The domain does not exist", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "409": { + "description": "There is already a similar action processing", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "429": { + "description": "Too many requests received within interval", + "schema": { + "$ref": "#/definitions/ErrorLimit" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + } + }, + "definitions": { + "Action": { + "properties": { + "type" : { + "type": "string", + "description": "The type of action being performed
  • AUTH_CODE_PURCHASE - Request for an auth code for a .de domain via POST /v2/customers/{customerId}/domains/{domain}/purchaseAuthCode.
  • AUTH_CODE_REGENERATE - Request to regenerate the authCode for a domain via POST /v2/customers/{customerId}/domains/{domain}/regenerateAuthCode
  • AUTO_RENEWAL - A Domain Auto Renew is in progress.
  • BACKORDER_PURCHASE - Request to purchase a domain backorder via POST /v2/customers/{customerId}/domains/backorders/purchase.
  • BACKORDER_DELETE - Request to cancel the current domain backorder via DELETE /v2/customers/{customerId}/domains/backorders/{domain}.
  • BACKORDER_UPDATE - Request update the current domain backorder via PATCH /v2/customers/{customerId}/domains/backorders/{domain}.
  • CHANGE_OF_REGISTRANT_DELETE - Request to delete a change of registrant request via DELETE /v2/customers/{customerId}/domains/{domain}/changeOfRegistrant.
  • CONTACT_CREATE - Request to create a contact via POST /v2/customers/{customerId}/domains/contacts.
  • CONTACT_DELETE - Request to delete a contact via DELETE /v2/customers/{customerId}/domains/contacts/{contactId}
  • CONTACT_UPDATE - Request to update a contact via PATCH /v2/customers/{customerId}/domains/contacts/{contactId}
  • DNS_VERIFICATION - Domain requires zone file setup.
  • DNSSEC_CREATE - Request to create DNSSEC record for the domain via PATCH /v2/customers/{customerId}/domains/{domain}/dnssecRecords.
  • DNSSEC_DELETE - Request to delete DNSSEC record for the domain via DELETE /v2/customers/{customerId}/domains/{domain}/dnssecRecords.
  • DOMAIN_DELETE - Request to delete the domain via DELETE /v2/customers/{customerId}/domains/{domain}
  • DOMAIN_UPDATE - Request to update the domain via PATCH /v2/customers/{customerId}/domains/{domain}
  • DOMAIN_UPDATE_CONTACTS -Request to update the domain contacts via PATCH /v2/customers/{customerId}/domains/{domain}/contacts
  • DOMAIN_UPDATE_NAME_SERVERS - Request to update the domain name servers via PUT /v2/customers/{customerId}/domains/{domain}/nameServers
  • EXPIRY - A Domain Expiration is in progress.
  • HOST_CREATE - Request to create a hostname via PUT /v2/customers/{customerId}/domains/{domain}/hosts/{hostname}
  • HOST_DELETE - Request to delete a hostname via DELETE /v2/customers/{customerId}/domains/{domain}/hosts/{hostname}
  • ICANN_VERIFICATION - Domain requires registrant verification for Icann.
  • PREMIUM - Premium Domain domain sale is in progress.
  • PRIVACY_FORWARDING_UPDATE - Request to update privacy forwarding information via PATCH /v2/customers/{customerId}/domains/{domain}/privacy/forwarding.
  • PRIVACY_DELETE - Request to remove privacy from a domain via DELETE /v2/customers/{customerId}/domains/{domain}/privacy
  • REDEEM - Request to redeem a domain via POST /v2/customers/{customerId}/domains/{domain}/redeem
  • REGISTER - Request to register a domain via POST /v2/customers/{customerId}/domains/{domain}/register
  • RENEW - Request to renew a domain via POST /v2/customers/{customerId}/domains/{domain}/renew
  • RENEW_UNDO - Request to undo a renewal for a uk domain via POST /v2/customers/{customerId}/domains/{domain}/undoRenew
  • TRADE - A domain trade request is in progress
  • TRADE_CANCEL - Request to cancel a trade for a domain via POST /v2/customers/{customerId}/domains/{domain}/tradeCancel
  • TRADE_PURCHASE - Request to purchase a trade for a domain via POST /v2/customers/{customerId}/domains/{domain}/tradePurchase
  • TRADE_PURCHASE_AUTH_TEXT_MESSAGE - Request for a trade purchase text message for a domain via POST /v2/customers/{customerId}/domains/{domain}/tradePurchaseAuthorizationTextMessage
  • TRADE_RESEND_AUTH_EMAIL - Request to resend the trade auth email message for a domain via POST /v2/customers/{customerId}/domains/{domain}/tradeResendAuthorizationEmail
  • TRANSFER - Request to transfer a domain via POST /v2/customers/{customerId}/domains/{domain}/transfer
  • TRANSFER_IN - A domain transfer in request is in progress.
  • TRANSFER_IN_ACCEPT - Request to accept a domain transfer in via POST /v2/customers/{customerId}/domains/{domain}/transferInAccept
  • TRANSFER_IN_CANCEL - Request to cancel a domain transfer via POST /v2/customers/{customerId}/domains/{domain}/transferInCancel
  • TRANSFER_IN_RESTART - Request to restart a domain transfer in via POST /v2/customers/{customerId}/domains/{domain}/transferInRestart
  • TRANSFER_IN_RETRY - Request to retry a domain transfer in via POST /v2/customers/{customerId}/domains/{domain}/transferInRetry
  • TRANSFER_OUT - A domain transfer out request is in progress.
  • TRANSFER_OUT_ACCEPT - Request to accept a transfer out request for a domain via POST /v2/customers/{customerId}/domains/{domain}/transferOutAccept
  • TRANSFER_OUT_REJECT - Request to reject a transfer out request for a domain via POST /v2/customers/{customerId}/domains/{domain}/transferOutReject
  • TRANSFER_OUT_REQUESTED - Request to transfer out for a domain (.de) via POST /v2/customers/{customerId}/domains/{domain}/transferOut
  • TRANSIT - Request to transit a de or at domain at the registry via POST /v2/customers/{customerId}/domains/{domain}/transit
", + "enum": [ + "AUTH_CODE_PURCHASE", + "AUTH_CODE_REGENERATE", + "AUTO_RENEWAL", + "BACKORDER_PURCHASE", + "BACKORDER_DELETE", + "BACKORDER_UPDATE", + "CHANGE_OF_REGISTRANT_DELETE", + "CONTACT_CREATE", + "CONTACT_DELETE", + "CONTACT_UPDATE", + "DNS_VERIFICATION", + "DNSSEC_CREATE", + "DNSSEC_DELETE", + "DOMAIN_DELETE", + "DOMAIN_UPDATE", + "DOMAIN_UPDATE_CONTACTS", + "DOMAIN_UPDATE_NAME_SERVERS", + "EXPIRY", + "HOST_CREATE", + "HOST_DELETE", + "ICANN_VERIFICATION", + "MIGRATE", + "MIGRATE_IN", + "PREMIUM", + "PRIVACY_FORWARDING_UPDATE", + "PRIVACY_PURCHASE", + "PRIVACY_DELETE", + "REDEEM", + "REGISTER", + "RENEW", + "RENEW_UNDO", + "TRADE", + "TRADE_CANCEL", + "TRADE_PURCHASE", + "TRADE_PURCHASE_AUTH_TEXT_MESSAGE", + "TRADE_RESEND_AUTH_EMAIL", + "TRANSFER", + "TRANSFER_IN", + "TRANSFER_IN_ACCEPT", + "TRANSFER_IN_CANCEL", + "TRANSFER_IN_RESTART", + "TRANSFER_IN_RETRY", + "TRANSFER_OUT", + "TRANSFER_OUT_ACCEPT", + "TRANSFER_OUT_REJECT", + "TRANSFER_OUT_REQUESTED", + "TRANSIT" + ] + }, + "origination" : { + "type": "string", + "enum": [ + "USER", + "SYSTEM" + ], + "description": "The origination of the action
  • USER - These are user requests.
  • SYSTEM - These are system processing actions.
" + }, + "createdAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the action was created" + }, + "startedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the action was started" + }, + "completedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the action was completed" + }, + "modifiedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the action was last modified" + }, + "status": { + "type": "string", + "default": "ACCEPTED", + "enum": [ + "ACCEPTED", + "AWAITING", + "CANCELLED", + "FAILED", + "PENDING", + "SUCCESS" + ], + "description": "The current status of the action
  • ACCEPTED - The action has been queued, processing has not started.
  • AWAITING - The action is waiting on a user input.
  • CANCELLED - The action has been cancelled by the user.
  • FAILED - An error occurred while the action was processing, no more processing will be performed.
  • PENDING - The action is being processed.
  • SUCCESS - The action has completed, no additional processing is required.
" + }, + "reason": { + "$ref": "#/definitions/ActionReason", + "description": "The detailed reason for the status" + }, + "requestId" : { + "type": "string", + "description": "A client provided identifier (via X-Request-Id header) used for tracking individual requests" + } + }, + "required": [ + "type", + "origination", + "createdAt", + "status" + ] + }, + "ActionReason": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "format": "constant", + "pattern": "^[A-Z_][A-Z0-9_]*$", + "description": "Short identifier, suitable for indicating the reason for the current status and how to handle within client code" + }, + "message": { + "type": "string", + "description": "Human-readable, English description of the code" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/ErrorField" + }, + "description": "List of the specific fields, and the errors found with their contents" + } + }, + "required": [ + "code" + ] + }, + "Address": { + "properties": { + "address1": { + "format": "street-address", + "type": "string" + }, + "address2": { + "format": "street-address2", + "type": "string" + }, + "city": { + "format": "city-name", + "type": "string" + }, + "country": { + "default": "US", + "description": "Two-letter ISO country code to be used as a hint for target region

\nNOTE: These are sample values, there are many\nmore", + "enum": [ + "AC", + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KR", + "KV", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "ST", + "SV", + "SX", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TP", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW" + ], + "format": "iso-country-code", + "type": "string" + }, + "postalCode": { + "description": "Postal or zip code", + "format": "postal-code", + "type": "string" + }, + "state": { + "description": "State or province or territory", + "format": "state-province-territory", + "type": "string" + } + }, + "required": [ + "address1", + "city", + "state", + "postalCode", + "country" + ] + }, + "ArrayOfDNSRecord": { + "type": "array", + "items": { + "$ref": "#/definitions/DNSRecord" + } + }, + "Consent": { + "properties": { + "agreedAt": { + "description": "Timestamp indicating when the end-user consented to these legal agreements", + "format": "iso-datetime", + "type": "string" + }, + "agreedBy": { + "description": "Originating client IP address of the end-user's computer when they consented to these legal agreements", + "type": "string" + }, + "agreementKeys": { + "description": "Unique identifiers of the legal agreements to which the end-user has agreed, as returned from the/domains/agreements endpoint", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "agreementKeys", + "agreedBy", + "agreedAt" + ] + }, + "ConsentRedemption": { + "additionalProperties": false, + "properties": { + "price": { + "type": "integer", + "format": "currency-micro-unit", + "description": "Price for the domain renewal (if domain renewal required for redemption). Please use GET /v2/customers/{customerId}/domains/{domain} to retrieve the redemption price and currency for the domain" + }, + "fee": { + "type": "integer", + "format": "currency-micro-unit", + "description": "Fee charged for the domain redemption. Please use GET /v2/customers/{customerId}/domains/{domain} to retrieve the redemption fee and currency for the domain" + }, + "currency": { + "type": "string", + "format": "iso-currency-code", + "default": "USD", + "pattern": "^[A-Z][A-Z][A-Z]$", + "description": "Currency in which the `price` and `fee` are listed" + }, + "agreedBy": { + "type": "string", + "description": "Originating client IP address of the end-user's computer when they consented to these legal agreements" + }, + "agreedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the end-user consented to these legal agreements" + } + }, + "required": [ + "price", + "fee", + "currency", + "agreedBy", + "agreedAt" + ] + }, + "ConsentDomainUpdate": { + "properties": { + "agreedAt": { + "description": "Timestamp indicating when the end-user consented to these agreements", + "format": "iso-datetime", + "type": "string" + }, + "agreedBy": { + "description": "Originating client IP address of the end-user's computer when they consented to the agreements", + "type": "string" + }, + "agreementKeys": { + "description": "Unique identifiers of the agreements to which the end-user has agreed, as required by the elements being updated
  • EXPOSE_REGISTRANT_ORGANIZATION - Required when the exposeRegistrantOrganization field is updated to true
  • EXPOSE_WHOIS - Required when the exposeWhois field is updated to true
", + "items": { + "enum": [ + "EXPOSE_REGISTRANT_ORGANIZATION", + "EXPOSE_WHOIS" + ], + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "agreementKeys", + "agreedBy", + "agreedAt" + ] + }, + "ConsentRenew": { + "additionalProperties": false, + "properties": { + "price": { + "type": "integer", + "format": "currency-micro-unit", + "description": "Price of the domain excluding taxes or fees. Please use GET /v2/customers/{customerId}/domains/{domain} to retrieve the renewal price and currency for the domain" + }, + "currency": { + "type": "string", + "format": "iso-currency-code", + "default": "USD", + "pattern": "^[A-Z][A-Z][A-Z]$", + "description": "Currency in which the `price` is listed" + }, + "registryPremiumPricing": { + "type": "boolean", + "description": "Only required for hosted registrar if domain is premium. If true indicates that the `price` and `currency` listed are the registry premium price and currency for the domain" + }, + "agreedBy": { + "type": "string", + "description": "Originating client IP address of the end-user's computer when they consented to these legal agreements" + }, + "agreedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the end-user consented to these legal agreements" + } + }, + "required": [ + "price", + "currency", + "agreedBy", + "agreedAt" + ] + }, + "ConsentV2": { + "additionalProperties": false, + "properties": { + "agreementKeys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Unique identifiers of the legal agreements to which the end-user has agreed, as returned from the/domains/agreements endpoint" + }, + "price": { + "type": "integer", + "format": "currency-micro-unit", + "description": "Price of the domain excluding taxes or fees. Please use GET /v1/domains/available to retrieve the price and currency for the domain" + }, + "currency": { + "type": "string", + "format": "iso-currency-code", + "default": "USD", + "pattern": "^[A-Z][A-Z][A-Z]$", + "description": "Currency in which the `price` is listed" + }, + "registryPremiumPricing": { + "type": "boolean", + "description": "Only required for hosted registrar if domain is premium. If true indicates that the `price` and `currency` listed are the registry premium price and currency for the domain" + }, + "agreedBy": { + "type": "string", + "description": "Originating client IP address of the end-user's computer when they consented to these legal agreements" + }, + "agreedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the end-user consented to these legal agreements" + }, + "claimToken": { + "description": "The trademark claim token, only needed if the domain has an active trademark claim", + "type": "string" + } + }, + "required": [ + "agreementKeys", + "price", + "currency", + "agreedBy", + "agreedAt" + ] + }, + "Contact": { + "properties": { + "addressMailing": { + "$ref": "#/definitions/Address" + }, + "email": { + "format": "email", + "type": "string" + }, + "fax": { + "format": "phone", + "type": "string" + }, + "jobTitle": { + "type": "string" + }, + "nameFirst": { + "format": "person-name", + "type": "string" + }, + "nameLast": { + "format": "person-name", + "type": "string" + }, + "nameMiddle": { + "type": "string" + }, + "organization": { + "format": "organization-name", + "type": "string" + }, + "phone": { + "format": "phone", + "type": "string" + } + }, + "required": [ + "nameFirst", + "nameLast", + "email", + "phone", + "addressMailing" + ] + }, + "ContactDomain": { + "properties" : { + "contactId" : { + "type" : "string", + "description": "Unique identifier for this Contact" + }, + "encoding" : { + "type": "string", + "default": "ASCII", + "enum": [ + "ASCII", + "UTF-8" + ], + "description": "The encoding of the contact data
  • ASCII - Data contains only ASCII characters that are not region or language specific
  • UTF-8 - Data contains characters that are specific to a region or language
" + }, + "nameFirst": { + "type": "string", + "format": "person-name", + "maxLength": 30 + }, + "nameMiddle": { + "type": "string" + }, + "nameLast": { + "type": "string", + "format": "person-name", + "maxLength": 30 + }, + "organization": { + "type": "string", + "format": "organization-name", + "maxLength": 100 + }, + "jobTitle": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 80 + }, + "phone": { + "type": "string", + "format": "phone", + "maxLength": 17 + }, + "fax": { + "type": "string", + "format": "phone", + "maxLength": 17 + }, + "addressMailing": { + "$ref": "#/definitions/Address" + }, + "exposeRegistrantOrganization": { + "type": "boolean", + "description": "Whether or not the domain registrant contact organization field should be shown in the WHOIS" + }, + "exposeWhois": { + "type": "boolean", + "description": "Whether or not the contact details should be shown in the WHOIS" + }, + "metadata": { + "type": "object", + "description": "The contact eligibility data fields as specified by GET /v2/customers/{customerId}/domains/contacts/schema/{tld}" + }, + "tlds" : { + "type" : "array", + "description": "The tlds that this contact can be assigned to", + "items" : { + "type": "string" + } + }, + "_createdAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the contact was created" + }, + "_modifiedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Timestamp indicating when the contact was last modified" + }, + "_deleted": { + "type": "boolean", + "description": "Flag indicating if the contact has been logically deleted in the system" + }, + "_revision": { + "type": "integer", + "description": "The current revision number of the contact." + } + }, + "required": [ + "nameFirst", + "nameLast", + "email", + "phone", + "addressMailing", + "exposeWhois", + "exposeRegistrantOrganization" + ] + }, + "ContactDomainCreate": { + "additionalProperties": false, + "properties": { + "encoding" : { + "type": "string", + "default": "ASCII", + "enum": [ + "ASCII", + "UTF-8" + ], + "description": "The encoding of the contact data
  • ASCII - Data contains only ASCII characters that are not region or language specific
  • UTF-8 - Data contains characters that are specific to a region or language
" + }, + "nameFirst": { + "type": "string", + "format": "person-name", + "maxLength": 30 + }, + "nameMiddle": { + "type": "string" + }, + "nameLast": { + "type": "string", + "format": "person-name", + "maxLength": 30 + }, + "organization": { + "type": "string", + "format": "organization-name", + "maxLength": 100 + }, + "jobTitle": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email", + "maxLength": 80 + }, + "phone": { + "type": "string", + "format": "phone", + "maxLength": 17 + }, + "fax": { + "type": "string", + "format": "phone", + "maxLength": 17 + }, + "addressMailing": { + "$ref": "#/definitions/Address" + }, + "metadata": { + "type": "object", + "description": "The contact eligibility data fields as specified by GET /v2/customers/{customerId}/domains/contacts/schema/{tld}" + } + }, + "required": [ + "encoding", + "nameFirst", + "nameLast", + "email", + "phone", + "addressMailing" + ] + }, + "ContactRegistrantChange": { + "properties": { + "email": { + "type": "string", + "format": "email", + "maxLength": 80 + }, + "firstName": { + "type": "string", + "format": "person-name", + "maxLength": 30 + }, + "lastName": { + "type": "string", + "format": "person-name", + "maxLength": 30 + }, + "organization": { + "type": "string", + "format": "organization-name", + "maxLength": 100 + } + }, + "required": [ + "email", + "firstName", + "lastName" + ] + }, + "DNSRecord": { + "properties": { + "data": { + "type": "string" + }, + "name": { + "format": "domain", + "type": "string" + }, + "port": { + "description": "Service port (SRV only)", + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "priority": { + "description": "Record priority (MX and SRV only)", + "format": "integer-positive", + "type": "integer" + }, + "protocol": { + "description": "Service protocol (SRV only)", + "type": "string" + }, + "service": { + "description": "Service type (SRV only)", + "type": "string" + }, + "ttl": { + "format": "integer-positive", + "type": "integer" + }, + "type": { + "enum": [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "SOA", + "SRV", + "TXT" + ], + "type": "string" + }, + "weight": { + "description": "Record weight (SRV only)", + "format": "integer-positive", + "type": "integer" + } + }, + "required": [ + "type", + "name", + "data" + ] + }, + "DNSRecordCreateType": { + "properties": { + "data": { + "type": "string" + }, + "name": { + "format": "domain", + "type": "string" + }, + "port": { + "description": "Service port (SRV only)", + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "priority": { + "description": "Record priority (MX and SRV only)", + "format": "integer-positive", + "type": "integer" + }, + "protocol": { + "description": "Service protocol (SRV only)", + "type": "string" + }, + "service": { + "description": "Service type (SRV only)", + "type": "string" + }, + "ttl": { + "format": "integer-positive", + "type": "integer" + }, + "weight": { + "description": "Record weight (SRV only)", + "format": "integer-positive", + "type": "integer" + } + }, + "required": [ + "name", + "data" + ] + }, + "DNSRecordCreateTypeName": { + "properties": { + "data": { + "type": "string" + }, + "port": { + "description": "Service port (SRV only)", + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "priority": { + "description": "Record priority (MX and SRV only)", + "format": "integer-positive", + "type": "integer" + }, + "protocol": { + "description": "Service protocol (SRV only)", + "type": "string" + }, + "service": { + "description": "Service type (SRV only)", + "type": "string" + }, + "ttl": { + "format": "integer-positive", + "type": "integer" + }, + "weight": { + "description": "Record weight (SRV only)", + "format": "integer-positive", + "type": "integer" + } + }, + "required": [ + "data" + ] + }, + "DomainAvailableBulk": { + "properties": { + "domains": { + "description": "Domain available response array", + "items": { + "$ref": "#/definitions/DomainAvailableResponse" + }, + "type": "array" + } + }, + "required": [ + "domains" + ] + }, + "DomainAvailableBulkMixed": { + "properties": { + "domains": { + "description": "Domain available response array", + "items": { + "$ref": "#/definitions/DomainAvailableResponse" + }, + "type": "array" + }, + "errors": { + "description": "Errors encountered while performing a domain available check", + "items": { + "$ref": "#/definitions/DomainAvailableError" + }, + "type": "array" + } + }, + "required": [ + "domains" + ] + }, + "DomainAvailableError": { + "properties": { + "code": { + "description": "Short identifier for the error, suitable for indicating the specific error within client code", + "format": "constant", + "type": "string" + }, + "domain": { + "description": "Domain name", + "type": "string" + }, + "message": { + "description": "Human-readable, English description of the error", + "type": "string" + }, + "path": { + "description": "
    \n
  • JSONPath referring to a field containing an error
  • \nOR\n
  • JSONPath referring to a field that refers to an object containing an error, with more detail in `pathRelated`
  • \n
", + "format": "json-path", + "type": "string" + }, + "status": { + "description": "HTTP status code that would return for a single check", + "type": "integer" + } + }, + "required": [ + "code", + "domain", + "path", + "status" + ] + }, + "DomainAvailableResponse": { + "properties": { + "available": { + "description": "Whether or not the domain name is available", + "type": "boolean" + }, + "currency": { + "default": "USD", + "description": "Currency in which the `price` is listed. Only returned if tld is offered", + "format": "iso-currency-code", + "type": "string" + }, + "definitive": { + "description": "Whether or not the `available` answer has been definitively verified with the registry", + "type": "boolean" + }, + "domain": { + "description": "Domain name", + "type": "string" + }, + "period": { + "description": "Number of years included in the price. Only returned if tld is offered", + "format": "integer-positive", + "type": "integer" + }, + "price": { + "description": "Price of the domain excluding taxes or fees. Only returned if tld is offered", + "format": "currency-micro-unit", + "type": "integer" + }, + "renewalPrice": { + "description": "Price for renewing the domain excluding taxes or fees. Only returned if tld is offered", + "format": "currency-micro-unit", + "type": "integer" + } + }, + "required": [ + "domain", + "available", + "definitive" + ] + }, + "DomainChangeOfRegistrant": { + "properties": { + "createDate": { + "type": "string", + "format": "iso-datetime" + }, + "gainingContact": { + "$ref": "#/definitions/ContactRegistrantChange" + }, + "losingContact": { + "$ref": "#/definitions/ContactRegistrantChange" + }, + "otherDomainsAffected": { + "type": "integer", + "format": "integer-positive" + }, + "shopperEmail": { + "type": "string", + "format": "email" + } + }, + "required": [ + "createDate", + "gainingContact", + "losingContact" + ] + }, + "DomainContacts": { + "properties": { + "contactAdmin": { + "$ref": "#/definitions/Contact" + }, + "contactBilling": { + "$ref": "#/definitions/Contact" + }, + "contactRegistrant": { + "$ref": "#/definitions/Contact" + }, + "contactTech": { + "$ref": "#/definitions/Contact" + } + }, + "required": [ + "contactRegistrant" + ] + }, + "DomainContactsCreateV2": { + "additionalProperties": false, + "type": "object", + "properties": { + "admin": { + "$ref": "#/definitions/ContactDomainCreate" + }, + "adminId": { + "description": "Unique identifier of the contact that the user wants to use for the domain admin contact. This can be specified instead of the `admin` property.\n", + "type": "string" + }, + "billing": { + "$ref": "#/definitions/ContactDomainCreate" + }, + "billingId": { + "description": "Unique identifier of the contact that the user wants to use for the domain billing contact. This can be specified instead of the `billing` property.\n", + "type": "string" + }, + "registrant": { + "$ref": "#/definitions/ContactDomainCreate" + }, + "registrantId": { + "description": "Unique identifier of the contact that the user wants to use for the domain registrant contact. This can be specified instead of the `registrant` property.\n", + "type": "string" + }, + "tech": { + "$ref": "#/definitions/ContactDomainCreate" + }, + "techId": { + "description": "Unique identifier of the contact that the user wants to use for the domain tech contact. This can be specified instead of the `tech` property.\n", + "type": "string" + } + } + }, + "DomainContactsUpdateV2": { + "additionalProperties": false, + "properties": { + "identityDocumentId": { + "type": "string", + "description": "Unique identifier of the identify document that the user wants to associate with the domain whose registrant contact is being updated. This is required only if user is trying to update registrant contact and the owning registry has a requirement for an approved identity document" + }, + "contacts": { + "$ref": "#/definitions/DomainContactsCreateV2" + } + } + }, + "DomainContactsV2": { + "additionalProperties": false, + "properties": { + "registrant": { + "$ref": "#/definitions/ContactDomain", + "description": "Registrant contact for the domain" + }, + "admin": { + "$ref": "#/definitions/ContactDomain", + "description": "Administrative contact for the domain" + }, + "tech": { + "$ref": "#/definitions/ContactDomain", + "description": "Technical contact for the domain" + }, + "billing": { + "$ref": "#/definitions/ContactDomain", + "description": "Billing contact for the domain" + } + } + }, + "DomainDetail": { + "properties": { + "authCode": { + "description": "Authorization code for transferring the Domain", + "type": "string" + }, + "contactAdmin": { + "$ref": "#/definitions/Contact", + "description": "Administrative contact for the domain registration" + }, + "contactBilling": { + "$ref": "#/definitions/Contact", + "description": "Billing contact for the domain registration" + }, + "contactRegistrant": { + "$ref": "#/definitions/Contact", + "description": "Registration contact for the domain" + }, + "contactTech": { + "$ref": "#/definitions/Contact", + "description": "Technical contact for the domain registration" + }, + "createdAt": { + "description": "Date and time when this domain was created", + "format": "date-time", + "type": "string" + }, + "deletedAt": { + "description": "Date and time when this domain was deleted", + "format": "date-time", + "type": "string" + }, + "transferAwayEligibleAt": { + "description": "Date and time when this domain is eligible to transfer", + "format": "date-time", + "type": "string" + }, + "domain": { + "description": "Name of the domain", + "type": "string" + }, + "domainId": { + "description": "Unique identifier for this Domain", + "format": "double", + "type": "number" + }, + "expirationProtected": { + "description": "Whether or not the domain is protected from expiration", + "type": "boolean" + }, + "expires": { + "description": "Date and time when this domain will expire", + "format": "date-time", + "type": "string" + }, + "exposeRegistrantOrganization": { + "type": "boolean", + "description": "Whether or not the domain registrant contact organization field should be shown in the WHOIS" + }, + "exposeWhois": { + "description": "Whether or not the domain contact details should be shown in the WHOIS", + "type": "boolean" + }, + "holdRegistrar": { + "description": "Whether or not the domain is on-hold by the registrar", + "type": "boolean" + }, + "locked": { + "description": "Whether or not the domain is locked to prevent transfers", + "type": "boolean" + }, + "nameServers": { + "description": "Fully-qualified domain names for DNS servers", + "items": { + "format": "host-name", + "type": "string" + }, + "type": "array" + }, + "privacy": { + "description": "Whether or not the domain has privacy protection", + "type": "boolean" + }, + "registrarCreatedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain was created by the registrar" + }, + "renewAuto": { + "description": "Whether or not the domain is configured to automatically renew", + "type": "boolean" + }, + "renewDeadline": { + "description": "Date the domain must renew on", + "format": "date-time", + "type": "string" + }, + "status": { + "description": "Processing status of the domain
    \n
  • ACTIVE - All is well
  • \n
  • AWAITING* - System is waiting for the end-user to complete an action
  • \n
  • CANCELLED* - Domain has been cancelled, and may or may not be reclaimable
  • \n
  • CONFISCATED - Domain has been confiscated, usually for abuse, chargeback, or fraud
  • \n
  • DISABLED* - Domain has been disabled
  • \n
  • EXCLUDED* - Domain has been excluded from Firehose registration
  • \n
  • EXPIRED* - Domain has expired
  • \n
  • FAILED* - Domain has failed a required action, and the system is no longer retrying
  • \n
  • HELD* - Domain has been placed on hold, and likely requires intervention from Support
  • \n
  • LOCKED* - Domain has been locked, and likely requires intervention from Support
  • \n
  • PARKED* - Domain has been parked, and likely requires intervention from Support
  • \n
  • PENDING* - Domain is working its way through an automated workflow
  • \n
  • RESERVED* - Domain is reserved, and likely requires intervention from Support
  • \n
  • REVERTED - Domain has been reverted, and likely requires intervention from Support
  • \n
  • SUSPENDED* - Domain has been suspended, and likely requires intervention from Support
  • \n
  • TRANSFERRED* - Domain has been transferred out
  • \n
  • UNKNOWN - Domain is in an unknown state
  • \n
  • UNLOCKED* - Domain has been unlocked, and likely requires intervention from Support
  • \n
  • UNPARKED* - Domain has been unparked, and likely requires intervention from Support
  • \n
  • UPDATED* - Domain ownership has been transferred to another account
  • \n
", + "type": "string" + }, + "subaccountId": { + "description": "Reseller subaccount shopperid who can manage the domain", + "type": "string" + }, + "transferProtected": { + "description": "Whether or not the domain is protected from transfer", + "type": "boolean" + }, + "verifications": { + "$ref": "#/definitions/VerificationsDomain", + "description": "Progress and status for each of the verification processes requested for this domain" + } + }, + "required": [ + "domainId", + "domain", + "status", + "expirationProtected", + "holdRegistrar", + "locked", + "privacy", + "renewAuto", + "renewDeadline", + "transferProtected", + "createdAt", + "authCode", + "nameServers", + "contactRegistrant", + "contactBilling", + "contactAdmin", + "contactTech" + ] + }, + "DomainDetailV2": { + "additionalProperties": false, + "properties": { + "domainId": { + "type": "string", + "description": "Unique identifier for this Domain" + }, + "domain": { + "type": "string", + "description": "Name of the domain", + "format": "domain" + }, + "subaccountId": { + "type": "string", + "description": "Reseller subaccount shopperid who can manage the domain" + }, + "status": { + "type": "string", + "description": "The current status of the domain
  • ACTIVE - Domain has been registered and is active.
  • CANCELLED - Domain has been cancelled by the user or system, and is not be reclaimable.
  • DELETED_REDEEMABLE - Domain is deleted but is redeemable.
  • EXPIRED - Domain has expired.
  • FAILED - Domain registration or transfer error.
  • LOCKED_REGISTRAR - Domain is locked at the registrar - this is usually the result of a spam, abuse, etc.
  • PARKED - Domain has been parked.
  • HELD_REGISTRAR - Domain is held at the registrar and cannot be transferred or modified - this is usually the result of a dispute.
  • OWNERSHIP_CHANGED - Domain has been moved to another account.
  • PENDING_TRANSFER - Domain transfer has been requested and is pending the transfer process.
  • PENDING_REGISTRATION - Domain is pending setup at the registry.
  • REPOSSESSED - Domain has been confiscated - this is usually the result of a chargeback, fraud, abuse, etc.).
  • SUSPENDED - Domain is in violation and has been suspended.
  • TRANSFERRED - Domain has been transferred to another registrar.
", + "enum": [ + "ACTIVE", + "CANCELLED", + "DELETED_REDEEMABLE", + "EXPIRED", + "FAILED", + "LOCKED_REGISTRAR", + "PARKED", + "HELD_REGISTRAR", + "OWNERSHIP_CHANGED", + "PENDING_TRANSFER", + "PENDING_REGISTRATION", + "REPOSSESSED", + "SUSPENDED", + "TRANSFERRED" + ] + }, + "expiresAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain will expire" + }, + "expirationProtected": { + "type": "boolean", + "description": "Whether or not the domain is protected from expiration" + }, + "holdRegistrar": { + "type": "boolean", + "description": "Whether or not the domain is on-hold by the registrar" + }, + "locked": { + "type": "boolean", + "description": "Whether or not the domain is locked to prevent transfers" + }, + "privacy": { + "type": "boolean", + "description": "Whether or not the domain has privacy protection" + }, + "registrarCreatedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain was created by the registrar" + }, + "renewAuto": { + "type": "boolean", + "description": "Whether or not the domain is configured to automatically renew" + }, + "renewDeadline": { + "type": "string", + "format": "iso-datetime", + "description": "Date the domain must renew on" + }, + "transferProtected": { + "type": "boolean", + "description": "Whether or not the domain is protected from transfer" + }, + "createdAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain was created" + }, + "deletedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain was deleted" + }, + "modifiedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain was last modified" + }, + "transferAwayEligibleAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain is eligible to transfer" + }, + "authCode": { + "type": "string", + "description": "Authorization code for transferring the Domain" + }, + "nameServers": { + "type": "array", + "items": { + "type": "string", + "format": "host-name" + }, + "description": "Fully-qualified domain names for DNS servers" + }, + "hostnames": { + "type": "array", + "items": { + "type": "string", + "format": "host-name" + }, + "description": "Hostnames owned by the domain" + }, + "renewal": { + "$ref": "#/definitions/RenewalDetails", + "description": "Information for renewal of domain" + }, + "verifications": { + "$ref": "#/definitions/VerificationsDomainV2", + "description": "Progress and status for each of the verification processes required for this domain" + }, + "contacts": { + "$ref": "#/definitions/DomainContactsV2", + "description": "Contacts for the domain registration" + }, + "actions": { + "type": "array", + "items": { + "$ref": "#/definitions/Action" + }, + "description": "List of current actions in progress for this domain" + }, + "dnssecRecords": { + "type": "array", + "items": { + "$ref": "#/definitions/DomainDnssec" + }, + "description": "List of active DNSSEC records for this domain" + }, + "registryStatusCodes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "ADD_PERIOD", + "AUTO_RENEW_PERIOD", + "CLIENT_DELETE_PROHIBITED", + "CLIENT_HOLD", + "CLIENT_RENEW_PROHIBITED", + "CLIENT_TRANSFER_PROHIBITED", + "CLIENT_UPDATE_PROHIBITED", + "INACTIVE", + "OK", + "PENDING_CREATE", + "PENDING_DELETE", + "PENDING_RENEW", + "PENDING_RESTORE", + "PENDING_TRANSFER", + "PENDING_UPDATE", + "REDEMPTION_PERIOD", + "RENEW_PERIOD", + "SERVER_DELETE_PROHIBITED", + "SERVER_HOLD", + "SERVER_RENEW_PROHIBITED", + "SERVER_TRANSFER_PROHIBITED", + "SERVER_UPDATE_PROHIBITED", + "TRANSFER_PERIOD" + ] + }, + "description": "The current registry status codes of the domain
  • ADD_PERIOD - This grace period is provided after the initial registration of a domain name.
  • AUTO_RENEW_PERIOD - This grace period is provided after a domain name registration period expires and is extended (renewed) automatically by the registry.
  • CLIENT_DELETE_PROHIBITED - This status code tells your domain's registry to reject requests to delete the domain.
  • CLIENT_HOLD - This status code tells your domain's registry to not activate your domain in the DNS and as a consequence, it will not resolve.
  • CLIENT_RENEW_PROHIBITED - This status code tells your domain's registry to reject requests to renew your domain.
  • CLIENT_TRANSFER_PROHIBITED - This status code tells your domain's registry to reject requests to transfer the domain from your current registrar to another.
  • CLIENT_UPDATE_PROHIBITED - This status code tells your domain's registry to reject requests to update the domain.
  • INACTIVE - This status code indicates that delegation information (name servers) has not been associated with your domain.
  • OK - This is the standard status for a domain, meaning it has no pending operations or prohibitions.
  • PENDING_CREATE - This status code indicates that a request to create your domain has been received and is being processed.
  • PENDING_DELETE - This status code indicates that the domain is either in a redemption period if combined with either REDEMPTION_PERIOD or PENDING_RESTORE, if not combined with these, then indicates that the redemption period for the domain has ended and domain will be be purged and dropped from the registry database.
  • PENDING_RENEW - This status code indicates that a request to renew your domain has been received and is being processed.
  • PENDING_RESTORE - This status code indicates that your registrar has asked the registry to restore your domain that was in REDEMPTION_PERIOD status
  • PENDING_TRANSFER - This status code indicates that a request to transfer your domain to a new registrar has been received and is being processed.
  • PENDING_UPDATE - This status code indicates that a request to update your domain has been received and is being processed.
  • REDEMPTION_PERIOD - This status code indicates that your registrar has asked the registry to delete your domain.
  • RENEW_PERIOD - This grace period is provided after a domain name registration period is explicitly extended (renewed) by the registrar.
  • SERVER_DELETE_PROHIBITED - This status code prevents your domain from being deleted.
  • SERVER_HOLD - This status code is set by your domain's Registry Operator. Your domain is not activated in the DNS.
  • SERVER_RENEW_PROHIBITED - This status code indicates your domain's Registry Operator will not allow your registrar to renew your domain.
  • SERVER_TRANSFER_PROHIBITED - This status code prevents your domain from being transferred from your current registrar to another.
  • SERVER_UPDATE_PROHIBITED - This status code locks your domain preventing it from being updated.
  • TRANSFER_PERIOD - This grace period is provided after the successful transfer of a domain name from one registrar to another.
" + } + }, + "required": [ + "domainId", + "domain", + "status", + "expirationProtected", + "holdRegistrar", + "locked", + "privacy", + "renewAuto", + "renewDeadline", + "transferProtected", + "createdAt", + "authCode", + "nameServers", + "contacts" + ] + }, + "DomainDnssec" : { + "properties": { + "algorithm": { + "description": "This identifies the cryptographic algorithm used to generate the signature
  • RSAMD5 - [01] DRSA/MD5
  • DSA - [03] DSA/SHA1
  • RSASHA1 - [05] RSA/SHA-1
  • DSA_NSEC3_SHA1 - [06] DSA-NSEC3-SHA1
  • RSASHA1_NSEC3_SHA1 - [07] RSASHA1-NSEC3-SHA1
  • RSASHA256 - [08] RSA/SHA-256
  • RSASHA512 - [10] RSA/SHA-512
  • ECC_GOST - [12] GOST R 34.10-2001
  • ECDSAP256SHA256 - [13] ECDSA Curve P-256 with SHA-256
  • ECDSAP384SHA384 - [14] ECDSA Curve P-384 with SHA-384
  • ED25519 - [15] Ed25519
  • ED448 - [16] Ed448
", + "enum": [ + "RSAMD5", + "DH", + "DSA", + "RSASHA1", + "DSA_NSEC3_SHA1", + "RSASHA1_NSEC3_SHA1", + "RSASHA256", + "RSASHA512", + "ECC_GOST", + "ECDSAP256SHA256", + "ECDSAP384SHA384", + "ED25519", + "ED448", + "PRIVATEDNS", + "PRIVATEOID" + ], + "type": "string" + }, + "keyTag": { + "description": "This is an integer value less than 65536 used to identify the DNSSEC record for the domain name.", + "type": "integer", + "format": "integer-positive", + "maximum": 65536 + }, + "digestType": { + "description": "This identifies the algorithm used to construct the digest
  • SHA1 - [01] SHA-1
  • SHA256 - [02] SHA-256
  • GOST - [03] GOST R 34.11-94
  • SHA384 - [04] SHA-384
", + "enum": [ + "SHA1", + "SHA256", + "GOST", + "SHA384" + ], + "type": "string" + }, + "digest": { + "type": "string", + "description": "The digest is an alpha-numeric value" + }, + "flags": { + "description": "This identifies the key type; either a Zone-Signing Key or a Key-Signing Key
  • ZSK - [256] Zone-Signing Key
  • KSK - [257] Key-Signing Key
", + "enum": [ + "ZSK", + "KSK" + ], + "type": "string" + }, + "publicKey": { + "type": "string", + "description": "Registries use this value to encrypt DS records. Decryption requires a matching public key" + }, + "maxSignatureLife": { + "type": "integer", + "format": "integer-positive", + "description": "This specifies the validity period for the signature. The value is expressed in seconds. You can use any integer value larger than zero" + } + }, + "required": [ + "algorithm" + ] + }, + "DomainNameServerUpdateV2": { + "additionalProperties": false, + "properties": { + "nameServers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Fully-qualified domain names for name servers to associate with the domain" + } + } + }, + "DomainPrivacyForwarding": { + "properties": { + "privateEmail": { + "type": "string", + "description": "The private email" + }, + "forwardingEmail": { + "type": "string", + "description": "The email that it forwards to" + }, + "emailPreference": { + "type": "string", + "enum": [ + "EMAIL_FILTER", + "EMAIL_SEND_ALL", + "EMAIL_SEND_NONE" + ], + "description": "The email forwarding preference for the domain
  • EMAIL_FILTER - Filter for spam and forward email.
  • EMAIL_SEND_ALL - Forward all email.
  • EMAIL_SEND_NONE - Don't forward email.
" + } + } + }, + "DomainPrivacyForwardingUpdate": { + "properties": { + "privateEmailType": { + "type": "string", + "enum": [ + "DEFAULT", + "RANDOM" + ], + "description": "The private email type
  • DEFAULT - Use default email address (example: domainname.com@domainsbyproxy.com)
  • RANDOM - Randomize email address, we’ll create a private email address using random characters to further protect you from unwanted spam. (5f9e6c9368a64565bf2b2cf8db91aea4@domainsbyproxy.com)
" + }, + "forwardingEmail": { + "type": "string", + "description": "The email that it forwards to" + }, + "emailPreference": { + "type": "string", + "enum": [ + "EMAIL_FILTER", + "EMAIL_SEND_ALL", + "EMAIL_SEND_NONE" + ], + "description": "The email forwarding preference for the domain
  • EMAIL_FILTER - Filter for spam and forward email.
  • EMAIL_SEND_ALL - Forward all email.
  • EMAIL_SEND_NONE - Don't forward email.
" + } + }, + "required": [ + "privateEmailType", + "emailPreference" + ] + }, + "DomainNotification": { + "properties": { + "notificationId": { + "description": "The notification ID to be used in POST /v2/customers/{customerId}/domains/notifications to acknowledge the notification", + "default": "", + "type": "string" + }, + "type": { + "description": "The type of action the notification relates to", + "enum": [ + "AUTH_CODE_PURCHASE", + "AUTH_CODE_REGENERATE", + "AUTO_RENEWAL", + "BACKORDER", + "BACKORDER_PURCHASE", + "BACKORDER_DELETE", + "BACKORDER_UPDATE", + "CHANGE_OF_REGISTRANT_DELETE", + "CONTACT_CREATE", + "CONTACT_DELETE", + "CONTACT_UPDATE", + "DNS_VERIFICATION", + "DNSSEC_CREATE", + "DNSSEC_DELETE", + "DOMAIN_DELETE", + "DOMAIN_UPDATE", + "DOMAIN_UPDATE_CONTACTS", + "DOMAIN_UPDATE_NAME_SERVERS", + "EXPIRY", + "HOST_CREATE", + "HOST_DELETE", + "ICANN_VERIFICATION", + "MIGRATE", + "MIGRATE_IN", + "PREMIUM", + "PRIVACY_FORWARDING_UPDATE", + "PRIVACY_PURCHASE", + "PRIVACY_DELETE", + "REDEEM", + "REGISTER", + "RENEW", + "RENEW_UNDO", + "TRADE", + "TRADE_CANCEL", + "TRADE_PURCHASE", + "TRADE_PURCHASE_AUTH_TEXT_MESSAGE", + "TRADE_RESEND_AUTH_EMAIL", + "TRANSFER", + "TRANSFER_IN", + "TRANSFER_IN_ACCEPT", + "TRANSFER_IN_CANCEL", + "TRANSFER_IN_RESTART", + "TRANSFER_IN_RETRY", + "TRANSFER_OUT", + "TRANSFER_OUT_ACCEPT", + "TRANSFER_OUT_REJECT", + "TRANSFER_OUT_REQUESTED", + "TRANSIT" + ], + "type": "string" + }, + "resource": { + "description": "The resource the notification pertains to.", + "default": "", + "type": "string" + }, + "resourceType": { + "description": "The type of resource the notification relates to", + "enum": [ + "CONTACT", + "DOMAIN", + "HOST" + ], + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "AWAITING", + "CANCELLED", + "FAILED", + "PENDING", + "SUCCESS" + ], + "description": "The resulting status of the action." + }, + "addedAt": { + "description": "The date the notification was added", + "default": "", + "type": "string", + "format": "iso-datetime" + }, + "requestId" : { + "type": "string", + "description": "A client provided identifier (via X-Request-Id header) indicating the request this notification is for" + }, + "metadata": { + "description": "The notification data for the given type as specifed by GET /v2/customers/{customerId}/domains/notifications/schema", + "default": "", + "type": "object" + } + }, + "required": [ + "notificationId", + "type", + "resource", + "resourceType", + "status", + "addedAt" + ] + }, + "DomainNotificationType": { + "properties": { + "type": { + "description": "The notification type", + "enum": [ + "AUTH_CODE_PURCHASE", + "AUTH_CODE_REGENERATE", + "AUTO_RENEWAL", + "BACKORDER", + "BACKORDER_PURCHASE", + "BACKORDER_DELETE", + "BACKORDER_UPDATE", + "CHANGE_OF_REGISTRANT_DELETE", + "CONTACT_CREATE", + "CONTACT_DELETE", + "CONTACT_UPDATE", + "DNS_VERIFICATION", + "DNSSEC_CREATE", + "DNSSEC_DELETE", + "DOMAIN_DELETE", + "DOMAIN_UPDATE", + "DOMAIN_UPDATE_CONTACTS", + "DOMAIN_UPDATE_NAME_SERVERS", + "EXPIRY", + "HOST_CREATE", + "HOST_DELETE", + "ICANN_VERIFICATION", + "MIGRATE", + "MIGRATE_IN", + "PREMIUM", + "PRIVACY_FORWARDING_UPDATE", + "PRIVACY_PURCHASE", + "PRIVACY_DELETE", + "REDEEM", + "REGISTER", + "RENEW", + "RENEW_UNDO", + "TRADE", + "TRADE_CANCEL", + "TRADE_PURCHASE", + "TRADE_PURCHASE_AUTH_TEXT_MESSAGE", + "TRADE_RESEND_AUTH_EMAIL", + "TRANSFER", + "TRANSFER_IN", + "TRANSFER_IN_ACCEPT", + "TRANSFER_IN_CANCEL", + "TRANSFER_IN_RESTART", + "TRANSFER_IN_RETRY", + "TRANSFER_OUT", + "TRANSFER_OUT_ACCEPT", + "TRANSFER_OUT_REJECT", + "TRANSFER_OUT_REQUESTED", + "TRANSIT" + ], + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "DomainPurchase": { + "properties": { + "consent": { + "$ref": "#/definitions/Consent" + }, + "contactAdmin": { + "$ref": "#/definitions/Contact" + }, + "contactBilling": { + "$ref": "#/definitions/Contact" + }, + "contactRegistrant": { + "$ref": "#/definitions/Contact" + }, + "contactTech": { + "$ref": "#/definitions/Contact" + }, + "domain": { + "description": "For internationalized domain names with non-ascii characters, the domain name is converted to punycode before format and pattern validation rules are checked", + "format": "domain", + "type": "string" + }, + "nameServers": { + "items": { + "format": "host-name", + "type": "string" + }, + "type": "array" + }, + "period": { + "default": 1, + "format": "integer-positive", + "maximum": 10, + "minimum": 1, + "type": "integer" + }, + "privacy": { + "default": false, + "type": "boolean" + }, + "renewAuto": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "domain", + "consent" + ] + }, + "DomainPurchaseResponse": { + "properties": { + "currency": { + "default": "USD", + "description": "Currency in which the `total` is listed", + "format": "iso-currency-code", + "type": "string" + }, + "itemCount": { + "description": "Number items included in the order", + "format": "integer-positive", + "type": "integer" + }, + "orderId": { + "description": "Unique identifier of the order processed to purchase the domain", + "format": "int64", + "type": "integer" + }, + "total": { + "description": "Total cost of the domain and any selected add-ons", + "format": "currency-micro-unit", + "type": "integer" + } + }, + "required": [ + "orderId", + "itemCount", + "total" + ] + }, + "DomainRenew": { + "properties": { + "period": { + "description": "Number of years to extend the Domain. Must not exceed maximum for TLD. When omitted, defaults to `period` specified during original purchase", + "format": "integer-positive", + "maximum": 10, + "minimum": 1, + "type": "integer" + } + } + }, + "DomainSuggestion": { + "properties": { + "domain": { + "description": "Suggested domain name", + "type": "string" + } + }, + "required": [ + "domain" + ] + }, + "DomainSummary": { + "properties": { + "authCode": { + "description": "Authorization code for transferring the Domain", + "type": "string" + }, + "contactAdmin": { + "$ref": "#/definitions/Contact", + "description": "Administrative contact for the domain registration" + }, + "contactBilling": { + "$ref": "#/definitions/Contact", + "description": "Billing contact for the domain registration" + }, + "contactRegistrant": { + "$ref": "#/definitions/Contact", + "description": "Registration contact for the domain" + }, + "contactTech": { + "$ref": "#/definitions/Contact", + "description": "Technical contact for the domain registration" + }, + "createdAt": { + "description": "Date and time when this domain was created", + "format": "date-time", + "type": "string" + }, + "deletedAt": { + "description": "Date and time when this domain was deleted", + "format": "date-time", + "type": "string" + }, + "transferAwayEligibleAt": { + "description": "Date and time when this domain is eligible to transfer", + "format": "date-time", + "type": "string" + }, + "domain": { + "description": "Name of the domain", + "type": "string" + }, + "domainId": { + "description": "Unique identifier for this Domain", + "format": "double", + "type": "number" + }, + "expirationProtected": { + "description": "Whether or not the domain is protected from expiration", + "type": "boolean" + }, + "expires": { + "description": "Date and time when this domain will expire", + "format": "date-time", + "type": "string" + }, + "exposeWhois": { + "description": "Whether or not the domain contact details should be shown in the WHOIS", + "type": "boolean" + }, + "holdRegistrar": { + "description": "Whether or not the domain is on-hold by the registrar", + "type": "boolean" + }, + "locked": { + "description": "Whether or not the domain is locked to prevent transfers", + "type": "boolean" + }, + "nameServers": { + "description": "Fully-qualified domain names for DNS servers", + "items": { + "format": "host-name", + "type": "string" + }, + "type": "array" + }, + "privacy": { + "description": "Whether or not the domain has privacy protection", + "type": "boolean" + }, + "registrarCreatedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time when this domain was created by the registrar" + }, + "renewAuto": { + "description": "Whether or not the domain is configured to automatically renew", + "type": "boolean" + }, + "renewDeadline": { + "description": "Date the domain must renew on", + "format": "date-time", + "type": "string" + }, + "renewable": { + "description": "Whether or not the domain is eligble for renewal based on status", + "type": "boolean" + }, + "status": { + "description": "Processing status of the domain
    \n
  • ACTIVE - All is well
  • \n
  • AWAITING* - System is waiting for the end-user to complete an action
  • \n
  • CANCELLED* - Domain has been cancelled, and may or may not be reclaimable
  • \n
  • CONFISCATED - Domain has been confiscated, usually for abuse, chargeback, or fraud
  • \n
  • DISABLED* - Domain has been disabled
  • \n
  • EXCLUDED* - Domain has been excluded from Firehose registration
  • \n
  • EXPIRED* - Domain has expired
  • \n
  • FAILED* - Domain has failed a required action, and the system is no longer retrying
  • \n
  • HELD* - Domain has been placed on hold, and likely requires intervention from Support
  • \n
  • LOCKED* - Domain has been locked, and likely requires intervention from Support
  • \n
  • PARKED* - Domain has been parked, and likely requires intervention from Support
  • \n
  • PENDING* - Domain is working its way through an automated workflow
  • \n
  • RESERVED* - Domain is reserved, and likely requires intervention from Support
  • \n
  • REVERTED - Domain has been reverted, and likely requires intervention from Support
  • \n
  • SUSPENDED* - Domain has been suspended, and likely requires intervention from Support
  • \n
  • TRANSFERRED* - Domain has been transferred out
  • \n
  • UNKNOWN - Domain is in an unknown state
  • \n
  • UNLOCKED* - Domain has been unlocked, and likely requires intervention from Support
  • \n
  • UNPARKED* - Domain has been unparked, and likely requires intervention from Support
  • \n
  • UPDATED* - Domain ownership has been transferred to another account
  • \n
", + "type": "string" + }, + "transferProtected": { + "description": "Whether or not the domain is protected from transfer", + "type": "boolean" + } + }, + "required": [ + "domainId", + "domain", + "status", + "expirationProtected", + "holdRegistrar", + "locked", + "privacy", + "renewAuto", + "renewDeadline", + "transferProtected", + "createdAt", + "contactRegistrant" + ] + }, + "DomainForwardingMask": { + "properties": { + "title" : { + "type": "string", + "description": "Displays at the top of the browser window and in search results." + }, + "description" : { + "type": "string", + "description": "A short description of your website to display in search engine results." + }, + "keywords": { + "type": "string", + "description": "A list of comma-separated keywords that describes the content and purpose of your website." + } + } + }, + "DomainForwardingCreate": { + "properties": { + "type": { + "type": "string", + "default": "REDIRECT_PERMANENT", + "enum": [ + "MASKED", + "REDIRECT_PERMANENT", + "REDIRECT_TEMPORARY" + ], + "description": "The type of fowarding to implement
  • MASKED - Prevents the forwarded domain or subdomain URL from displaying in the browser's address bar.
  • REDIRECT_PERMANENT* - Redirects to the url you specified in the forwardTo field using a `301 Moved Permanently` HTTP response. The HTTP 301 response code tells user-agents (including search engines) that the location has permanently moved.
  • REDIRECT_TEMPORARY - Redirects to the url you specified in the forwardTo field using a `302 Found` HTTP response. The HTTP 302 response code tells user-agents (including search engines) that the location has temporarily moved.
" + }, + "url" : { + "type": "string", + "format": "url", + "description": "Forwards http(s) traffic to this destination url (ex. http://www.somedomain.com/)" + }, + "mask": { + "$ref": "#/definitions/DomainForwardingMask", + "description": "Additional configuration that can be provided when type = 'MASKED'" + } + }, + "required": [ + "type", + "url" + ] + }, + "DomainForwarding": { + "properties": { + "fqdn" : { + "type": "string", + "description": "The fqdn (domain or sub domain) to forward (ex somedomain.com or sub.somedomain.com)" + }, + "type": { + "type": "string", + "default": "REDIRECT_PERMANENT", + "enum": [ + "MASKED", + "REDIRECT_PERMANENT", + "REDIRECT_TEMPORARY" + ], + "description": "The type of fowarding to implement
  • MASKED - Prevents the forwarded domain or subdomain URL from displaying in the browser's address bar.
  • REDIRECT_PERMANENT* - Redirects to the url you specified in the forwardTo field using a `301 Moved Permanently` HTTP response. The HTTP 301 response code tells user-agents (including search engines) that the location has permanently moved.
  • REDIRECT_TEMPORARY - Redirects to the url you specified in the forwardTo field using a `302 Found` HTTP response. The HTTP 302 response code tells user-agents (including search engines) that the location has temporarily moved.
" + }, + "url" : { + "type": "string", + "format": "url", + "description": "Forwards http(s) traffic to this destination url (ex. http://www.somedomain.com/)" + }, + "mask": { + "$ref": "#/definitions/DomainForwardingMask", + "description": "Additional configuration that can be provided when type = 'MASKED'" + } + }, + "required": [ + "fqdn", + "type", + "url" + ] + }, + "DomainPurchaseV2": { + "additionalProperties": false, + "properties": { + "domain": { + "type": "string", + "format": "domain", + "pattern": "^[^.]{1,63}.[^.]{2,}$", + "description": "For internationalized domain names with non-ascii characters, the domain name is converted to punycode before format and pattern validation rules are checked" + }, + "consent": { + "$ref": "#/definitions/ConsentV2" + }, + "period": { + "type": "integer", + "format": "integer-positive", + "default": 1, + "minimum": 1, + "maximum": 10, + "pattern": "^[0-9]+$" + }, + "nameServers": { + "type": "array", + "items": { + "type": "string", + "format": "host-name" + }, + "maxItems": 2 + }, + "renewAuto": { + "type": "boolean", + "default": true + }, + "privacy": { + "type": "boolean", + "default": false + }, + "contacts": { + "$ref": "#/definitions/DomainContactsCreateV2", + "description": "Contacts for the domain registration" + }, + "metadata": { + "type": "object", + "description": "The domain eligibility data fields as specified by GET /v2/customers/{customerId}/domains/register/schema/{tld}" + } + }, + "required": [ + "domain", + "consent" + ] + }, + "DomainRedeemV2": { + "additionalProperties": false, + "properties": { + "consent": { + "$ref": "#/definitions/ConsentRedemption" + } + }, + "required": [ + "consent" + ] + }, + "DomainRenewV2": { + "additionalProperties": false, + "properties": { + "expires": { + "type": "string", + "format": "iso-datetime", + "description": "Current date when this domain will expire" + }, + "consent": { + "$ref": "#/definitions/ConsentRenew" + }, + "period": { + "type": "integer", + "description": "Number of years to extend the Domain. Must not exceed maximum for TLD. When omitted, defaults to `period` specified during original purchase", + "format": "integer-positive", + "minimum": 1, + "maximum": 10, + "pattern": "^[0-9]+$" + } + }, + "required": [ + "consent", + "expires" + ] + }, + "DomainTransferAuthCode" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "authCode" : { + "description" : "Authorization code for transferring the Domain", + "type" : "string" + } + }, + "required" : [ "authCode" ] + }, + "DomainTransferStatus" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "transferStatusCodes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "CLIENT_APPROVED", + "CLIENT_CANCELLED", + "CLIENT_REJECTED", + "PENDING", + "SERVER_APPROVED", + "SERVER_CANCELLED" + ] + }, + "description": "The current registry transfer status codes of the domain
  • CLIENT_APPROVED - The losing registrar approved the transfer.
  • CLIENT_CANCELLED - The gaining registrar cancelled the transfer.
  • CLIENT_REJECTED - The losing registrar rejected the transfer.
  • PENDING - The transfer is pending at the registry.
  • SERVER_APPROVED - The registry approved the transfer, auto-approve.
  • SERVER_CANCELLED - The registry cancelled the transfer.
" + } + }, + "required" : [ "transferStatusCodes" ] + }, + "DomainTransferIn": { + "properties": { + "authCode": { + "description": "Authorization code from registrar for transferring a domain", + "type": "string" + }, + "consent": { + "$ref": "#/definitions/Consent", + "description": "Required agreements can be retrieved via the GET ./domains/agreements endpoint" + }, + "period": { + "default": 1, + "description": "Can be more than 1 but no more than 10 years total including current registration length", + "format": "integer-positive", + "maximum": 10, + "minimum": 1, + "type": "integer" + }, + "privacy": { + "default": false, + "description": "Whether or not privacy has been requested", + "type": "boolean" + }, + "renewAuto": { + "default": true, + "description": "Whether or not the domain should be configured to automatically renew", + "type": "boolean" + }, + "contactAdmin": { + "$ref": "#/definitions/Contact", + "description": "The contact to use for the domain admin contact. Depending on the tld of the domain being transferred, this field may be required." + }, + "contactBilling": { + "$ref": "#/definitions/Contact", + "description": "The contact to use for the domain billing contact. Depending on the tld of the domain being transferred, this field may be required." + }, + "contactRegistrant": { + "$ref": "#/definitions/Contact", + "description": "The contact to use for the domain registrant contact. Depending on the tld of the domain being transferred, this field may be required." + }, + "contactTech": { + "$ref": "#/definitions/Contact", + "description": "The contact to use for the domain tech contact. Depending on the tld of the domain being transferred, this field may be required." + } + }, + "required": [ + "authCode", + "consent" + ] + }, + "DomainTransferInV2": { + "additionalProperties": false, + "properties": { + "authCode": { + "type": "string", + "description": "Authorization code from registrar for transferring a domain" + }, + "period": { + "type": "integer", + "format": "integer-positive", + "default": 1, + "minimum": 1, + "maximum": 10, + "pattern": "^[0-9]+$", + "description": "Can be more than 1 but no more than 10 years total including current registration length" + }, + "renewAuto": { + "type": "boolean", + "default": true, + "description": "Whether or not the domain should be configured to automatically renew" + }, + "privacy": { + "type": "boolean", + "default": false, + "description": "Whether or not privacy has been requested" + }, + "identityDocumentId": { + "type": "string", + "description": "Unique identifier of the identify document that the user wants to associate with the domain being transferred in. This is required only if the gaining registry has a requirement for an approved identity document" + }, + "consent": { + "$ref": "#/definitions/ConsentV2", + "description": "Required agreements can be retrieved via the GET ./domains/agreements endpoint" + }, + "contacts": { + "$ref": "#/definitions/DomainContactsCreateV2", + "description": "Contacts for the domain registration" + }, + "metadata": { + "type": "object", + "description": "The domain eligibility data fields as specified by GET /v2/customers/{customerId}/domains/register/schema/{tld}" + } + }, + "required": [ + "authCode", + "consent" + ] + }, + "DomainUpdate": { + "properties": { + "locked": { + "description": "Whether or not the domain should be locked to prevent transfers", + "type": "boolean" + }, + "nameServers": { + "description": "Fully-qualified domain names for Name Servers to associate with the domain", + "items": { + "format": "host-name" + }, + "type": "array" + }, + "renewAuto": { + "description": "Whether or not the domain should be configured to automatically renew", + "type": "boolean" + }, + "subaccountId": { + "description": "Reseller subaccount shopperid who can manage the domain", + "type": "string" + }, + "exposeRegistrantOrganization": { + "type": "boolean", + "description": "Whether or not the domain registrant contact organization field should be shown in the WHOIS" + }, + "exposeWhois": { + "description": "Whether or not the domain contact details should be shown in the WHOIS", + "type": "boolean" + }, + "consent": { + "$ref": "#/definitions/ConsentDomainUpdate" + } + } + }, + "DomainsContactsBulk": { + "properties": { + "contactAdmin": { + "$ref": "#/definitions/Contact" + }, + "contactBilling": { + "$ref": "#/definitions/Contact" + }, + "contactPresence": { + "$ref": "#/definitions/Contact" + }, + "contactRegistrant": { + "$ref": "#/definitions/Contact" + }, + "contactTech": { + "$ref": "#/definitions/Contact" + }, + "domains": { + "description": "An array of domain names to be validated against. Alternatively, you can specify the extracted tlds. However, full domain names are required if the tld is `uk`", + "items": { + "format": "domain", + "type": "string" + }, + "type": "array" + }, + "entityType": { + "description": "Canadian Presence Requirement (CA)", + "enum": [ + "ABORIGINAL", + "ASSOCIATION", + "CITIZEN", + "CORPORATION", + "EDUCATIONAL", + "GOVERNMENT", + "HOSPITAL", + "INDIAN_BAND", + "LEGAL_REPRESENTATIVE", + "LIBRARY_ARCHIVE_MUSEUM", + "MARK_REGISTERED", + "MARK_TRADE", + "PARTNERSHIP", + "POLITICAL_PARTY", + "RESIDENT_PERMANENT", + "TRUST", + "UNION" + ], + "type": "string" + } + }, + "required": [ + "domains" + ] + }, + "Error": { + "properties": { + "code": { + "description": "Short identifier for the error, suitable for indicating the specific error within client code", + "format": "constant", + "type": "string" + }, + "fields": { + "description": "List of the specific fields, and the errors found with their contents", + "items": { + "$ref": "#/definitions/ErrorField" + }, + "type": "array" + }, + "message": { + "description": "Human-readable, English description of the error", + "type": "string" + } + }, + "required": [ + "code" + ] + }, + "ErrorDomainContactsValidate": { + "properties": { + "code": { + "description": "Short identifier for the error, suitable for indicating the specific error within client code", + "format": "constant", + "type": "string" + }, + "fields": { + "description": "List of the specific fields, and the errors found with their contents", + "items": { + "$ref": "#/definitions/ErrorFieldDomainContactsValidate" + }, + "type": "array" + }, + "message": { + "description": "Human-readable, English description of the error", + "type": "string" + }, + "stack": { + "description": "Stack trace indicating where the error occurred.
NOTE: This attribute MAY be included for Development and Test environments. However, it MUST NOT be exposed from OTE nor Production systems", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "code" + ] + }, + "ErrorField": { + "properties": { + "code": { + "description": "Short identifier for the error, suitable for indicating the specific error within client code", + "format": "constant", + "type": "string" + }, + "message": { + "description": "Human-readable, English description of the problem with the contents of the field", + "type": "string" + }, + "path": { + "description": "
    \n
  • JSONPath referring to a field containing an error
  • \nOR\n
  • JSONPath referring to a field that refers to an object containing an error, with more detail in `pathRelated`
  • \n
", + "format": "json-path", + "type": "string" + }, + "pathRelated": { + "description": "JSONPath referring to a field containing an error, which is referenced by `path`", + "format": "json-path", + "type": "string" + } + }, + "required": [ + "path", + "code" + ] + }, + "ErrorFieldDomainContactsValidate": { + "properties": { + "code": { + "description": "Short identifier for the error, suitable for indicating the specific error within client code", + "format": "constant", + "type": "string" + }, + "domains": { + "description": "An array of domain names the error is for. If tlds are specified in the request, `domains` will contain tlds. For example, if `domains` in request is [\"test1.com\", \"test2.uk\", \"net\"], and the field is invalid for com and net, then one of the `fields` in response will have [\"test1.com\", \"net\"] as `domains`", + "items": { + "type": "string" + }, + "type": "array" + }, + "message": { + "description": "Human-readable, English description of the problem with the contents of the field", + "type": "string" + }, + "path": { + "description": "1) JSONPath referring to the field within the data containing an error
or
2) JSONPath referring to an object containing an error", + "format": "json-path", + "type": "string" + }, + "pathRelated": { + "description": "JSONPath referring to the field on the object referenced by `path` containing an error", + "format": "json-path", + "type": "string" + } + }, + "required": [ + "code", + "domains", + "path" + ] + }, + "ErrorLimit": { + "properties": { + "code": { + "description": "Short identifier for the error, suitable for indicating the specific error within client code", + "format": "constant", + "type": "string" + }, + "fields": { + "description": "List of the specific fields, and the errors found with their contents", + "items": { + "$ref": "#/definitions/ErrorField" + }, + "type": "array" + }, + "message": { + "description": "Human-readable, English description of the error", + "type": "string" + }, + "retryAfterSec": { + "description": "Number of seconds to wait before attempting a similar request", + "format": "integer-positive", + "type": "integer" + } + }, + "required": [ + "retryAfterSec", + "code" + ] + }, + "JsonDataType": { + "properties": { + "format": { + "type": "string" + }, + "pattern": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "$ref" + ] + }, + "JsonProperty": { + "properties": { + "defaultValue": { + "type": "string" + }, + "format": { + "type": "string" + }, + "items": { + "items": { + "$ref": "#/definitions/JsonDataType" + }, + "type": "object" + }, + "maxItems": { + "type": "integer" + }, + "maximum": { + "type": "integer" + }, + "minItems": { + "type": "integer" + }, + "minimum": { + "type": "integer" + }, + "pattern": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "type": { + "type": "string" + } + }, + "required": [ + "type", + "$ref", + "required" + ] + }, + "JsonSchema": { + "properties": { + "id": { + "type": "string" + }, + "models": { + "items": { + "$ref": "#/definitions/JsonSchema" + }, + "type": "object" + }, + "properties": { + "items": { + "$ref": "#/definitions/JsonProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "id", + "properties", + "required", + "models" + ] + }, + "LegalAgreement": { + "properties": { + "agreementKey": { + "description": "Unique identifier for the legal agreement", + "type": "string" + }, + "content": { + "description": "Contents of the legal agreement, suitable for embedding", + "type": "string" + }, + "title": { + "description": "Title of the legal agreement", + "type": "string" + }, + "url": { + "description": "URL to a page containing the legal agreement", + "format": "url", + "type": "string" + } + }, + "required": [ + "agreementKey", + "title", + "content" + ] + }, + "Maintenance": { + "additionalProperties": false, + "properties": { + "createdAt": { + "description": "Date and time (UTC) when this maintenance was created", + "type": "string", + "format": "iso-datetime" + }, + "endsAt": { + "description": "Date and time (UTC) when this maintenance will complete", + "type": "string", + "format": "iso-datetime" + }, + "environment": { + "type": "string", + "enum": [ + "OTE", + "PRODUCTION" + ], + "description": "The environment on which the maintenance will be performed
  • OTE - The Operational Testing Environment.
  • PRODUCTION - The Live Production Environment.
" + }, + "maintenanceId" : { + "type": "string", + "description": "The identifier for the system maintenance" + }, + "modifiedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time (UTC) when this maintenance was last modified" + }, + "reason": { + "type": "string", + "enum": [ + "EMERGENCY", + "PLANNED" + ], + "description": "The reason for the maintenance being performed
  • EMERGENCY - Unexpected Emergency maintenance.
  • PLANNED - Planned system maintenance.
" + }, + "startsAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time (UTC) when this maintenance will start" + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "CANCELLED" + ], + "description": "The status of maintenance
  • ACTIVE - The upcoming maintenance is active.
  • CANCELLED - The upcoming maintenance has been cancelled.
" + }, + "summary" : { + "type": "string", + "description": "A brief description of what is being performed" + }, + "tlds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of tlds that are in maintenance. Generally only applies when `type` is REGISTRY" + }, + "type": { + "type": "string", + "enum": [ + "API", + "REGISTRY", + "UI" + ], + "description": "The type of maintenance being performed
  • API - Programmatic Api components.
  • REGISTRY - The underlying Registry providing the tld(s).
  • UI - User Interface components.
" + } + }, + "required": [ + "createdAt", + "endsAt", + "environment", + "maintenanceId", + "modifiedAt", + "reason", + "startsAt", + "status", + "summary", + "type" + ] + }, + "MaintenanceDetail": { + "additionalProperties": false, + "properties": { + "createdAt": { + "description": "Date and time (UTC) when this maintenance was created", + "type": "string", + "format": "iso-datetime" + }, + "endsAt": { + "description": "Date and time (UTC) when this maintenance will complete", + "type": "string", + "format": "iso-datetime" + }, + "environment": { + "type": "string", + "enum": [ + "OTE", + "PRODUCTION" + ], + "description": "The environment on which the maintenance will be performed
  • OTE - The Operational Testing Environment.
  • PRODUCTION - The Live Production Environment.
" + }, + "maintenanceId" : { + "type": "string", + "description": "The identifier for the system maintenance" + }, + "modifiedAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time (UTC) when this maintenance was last modified" + }, + "reason": { + "type": "string", + "enum": [ + "EMERGENCY", + "PLANNED" + ], + "description": "The reason for the maintenance being performed
  • EMERGENCY - Unexpected Emergency maintenance.
  • PLANNED - Planned system maintenance.
" + }, + "startsAt": { + "type": "string", + "format": "iso-datetime", + "description": "Date and time (UTC) when this maintenance will start" + }, + "status": { + "type": "string", + "enum": [ + "ACTIVE", + "CANCELLED" + ], + "description": "The status of maintenance
  • ACTIVE - The upcoming maintenance is active.
  • CANCELLED - The upcoming maintenance has been cancelled.
" + }, + "summary" : { + "type": "string", + "description": "A brief description of what is being performed" + }, + "systems": { + "type": "array", + "items": { + "$ref": "#/definitions/MaintenanceSystem" + }, + "description": "List of systems that are impacted by the maintenance." + }, + "tlds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of tlds that are in maintenance. Generally only applies when `type` is REGISTRY" + }, + "type": { + "type": "string", + "enum": [ + "API", + "REGISTRY", + "UI" + ], + "description": "The type of maintenance being performed
  • API - Programmatic Api components.
  • REGISTRY - The underlying Registry providing the tld(s).
  • UI - User Interface components.
" + } + }, + "required": [ + "createdAt", + "endsAt", + "environment", + "maintenanceId", + "modifiedAt", + "reason", + "startsAt", + "status", + "summary", + "type" + ] + }, + "MaintenanceSystem": { + "additionalProperties": false, + "properties" : { + "name" : { + "type": "string", + "enum": [ + "DOMAIN_CHECKS", + "DOMAIN_MANAGEMENT", + "DOMAIN_REGISTRATION", + "DOMAIN_REGISTRATION_DATA", + "DOMAIN_RESOLUTION", + "RESELLER_ADMIN_PORTAL", + "RESELLER_STOREFRONT" + ], + "description": "The name of the system affected by the maintenance
  • DOMAIN_CHECKS - Refers to domain availability checks.
  • DOMAIN_MANAGEMENT - Refers to domain management options including various update options on the domain, contacts, records, etc.
  • DOMAIN_REGISTRATION - Refers to domain registrations, renewals, transfers.
  • DOMAIN_REGISTRATION_DATA - Refers to RDAP and WHOIS Service queries for domains.
  • DOMAIN_RESOLUTION - Refers to DNS resolution for domains.
  • RESELLER_ADMIN_PORTAL - Refers to Admin portals to manage the reseller account and settings.
  • RESELLER_STOREFRONT - Refers to the Reseller Storefront features (Standard and Custom).
" + }, + "impact": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "DELAYED", + "DOWN", + "NON_AUTHORITATIVE", + "PARTIAL" + ] + }, + "description": "The impact of the maintenance to the system
  • DELAYED - This response generally applies to systems where the request is queued up and processed once the system is back online.
  • DOWN - The system will be entirely offline; errors are expected.
  • NON_AUTHORITATIVE - This response generally applies to DOMAIN_CHECKS and DOMAIN_MANAGEMENT `system` values where a cached answer will be supplied.
  • PARTIAL - The system will experience partial feature outages; some errors are expected.
" + } + }, + "required": [ + "name", + "impact" + ] + }, + "PrivacyPurchase": { + "properties": { + "consent": { + "$ref": "#/definitions/Consent", + "description": "Specify agreement `DNPA`. Required agreements can be retrieved via the GET ./domains/agreements endpoint" + } + }, + "required": [ + "consent" + ] + }, + "RealNameValidation": { + "properties": { + "status": { + "enum": [ + "APPROVED", + "PENDING", + "PENDING_ASSOCIATION_WITH_DOMAIN", + "PENDING_SUBMISSION_TO_VERIFICATION_SERVICE", + "PENDING_VERIFICATION_SERVICE_REPLY", + "PENDING_SUBMISSION_TO_REGISTRY", + "PENDING_REGISTRY_REPLY", + "PENDING_DOMAIN_UPDATE", + "REJECTED" + ], + "type": "string" + } + } + }, + "RenewalDetails": { + "properties": { + "renewable": { + "type": "boolean", + "description": "Whether or not the domain is eligble for renewal based on status" + }, + "price": { + "type": "integer", + "format": "currency-micro-unit", + "description": "Price for the domain renewal excluding taxes or fees" + }, + "currency": { + "type": "string", + "format": "iso-currency-code", + "default": "USD", + "pattern": "^[A-Z][A-Z][A-Z]$", + "description": "Currency in which the `price` is listed" + } + }, + "required": [ + "price", + "currency" + ] + }, + "TldSummary": { + "properties": { + "name": { + "description": "Name of the top-level domain", + "format": "tld", + "type": "string" + }, + "type": { + "default": "GENERIC", + "description": "Type of the top-level domain", + "enum": [ + "COUNTRY_CODE", + "GENERIC" + ], + "type": "string" + } + }, + "required": [ + "name", + "type" + ] + }, + "UsageMonthly" : { + "additionalProperties": false, + "properties" : { + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/UsageMonthlyDetail" + }, + "description": "List of total request counts per endpoint." + }, + "quota": { + "type": "integer", + "description": "The total number of allowed requests in the month. See https://developer.godaddy.com/getstarted for more information on api quotas and access limits." + }, + "total": { + "type": "integer", + "description": "The total number of requests in the month." + }, + "yyyymm": { + "type": "string", + "description": "The year/month timeframe for the request counts (in the format yyyy-mm)" + } + }, + "required": [ + "total", + "yyyymm" + ] + }, + "UsageMonthlyDetail" : { + "additionalProperties": false, + "properties" : { + "path": { + "type": "string", + "description": "The api path for the requests" + }, + "total": { + "type": "integer", + "description": "The total number of requests in the month for the request path." + } + }, + "required": [ + "path", + "total" + ] + }, + "VerificationDomainName": { + "properties": { + "status": { + "description": "Status of the domain name verification", + "enum": [ + "APPROVED", + "PENDING", + "REJECTED", + "UNABLE_TO_RETRIEVE_STATUS" + ], + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "VerificationRealName": { + "properties": { + "status": { + "description": "Status of the real name verification
  • APPROVED - All is well
  • PENDING - Real name verification is working its way through the workflow
  • REJECTED_DOCUMENT_OUTDATED - Local government verification shows there is a newer version of your document. Upload the latest version of the document and retry real name verification
  • REJECTED_EXPIRED_BUSINESS_LICENSE - Business license is expired
  • REJECTED_EXPIRED_ORGANIZATION_CODE - Organization code certificate number has expired
  • REJECTED_ILLEGIBLE_DOCUMENT_NAME - There isn’t a clear name on your uploaded document, please upload a different document to retry real name verification
  • REJECTED_ILLEGIBLE_IDENTIFICATION - Registrant identification is not clear. Upload a better image to retry
  • REJECTED_INCOMPLETE_IDENTIFICATION - Registrant identification is incomplete
  • REJECTED_INCOMPLETE_REGISTRATION_LETTER - Registration letter is incomplete
  • REJECTED_INCONSISTENT_IDENTITY_CARD - Provided identity card is inconsistent with the identity card on record
  • REJECTED_INCONSISTENT_ORGANIZATION_CODE - Provided organization information is inconsistent with the results obtained using the submitted organization code
  • REJECTED_INCONSISTENT_REGISTRANT_NAME - Name on the registrant identification does not match the name in the system
  • REJECTED_INVALID_BUSINESS_LICENSE_OR_ORGANIZATION_CODE - Your document contains an invalid business license or organization code certificate number
  • REJECTED_INVALID_DOCUMENT - Document is invalid. Please upload another document to retry real name verification
  • REJECTED_MISMATCH_BUSINESS_ID - Business id does not match the business id in the document
  • REJECTED_MISMATCH_BUSINESS_NAME - Business name does not match the business name in the document
  • REJECTED_MISMATCH_DOCUMENT_ID - Document id does not match the id in the document
  • REJECTED_MISMATCH_DOCUMENT_NAME - Document name does not match the name in the document
  • REJECTED_MISMATCH_DOCUMENT_TYPE - Document type does not match the document
  • REJECTED_MISMATCH_REGISTRANT_INFO - The information provided for the registrant does not match the document
  • REJECTED_MISMATCH_REGISTRANT_LOCALITY - Registrant region is overseas, but a local identity document was provided
  • REJECTED_MISMATCH_REGISTRANT_NAME - Registrant name has been changed, so the request must be resubmitted
  • REJECTED_UNABLE_TO_OPEN - Registrant identification could not be opened. Please upload the document again to retry real name verification
  • REJECTED_UNABLE_TO_VERIFY - Unable to initiate verification. Please upload the document again to retry real name verification
  • REJECTED_UNKNOWN_ERROR - Document was rejected due to an unknown error. For more information, contact customer support
  • UNABLE_TO_RETRIEVE_STATUS - Unable to retrieve status for the real name verification process. Retry, if this status persists, contact customer support
", + "enum": [ + "APPROVED", + "PENDING", + "REJECTED_DOCUMENT_OUTDATED", + "REJECTED_EXPIRED_BUSINESS_LICENSE", + "REJECTED_EXPIRED_ORGANIZATION_CODE", + "REJECTED_ILLEGIBLE_DOCUMENT_NAME", + "REJECTED_ILLEGIBLE_IDENTIFICATION", + "REJECTED_INCOMPLETE_IDENTIFICATION", + "REJECTED_INCOMPLETE_REGISTRATION_LETTER", + "REJECTED_INCONSISTENT_IDENTITY_CARD", + "REJECTED_INCONSISTENT_ORGANIZATION_CODE", + "REJECTED_INCONSISTENT_REGISTRANT_NAME", + "REJECTED_INVALID_BUSINESS_LICENSE_OR_ORGANIZATION_CODE", + "REJECTED_INVALID_DOCUMENT", + "REJECTED_MISMATCH_BUSINESS_ID", + "REJECTED_MISMATCH_BUSINESS_NAME", + "REJECTED_MISMATCH_DOCUMENT_ID", + "REJECTED_MISMATCH_DOCUMENT_NAME", + "REJECTED_MISMATCH_DOCUMENT_TYPE", + "REJECTED_MISMATCH_REGISTRANT_INFO", + "REJECTED_MISMATCH_REGISTRANT_LOCALITY", + "REJECTED_MISMATCH_REGISTRANT_NAME", + "REJECTED_UNABLE_TO_OPEN", + "REJECTED_UNABLE_TO_VERIFY", + "REJECTED_UNKNOWN_ERROR", + "UNABLE_TO_RETRIEVE_STATUS" + ], + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "VerificationsDomain": { + "properties": { + "domainName": { + "$ref": "#/definitions/VerificationDomainName", + "description": "Verification of domain name against a prohibited list maintained by the government" + }, + "realName": { + "$ref": "#/definitions/VerificationRealName", + "description": "Verification of identity by comparing registration data against government issued documents" + } + } + }, + "VerificationsDomainV2": { + "additionalProperties": false, + "properties": { + "icann": { + "type": "string", + "enum": [ + "COMPLETED", + "PENDING", + "UNABLE_TO_RETRIEVE_STATUS" + ], + "description": "Status of the Icann verification of domain registrant contact by completing email and/or phone verification
  • COMPLETED - Icann verification has been completed.
  • PENDING - Icann verification has not been completed.
  • UNABLE_TO_RETRIEVE_STATUS - Icann verification not supported for specified TLD.
" + }, + "realName": { + "type": "string", + "enum": [ + "APPROVED", + "PENDING", + "REJECTED_DOCUMENT_OUTDATED", + "REJECTED_EXPIRED_BUSINESS_LICENSE", + "REJECTED_EXPIRED_ORGANIZATION_CODE", + "REJECTED_ILLEGIBLE_DOCUMENT_NAME", + "REJECTED_ILLEGIBLE_IDENTIFICATION", + "REJECTED_INCOMPLETE_IDENTIFICATION", + "REJECTED_INCOMPLETE_REGISTRATION_LETTER", + "REJECTED_INCONSISTENT_IDENTITY_CARD", + "REJECTED_INCONSISTENT_ORGANIZATION_CODE", + "REJECTED_INCONSISTENT_REGISTRANT_NAME", + "REJECTED_INVALID_BUSINESS_LICENSE_OR_ORGANIZATION_CODE", + "REJECTED_INVALID_DOCUMENT", + "REJECTED_MISMATCH_BUSINESS_ID", + "REJECTED_MISMATCH_BUSINESS_NAME", + "REJECTED_MISMATCH_DOCUMENT_ID", + "REJECTED_MISMATCH_DOCUMENT_NAME", + "REJECTED_MISMATCH_DOCUMENT_TYPE", + "REJECTED_MISMATCH_REGISTRANT_INFO", + "REJECTED_MISMATCH_REGISTRANT_LOCALITY", + "REJECTED_MISMATCH_REGISTRANT_NAME", + "REJECTED_UNABLE_TO_OPEN", + "REJECTED_UNABLE_TO_VERIFY", + "REJECTED_UNKNOWN_ERROR", + "UNABLE_TO_RETRIEVE_STATUS" + ], + "description": "Status of the real name verification of an identity by comparing registration data against government issued documents
  • APPROVED - All is well
  • PENDING - Real name verification is working its way through the workflow
  • REJECTED_DOCUMENT_OUTDATED - Local government verification shows there is a newer version of your document. Upload the latest version of the document and retry real name verification
  • REJECTED_EXPIRED_BUSINESS_LICENSE - Business license is expired
  • REJECTED_EXPIRED_ORGANIZATION_CODE - Organization code certificate number has expired
  • REJECTED_ILLEGIBLE_DOCUMENT_NAME - There isn’t a clear name on your uploaded document, please upload a different document to retry real name verification
  • REJECTED_ILLEGIBLE_IDENTIFICATION - Registrant identification is not clear. Upload a better image to retry
  • REJECTED_INCOMPLETE_IDENTIFICATION - Registrant identification is incomplete
  • REJECTED_INCOMPLETE_REGISTRATION_LETTER - Registration letter is incomplete
  • REJECTED_INCONSISTENT_IDENTITY_CARD - Provided identity card is inconsistent with the identity card on record
  • REJECTED_INCONSISTENT_ORGANIZATION_CODE - Provided organization information is inconsistent with the results obtained using the submitted organization code
  • REJECTED_INCONSISTENT_REGISTRANT_NAME - Name on the registrant identification does not match the name in the system
  • REJECTED_INVALID_BUSINESS_LICENSE_OR_ORGANIZATION_CODE - Your document contains an invalid business license or organization code certificate number
  • REJECTED_INVALID_DOCUMENT - Document is invalid. Please upload another document to retry real name verification
  • REJECTED_MISMATCH_BUSINESS_ID - Business id does not match the business id in the document
  • REJECTED_MISMATCH_BUSINESS_NAME - Business name does not match the business name in the document
  • REJECTED_MISMATCH_DOCUMENT_ID - Document id does not match the id in the document
  • REJECTED_MISMATCH_DOCUMENT_NAME - Document name does not match the name in the document
  • REJECTED_MISMATCH_DOCUMENT_TYPE - Document type does not match the document
  • REJECTED_MISMATCH_REGISTRANT_INFO - The information provided for the registrant does not match the document
  • REJECTED_MISMATCH_REGISTRANT_LOCALITY - Registrant region is overseas, but a local identity document was provided
  • REJECTED_MISMATCH_REGISTRANT_NAME - Registrant name has been changed, so the request must be resubmitted
  • REJECTED_UNABLE_TO_OPEN - Registrant identification could not be opened. Please upload the document again to retry real name verification
  • REJECTED_UNABLE_TO_VERIFY - Unable to initiate verification. Please upload the document again to retry real name verification
  • REJECTED_UNKNOWN_ERROR - Document was rejected due to an unknown error. For more information, contact customer support
  • UNABLE_TO_RETRIEVE_STATUS - Unable to retrieve status for the real name verification process. Retry, if this status persists, contact customer support
" + }, + "domainName": { + "type": "string", + "enum": [ + "APPROVED", + "PENDING", + "REJECTED", + "UNABLE_TO_RETRIEVE_STATUS" + ], + "description": "Status of the verification of the domain name against a prohibited list maintained by the government" + } + } + }, + "domain": { + "properties": { + "id": { + "format": "double", + "type": "number" + } + } + } + } +} diff --git a/rust/domains-client/scripts/regenerate-spec.sh b/rust/domains-client/scripts/regenerate-spec.sh new file mode 100755 index 0000000..9cc6373 --- /dev/null +++ b/rust/domains-client/scripts/regenerate-spec.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Regenerate the trimmed OpenAPI 3.0 spec the domains-client build consumes. +# +# Pipeline: +# 1. Download the upstream GoDaddy Domains API spec (Swagger 2.0). +# 2. Trim to the GET /v1/domains/available + GET /v1/domains/suggest operations +# and the transitive closure of definitions they reference. +# 3. Convert Swagger 2.0 -> OpenAPI 3.0 with `swagger2openapi` (Node, via npx). +# +# Run this ONLY when the upstream spec changes; the committed domains.oas3.json +# is what the crate's build.rs feeds to progenitor (the build never hits the +# network). Requires: curl, python3, and Node/npx (for swagger2openapi). +set -euo pipefail + +here="$(cd "$(dirname "$0")/.." && pwd)" # domains-client/ +openapi_dir="$here/openapi" +v2="$openapi_dir/swagger_domains.v2.json" +trimmed_v2="$openapi_dir/.swagger_domains.trimmed.v2.json" +oas3="$openapi_dir/domains.oas3.json" + +echo "==> Downloading upstream Swagger 2.0 spec" +curl -fsSL "https://developer.godaddy.com/swagger/swagger_domains.json" -o "$v2" + +echo "==> Trimming to available + suggest (GET) and their definition closure" +python3 - "$v2" "$trimmed_v2" <<'PY' +import json, sys + +src, dst = sys.argv[1], sys.argv[2] +d = json.load(open(src)) + +KEEP = ["/v1/domains/available", "/v1/domains/suggest"] +OP_IDS = {"/v1/domains/available": "available", "/v1/domains/suggest": "suggest"} + +paths = {} +for p in KEEP: + get = d["paths"][p]["get"] + get["operationId"] = OP_IDS[p] + get["produces"] = ["application/json"] # drop xml/js variants + # Keep only the 2xx response: the client needs the success type; non-2xx + # surface via HTTP status + body. (Also avoids progenitor's single-error-type + # constraint when the spec mixes Error + ErrorLimit across 4xx/5xx.) + get["responses"] = { + code: resp for code, resp in get.get("responses", {}).items() + if str(code).startswith("2") + } + paths[p] = {"get": get} + +def dedup_enums(obj): + # typify cannot build a Rust enum from case-only duplicates + # (e.g. ["FAST","FULL","fast","full"]); keep the first per case-fold. + if isinstance(obj, dict): + e = obj.get("enum") + if isinstance(e, list): + seen, uniq = set(), [] + for v in e: + key = v.lower() if isinstance(v, str) else v + if key not in seen: + seen.add(key) + uniq.append(v) + obj["enum"] = uniq + for v in obj.values(): + dedup_enums(v) + elif isinstance(obj, list): + for v in obj: + dedup_enums(v) + +def refs(obj): + out = set() + if isinstance(obj, dict): + for k, v in obj.items(): + if k == "$ref" and isinstance(v, str): + out.add(v.split("/")[-1]) + else: + out |= refs(v) + elif isinstance(obj, list): + for v in obj: + out |= refs(v) + return out + +dedup_enums(paths) + +needed, stack = set(), list(refs(paths)) +while stack: + n = stack.pop() + if n in needed: + continue + needed.add(n) + stack += [r for r in refs(d["definitions"].get(n, {})) if r not in needed] + +out = { + "swagger": "2.0", + "info": {"title": "GoDaddy Domains API (availability subset)", + "version": d.get("info", {}).get("version", "1.0.0")}, + "host": d.get("host", "api.ote-godaddy.com"), + "basePath": d.get("basePath", "/"), + "schemes": d.get("schemes", ["https"]), + "paths": paths, + "definitions": {n: d["definitions"][n] for n in sorted(needed) if n in d["definitions"]}, +} +json.dump(out, open(dst, "w"), indent=2) +print(f" kept {len(paths)} paths, {len(out['definitions'])} definitions") +PY + +echo "==> Converting Swagger 2.0 -> OpenAPI 3.0" +npx -y swagger2openapi "$trimmed_v2" -o "$oas3" +rm -f "$trimmed_v2" +echo "==> Wrote $oas3" diff --git a/rust/domains-client/src/lib.rs b/rust/domains-client/src/lib.rs new file mode 100644 index 0000000..5c986d4 --- /dev/null +++ b/rust/domains-client/src/lib.rs @@ -0,0 +1,52 @@ +//! GoDaddy Domains API client (availability + suggest). +//! +//! The contents of this crate are **generated** by `progenitor` at build time +//! from the vendored OpenAPI 3.0 spec (`openapi/domains.oas3.json`). Construct +//! [`Client`] with [`Client::new_with_client`] to supply a pre-authenticated +//! `reqwest::Client` (the CLI sets the `Authorization: sso-key …`/Bearer header +//! itself). See `scripts/regenerate-spec.sh` to refresh the spec. +//! +//! Generated code is exempt from the workspace's strict style lints. +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(rustdoc::all)] + +include!(concat!(env!("OUT_DIR"), "/codegen.rs")); + +/// Error building the authenticated HTTP client. +#[derive(Debug, thiserror::Error)] +pub enum BuildError { + #[error("invalid header value: {0}")] + Header(#[from] reqwest::header::InvalidHeaderValue), + #[error("failed to build HTTP client: {0}")] + Http(#[from] reqwest::Error), +} + +/// Build a [`Client`] whose every request carries a pre-set `Authorization` +/// header and `x-request-id`. +/// +/// `authorization` is the full header value the domain endpoints expect — e.g. +/// `"sso-key :"` (the usual path) or `"Bearer "`. Keeping +/// the `reqwest::Client` construction here means callers never name reqwest's +/// types, so the main crate is unaffected by this crate's reqwest version. +pub fn client_with_auth( + base_url: &str, + authorization: &str, + user_agent: &str, + request_id: &str, +) -> Result { + use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue}; + + let mut headers = HeaderMap::new(); + headers.insert(AUTHORIZATION, HeaderValue::from_str(authorization)?); + headers.insert( + HeaderName::from_static("x-request-id"), + HeaderValue::from_str(request_id)?, + ); + let http = reqwest::Client::builder() + .user_agent(user_agent) + .default_headers(headers) + .build()?; + Ok(Client::new_with_client(base_url, http)) +} diff --git a/rust/examples/environments.toml b/rust/examples/environments.toml new file mode 100644 index 0000000..29f278d --- /dev/null +++ b/rust/examples/environments.toml @@ -0,0 +1,35 @@ +# gddy environments — copy/paste template for internal developers. +# +# Copy this file to your gddy config dir (only the entries you need): +# Linux/XDG : ~/.config/gddy/environments.toml (or $XDG_CONFIG_HOME/gddy/…) +# macOS : ~/Library/Application Support/gddy/environments.toml +# Windows : %APPDATA%\gddy\environments.toml +# +# Built-in `ote` and `prod` resolve without this file; you only need entries for +# the internal `dev`/`test` environments. Anything here can be overridden at +# runtime by `_API_URL`, `_DOMAINS_API_URL`, `_API_KEY`, +# `_API_SECRET` env vars (e.g. DEV_API_KEY), or `--api-key/--api-secret`. +# +# ── Auth ─────────────────────────────────────────────────────────────────── +# The `domain` commands hit the GoDaddy Domains API, which is OAuth-enabled on +# all environments (run `gddy auth login --env ` first). So no sso-key is +# needed for the domain commands. If you do need an sso-key for some other, +# not-yet-OAuth-enabled endpoint, add `api_key`/`api_secret` to the env (or pass +# `--api-key`/`--api-secret`); the CLI then sends `Authorization: sso-key …` for +# `domain:*` commands instead of the OAuth bearer. +# +# `client_id`: a custom env needs one for PKCE login; the per-env public OAuth +# client ids are filled in below. Override via DEV_OAUTH_CLIENT_ID / +# TEST_OAUTH_CLIENT_ID if needed. auth/token URLs derive from api_url +# (…/v2/oauth2/authorize, …/v2/oauth2/token). `domains_api_url` defaults to +# `api_url`, so a single host per env is enough. + +# Internal dev gateway. +[environments.dev] +api_url = "https://api.dev-godaddy.com" +client_id = "94488449-5769-4ecf-8bf4-9f8aa83859a3" + +# Internal test gateway. +[environments.test] +api_url = "https://api.test-godaddy.com" +client_id = "e710d8b9-f4e5-4178-b1bf-98dfcd15d4ed" diff --git a/rust/src/auth.rs b/rust/src/auth.rs index cf823ff..9cfe8b3 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -37,6 +37,7 @@ impl GoDaddyAuthProvider { } fn build_provider(env: &ResolvedEnv) -> PkceAuthProvider { + log_resolved_oauth(env); PkceAuthProvider::new( env.name.clone(), env.auth_url.clone(), @@ -48,6 +49,40 @@ fn build_provider(env: &ResolvedEnv) -> PkceAuthProvider { .with_redirect_uri(environments::REDIRECT_URI) } +/// Emit (at debug level) the OAuth parameters that will be used for login and +/// the code→token exchange. cli-engine builds the actual token request, so this +/// is the CLI's single point of visibility into the client id / endpoints that +/// drive an `invalid_client`/`invalid_grant` failure. +/// +/// It mirrors cli-engine's `_OAUTH_*` env-var overrides +/// (`PkceAuthProvider::effective_*`) so the logged values are what's actually +/// sent — and flags when a value comes from an env var rather than config, which +/// is the usual cause of a "wrong client id". No secrets are logged (the OAuth +/// client id is a public identifier; tokens never pass through here). +/// +/// Enable with `RUST_LOG=gddy=debug` (e.g. `RUST_LOG=gddy=debug gddy domain +/// available example.com --env dev`). +fn log_resolved_oauth(env: &ResolvedEnv) { + if !tracing::enabled!(tracing::Level::DEBUG) { + return; + } + let prefix = env.name.to_uppercase().replace('-', "_"); + let override_var = |suffix: &str| std::env::var(format!("{prefix}_OAUTH_{suffix}")).ok(); + let client_id_ovr = override_var("CLIENT_ID"); + let auth_url_ovr = override_var("AUTH_URL"); + let token_url_ovr = override_var("TOKEN_URL"); + tracing::debug!( + env = %env.name, + client_id = %client_id_ovr.as_deref().unwrap_or(&env.client_id), + client_id_from_env_var = client_id_ovr.is_some(), + auth_url = %auth_url_ovr.as_deref().unwrap_or(&env.auth_url), + token_url = %token_url_ovr.as_deref().unwrap_or(&env.token_url), + token_url_from_env_var = token_url_ovr.is_some(), + redirect_uri = environments::REDIRECT_URI, + "resolved OAuth client for login/token exchange" + ); +} + #[async_trait] impl AuthProvider for GoDaddyAuthProvider { fn name(&self) -> &str { @@ -93,3 +128,177 @@ impl AuthProvider for GoDaddyAuthProvider { Ok(envs) } } + +/// Stored in [`Credential::provider`] for the sso-key bypass path, so the domain +/// client selects the `sso-key` Authorization scheme instead of Bearer. +pub const SSO_KEY_PROVIDER: &str = "sso-key"; + +/// Process-global sso-key supplied via the `--api-key`/`--api-secret` flags. +/// +/// The flags are bridged here (rather than to `_API_KEY` env vars, which +/// edition-2024 makes `unsafe` to set) by [`set_api_key_override`] during flag +/// application; the composite provider consults it before the per-env config. +static API_KEY_OVERRIDE: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + +fn api_key_override_cell() -> &'static std::sync::Mutex> { + API_KEY_OVERRIDE.get_or_init(|| std::sync::Mutex::new(None)) +} + +/// Record an sso-key from the `--api-key`/`--api-secret` flags. Highest +/// precedence for domain-command auth (beats `_API_KEY` and config). +pub fn set_api_key_override(key: String, secret: String) { + if let Ok(mut guard) = api_key_override_cell().lock() { + *guard = Some((key, secret)); + } +} + +fn api_key_override() -> Option<(String, String)> { + api_key_override_cell().lock().ok().and_then(|g| g.clone()) +} + +/// Auth provider that composes [`GoDaddyAuthProvider`] (OAuth/PKCE) but, for +/// `domain:*` commands whose target environment has an sso-key configured, +/// returns that key instead. +/// +/// The GoDaddy Domains API endpoints authenticate with +/// `Authorization: sso-key :`, not OAuth. Every other command — and +/// any domain command in an environment without a configured key — continues to +/// use OAuth (including scope step-up). Scoping the bypass to `domain:*` keeps it +/// from affecting unrelated commands. +#[derive(Debug, Default)] +pub struct CompositeAuthProvider { + oauth: GoDaddyAuthProvider, +} + +impl CompositeAuthProvider { + pub fn new() -> Self { + Self { + oauth: GoDaddyAuthProvider::new(), + } + } + + /// Build an sso-key credential, if this is a `domain:*` command and both a + /// key and secret are present. Pure (no process/config access) for testing. + fn sso_key_credential_from( + env: &str, + command: &str, + key: Option<&str>, + secret: Option<&str>, + ) -> Option { + if !command.starts_with("domain:") { + return None; + } + let key = key.map(str::trim).filter(|s| !s.is_empty())?; + let secret = secret.map(str::trim).filter(|s| !s.is_empty())?; + Some(Credential { + token: format!("{key}:{secret}"), + provider: SSO_KEY_PROVIDER.to_owned(), + env: env.to_owned(), + ..Default::default() + }) + } + + /// Resolve the sso-key for a domain command (flag override → per-env config) + /// and turn it into a credential. + fn sso_key_credential(env: &str, command: &str) -> Option { + if let Some((key, secret)) = api_key_override() { + return Self::sso_key_credential_from(env, command, Some(&key), Some(&secret)); + } + let domains = environments::resolve_domains(env).ok()?; + Self::sso_key_credential_from( + env, + command, + domains.api_key.as_deref(), + domains.api_secret.as_deref(), + ) + } +} + +#[async_trait] +impl AuthProvider for CompositeAuthProvider { + fn name(&self) -> &str { + self.oauth.name() + } + + async fn get_credential(&self, env: &str, command: &str, tier: &str) -> Result { + if let Some(cred) = Self::sso_key_credential(env, command) { + return Ok(cred); + } + self.oauth.get_credential(env, command, tier).await + } + + async fn get_credential_for(&self, req: &CredentialRequest<'_>) -> Result { + if let Some(cred) = Self::sso_key_credential(req.env, req.command) { + return Ok(cred); + } + self.oauth.get_credential_for(req).await + } + + async fn status(&self, env: &str) -> Result { + self.oauth.status(env).await + } + + async fn logout(&self, env: &str) -> Result<()> { + self.oauth.logout(env).await + } + + async fn list_environments(&self) -> Result> { + self.oauth.list_environments().await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sso_key_only_for_domain_commands_with_key_and_secret() { + // domain command + both key/secret -> sso-key credential. + let cred = CompositeAuthProvider::sso_key_credential_from( + "ote", + "domain:available", + Some("KEY"), + Some("SECRET"), + ) + .expect("sso-key credential"); + assert_eq!(cred.token, "KEY:SECRET"); + assert_eq!(cred.provider, SSO_KEY_PROVIDER); + assert_eq!(cred.env, "ote"); + } + + #[test] + fn no_sso_key_for_non_domain_commands() { + assert!( + CompositeAuthProvider::sso_key_credential_from( + "ote", + "application:list", + Some("KEY"), + Some("SECRET"), + ) + .is_none() + ); + } + + #[test] + fn no_sso_key_when_key_or_secret_missing_or_blank() { + assert!( + CompositeAuthProvider::sso_key_credential_from( + "ote", + "domain:suggest", + Some("KEY"), + None + ) + .is_none() + ); + assert!( + CompositeAuthProvider::sso_key_credential_from( + "ote", + "domain:suggest", + Some(" "), + Some("SECRET"), + ) + .is_none() + ); + } +} diff --git a/rust/src/domain/mod.rs b/rust/src/domain/mod.rs new file mode 100644 index 0000000..78bee46 --- /dev/null +++ b/rust/src/domain/mod.rs @@ -0,0 +1,265 @@ +//! `gddy domain` — domain availability and suggestions. +//! +//! These endpoints (the public GoDaddy Domains API) authenticate with an +//! sso-key, not OAuth. The HTTP layer is the typed, spec-generated +//! [`domains_client`] crate; auth scheme selection (sso-key vs Bearer) is driven +//! by the credential the [`CompositeAuthProvider`](crate::auth::CompositeAuthProvider) +//! returns for `domain:*` commands. + +use cli_engine::{ + CliCoreError, CommandContext, CommandResult, CommandSpec, GroupSpec, Module, NextAction, + NextActionParam, Result, RuntimeCommandSpec, RuntimeGroupSpec, Tier, +}; +use serde_json::json; + +use crate::{auth::SSO_KEY_PROVIDER, environments}; + +const USER_AGENT: &str = concat!("godaddy-cli/", env!("CARGO_PKG_VERSION")); + +/// OAuth scope the domain availability + suggest endpoints require. +/// +/// Source of truth (undocumented in the published Swagger spec): the GoDaddy +/// domains OAuth scope → endpoint whitelist in `gdcorp-domains/api-domain-data`, +/// `api/oauthscopewhitelist.json`. Both `GET /v1/domains/available` and +/// `GET /v1/domains/suggest` are listed under `domains.domain:read`: +/// +/// +/// Declared on the commands via [`CommandSpec::with_scopes`] so cli-engine's +/// OAuth scope step-up mints a token carrying it. Ignored on the sso-key path +/// (sso-key auth is unscoped). +const DOMAINS_READ_SCOPE: &str = "domains.domain:read"; + +fn map_env_err(e: environments::EnvError) -> CliCoreError { + CliCoreError::message(e.to_string()) +} + +/// Convert a currency micro-unit amount (e.g. 11_990_000) to a decimal string +/// ("11.99"). Domain prices are returned in micro-units. +fn format_price(micros: Option) -> Option { + micros.map(|m| format!("{}.{:02}", m / 1_000_000, (m.abs() % 1_000_000) / 10_000)) +} + +/// Build a Domains API client for the active environment, choosing the auth +/// scheme from the resolved credential (sso-key for the bypass path, else +/// Bearer). The credential is resolved through the registered composite provider. +async fn make_client(ctx: &CommandContext) -> Result { + let env = ctx.middleware.env.clone(); + let domains = environments::resolve_domains(&env).map_err(map_env_err)?; + let cred = ctx.credential().await?; + let authorization = if cred.provider == SSO_KEY_PROVIDER { + format!("sso-key {}", cred.token) + } else { + format!("Bearer {}", cred.token) + }; + let request_id = uuid::Uuid::new_v4().to_string(); + domains_client::client_with_auth(&domains.base_url, &authorization, USER_AGENT, &request_id) + .map_err(|e| CliCoreError::message(format!("failed to build domains client: {e}"))) +} + +fn string_list(ctx: &CommandContext, key: &str) -> Vec { + match ctx.args.get(key) { + Some(serde_json::Value::Array(arr)) => arr + .iter() + .filter_map(|v| v.as_str().map(str::to_owned)) + .collect(), + Some(serde_json::Value::String(s)) => vec![s.clone()], + _ => Vec::new(), + } +} + +pub fn module() -> Module { + Module::new("Domains", |_ctx| { + RuntimeGroupSpec::new(GroupSpec::new( + "domain", + "Domain availability and suggestions", + )) + .with_command(RuntimeCommandSpec::new_with_context( + CommandSpec::new("available", "Check whether a domain is available") + .with_system("domain") + .with_tier(Tier::Read) + .with_scopes(&[DOMAINS_READ_SCOPE]) + .with_arg( + clap::Arg::new("domain") + .value_name("DOMAIN") + .required(true) + .help("Domain name to check (e.g. example.com)"), + ) + .with_arg( + clap::Arg::new("check-type") + .long("check-type") + .value_name("TYPE") + .value_parser(["fast", "full"]) + .help("Optimize for speed (fast) or accuracy (full)"), + ) + .with_arg( + clap::Arg::new("for-transfer") + .long("for-transfer") + .action(clap::ArgAction::SetTrue) + .help("Also include domains available for transfer"), + ), + |ctx| async move { + let domain = ctx + .args + .get("domain") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_owned(); + let check_type = match ctx.args.get("check-type").and_then(|v| v.as_str()) { + Some(s) => Some( + domains_client::types::AvailableCheckType::try_from( + s.to_uppercase().as_str(), + ) + .map_err(|_| { + CliCoreError::message(format!( + "invalid --check-type {s:?}; expected fast|full" + )) + })?, + ), + None => None, + }; + let for_transfer = ctx + .args + .get("for-transfer") + .and_then(|v| v.as_bool()) + .filter(|&b| b); + + let client = make_client(&ctx).await?; + let resp = client + .available(check_type, &domain, for_transfer) + .await + .map_err(|e| { + CliCoreError::message(format!("domain availability check failed: {e}")) + })?; + let body = resp.into_inner(); + + let mut result = json!({ + "domain": body.domain, + "available": body.available, + "definitive": body.definitive, + }); + if let Some(price) = format_price(body.price) { + result["price"] = json!(price); + result["currency"] = json!(body.currency); + } + if let Some(renewal) = format_price(body.renewal_price) { + result["renewalPrice"] = json!(renewal); + } + if let Some(period) = body.period { + result["period"] = json!(period); + } + + let cmd = CommandResult::new(result); + // If it's taken, point at suggestions for the same seed. + if body.available { + Ok(cmd) + } else { + Ok(cmd.with_next_actions(vec![ + NextAction::new( + "domain suggest ", + "Find alternative available domains", + ) + .with_param("query", NextActionParam::required()), + ])) + } + }, + )) + .with_command(RuntimeCommandSpec::new_with_context( + CommandSpec::new("suggest", "Suggest available domains for a query") + .with_system("domain") + .with_tier(Tier::Read) + .with_scopes(&[DOMAINS_READ_SCOPE]) + .with_arg( + clap::Arg::new("query") + .value_name("QUERY") + .required(true) + .help("Seed domain or keywords to base suggestions on"), + ) + .with_arg( + clap::Arg::new("tlds") + .long("tlds") + .value_name("TLD") + .action(clap::ArgAction::Append) + .help("Limit suggestions to these TLDs (repeatable)"), + ) + .with_arg( + clap::Arg::new("limit") + .long("limit") + .value_name("N") + .value_parser(clap::value_parser!(i64).range(1..)) + .help("Maximum number of suggestions to return"), + ) + .with_arg( + clap::Arg::new("country") + .long("country") + .value_name("CC") + .help("Two-letter ISO country hint (e.g. US)"), + ) + .with_arg( + clap::Arg::new("city") + .long("city") + .value_name("CITY") + .help("City hint for the target region"), + ), + |ctx| async move { + let query = ctx + .args + .get("query") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_owned(); + let tlds = string_list(&ctx, "tlds"); + let tlds = (!tlds.is_empty()).then_some(tlds); + let limit = ctx.args.get("limit").and_then(|v| v.as_i64()); + let city = ctx.args.get("city").and_then(|v| v.as_str()); + let country = match ctx.args.get("country").and_then(|v| v.as_str()) { + Some(c) => Some( + domains_client::types::SuggestCountry::try_from(c.to_uppercase().as_str()) + .map_err(|_| { + CliCoreError::message(format!("invalid --country {c:?}")) + })?, + ), + None => None, + }; + + let client = make_client(&ctx).await?; + let resp = client + .suggest( + city, + country, + None, + None, + limit, + Some(&query), + None, + tlds.as_ref(), + None, + None, + ) + .await + .map_err(|e| CliCoreError::message(format!("domain suggestion failed: {e}")))?; + let suggestions: Vec = + resp.into_inner().into_iter().map(|s| s.domain).collect(); + + Ok( + CommandResult::new(json!(suggestions)).with_next_actions(vec![ + NextAction::new("domain available ", "Check a suggested domain") + .with_param("domain", NextActionParam::required()), + ]), + ) + }, + )) + }) +} + +#[cfg(test)] +mod tests { + use super::format_price; + + #[test] + fn formats_micro_units_to_decimal() { + assert_eq!(format_price(Some(11_990_000)).as_deref(), Some("11.99")); + assert_eq!(format_price(Some(1_000_000)).as_deref(), Some("1.00")); + assert_eq!(format_price(Some(20_500_000)).as_deref(), Some("20.50")); + assert_eq!(format_price(None), None); + } +} diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index adf8eaa..e672f49 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -50,12 +50,12 @@ const BUILTINS: &[Builtin] = &[ Builtin { name: "ote", api_url: "https://api.ote-godaddy.com", - client_id: "a502484b-d7b1-4509-aa88-08b391a54c28", + client_id: "91660d79-c909-426c-b5c8-e0f575e8fcd2", }, Builtin { name: "prod", api_url: "https://api.godaddy.com", - client_id: "39489dee-4103-4284-9aab-9f2452142bce", + client_id: "bc87f347-af82-4892-833f-818f54a0e79e", }, ]; @@ -67,6 +67,23 @@ pub struct ResolvedEnv { pub client_id: String, pub auth_url: String, pub token_url: String, + /// Base URL for the domain commands. Some endpoints (e.g. domain + /// availability) live behind a different host than the OAuth/`api_url` + /// service; this defaults to `api_url` when not overridden. + pub domains_api_url: String, + /// sso-key API key/secret for the domain endpoints, which use + /// `Authorization: sso-key :` rather than OAuth. Optional: when + /// absent, domain commands fall back to the OAuth credential. + pub api_key: Option, + pub api_secret: Option, +} + +/// The domain-command view of a resolved environment. +#[derive(Debug, Clone)] +pub struct ResolvedDomains { + pub base_url: String, + pub api_key: Option, + pub api_secret: Option, } /// Schema of the local environments file (see [`environments_path`]). @@ -85,6 +102,14 @@ pub struct EnvEntry { pub auth_url: Option, #[serde(default)] pub token_url: Option, + /// Override base URL for domain commands (defaults to `api_url`). + #[serde(default)] + pub domains_api_url: Option, + /// sso-key credentials for domain endpoints (see [`ResolvedEnv::api_key`]). + #[serde(default)] + pub api_key: Option, + #[serde(default)] + pub api_secret: Option, } #[derive(Debug, thiserror::Error)] @@ -145,6 +170,13 @@ fn clean_url(raw: &str) -> Option { (!host.is_empty()).then(|| trimmed.to_owned()) } +/// Trims a candidate secret/value and returns it only if non-empty. Keeps the +/// key/secret resolution consistent with `clean_url`'s "blank never clobbers". +fn non_empty(raw: &str) -> Option { + let trimmed = raw.trim(); + (!trimmed.is_empty()).then(|| trimmed.to_owned()) +} + /// Path to the local environments config file, if a config dir can be resolved. /// /// Uses `dirs::config_dir()` which honors `XDG_CONFIG_HOME` (→ `~/.config`) on @@ -204,6 +236,9 @@ fn resolve_with( }; let mut auth_url: Option = None; let mut token_url: Option = None; + let mut domains_api_url: Option = None; + let mut api_key: Option = None; + let mut api_secret: Option = None; // Layer 2: local config entry (overrides/defines). An empty/whitespace // api_url is ignored so it can't clobber a built-in default. @@ -218,13 +253,26 @@ fn resolve_with( // empty value is ignored, falling back to the derived endpoints. auth_url = entry.auth_url.as_deref().and_then(clean_url); token_url = entry.token_url.as_deref().and_then(clean_url); + domains_api_url = entry.domains_api_url.as_deref().and_then(clean_url); + api_key = entry.api_key.as_deref().and_then(non_empty); + api_secret = entry.api_secret.as_deref().and_then(non_empty); } - // Layer 3: per-env `_API_URL` override (highest precedence). Empty - // values are ignored (handled by clean_url). - if let Some(url) = var(&format!("{}_API_URL", env_prefix(name))).and_then(|v| clean_url(&v)) { + // Layer 3: per-env `_*` overrides (highest precedence). Empty values + // are ignored (clean_url for URLs, non_empty for the key/secret). + let prefix = env_prefix(name); + if let Some(url) = var(&format!("{prefix}_API_URL")).and_then(|v| clean_url(&v)) { api_url = Some(url); } + if let Some(url) = var(&format!("{prefix}_DOMAINS_API_URL")).and_then(|v| clean_url(&v)) { + domains_api_url = Some(url); + } + if let Some(k) = var(&format!("{prefix}_API_KEY")).and_then(|v| non_empty(&v)) { + api_key = Some(k); + } + if let Some(s) = var(&format!("{prefix}_API_SECRET")).and_then(|v| non_empty(&v)) { + api_secret = Some(s); + } // api_url is already trimmed/normalized by clean_url (and built-ins carry no // trailing slash), so callers concatenating paths never produce `//`. @@ -235,6 +283,8 @@ fn resolve_with( let auth_url = auth_url.unwrap_or_else(|| derive_auth_url(&api_url)); let token_url = token_url.unwrap_or_else(|| derive_token_url(&api_url)); + // Domain endpoints default to the same host as the OAuth/api_url service. + let domains_api_url = domains_api_url.unwrap_or_else(|| api_url.clone()); Ok(ResolvedEnv { name: name.to_owned(), @@ -242,6 +292,9 @@ fn resolve_with( client_id, auth_url, token_url, + domains_api_url, + api_key, + api_secret, }) } @@ -291,6 +344,17 @@ pub fn resolve(name: &str) -> Result { } } +/// Resolve the domain-command view of an environment: the domains base URL and +/// any sso-key credentials. Thin wrapper over [`resolve`]. +pub fn resolve_domains(name: &str) -> Result { + let env = resolve(name)?; + Ok(ResolvedDomains { + base_url: env.domains_api_url, + api_key: env.api_key, + api_secret: env.api_secret, + }) +} + /// The default environment's built-in API base URL. /// /// Infallible last-resort value (unlike [`resolve`], which can fail on a @@ -342,6 +406,9 @@ mod tests { client_id: None, auth_url: None, token_url: None, + domains_api_url: None, + api_key: None, + api_secret: None, } } @@ -354,7 +421,7 @@ mod tests { let file = EnvironmentsFile::default(); let env = resolve_with("prod", &file, no_vars).expect("prod resolves"); assert_eq!(env.api_url, "https://api.godaddy.com"); - assert_eq!(env.client_id, "39489dee-4103-4284-9aab-9f2452142bce"); + assert_eq!(env.client_id, "bc87f347-af82-4892-833f-818f54a0e79e"); assert_eq!(env.auth_url, "https://api.godaddy.com/v2/oauth2/authorize"); assert_eq!(env.token_url, "https://api.godaddy.com/v2/oauth2/token"); } @@ -388,7 +455,7 @@ mod tests { let env = resolve_with("prod", &file, var).expect("prod resolves"); assert_eq!(env.api_url, "https://sandbox.example.test"); // Client id is retained from the built-in. - assert_eq!(env.client_id, "39489dee-4103-4284-9aab-9f2452142bce"); + assert_eq!(env.client_id, "bc87f347-af82-4892-833f-818f54a0e79e"); } #[test] @@ -416,6 +483,9 @@ mod tests { client_id: Some("custom-client".to_owned()), auth_url: Some("https://auth.example.invalid/authorize".to_owned()), token_url: Some("https://auth.example.invalid/token".to_owned()), + domains_api_url: None, + api_key: None, + api_secret: None, }, ); let env = resolve_with("dev", &file, no_vars).expect("dev resolves"); @@ -478,6 +548,9 @@ mod tests { client_id: None, auth_url: Some("auth.example.invalid/authorize".to_owned()), // no scheme token_url: Some(" ".to_owned()), // blank + domains_api_url: None, + api_key: None, + api_secret: None, }, ); let env = resolve_with("dev", &file, no_vars).expect("dev resolves"); @@ -555,6 +628,75 @@ mod tests { assert!(!is_known_with("missing", &file, var)); } + #[test] + fn domains_api_url_defaults_to_api_url() { + let file = EnvironmentsFile::default(); + let env = resolve_with("prod", &file, no_vars).expect("prod resolves"); + assert_eq!(env.domains_api_url, env.api_url); + assert!(env.api_key.is_none() && env.api_secret.is_none()); + } + + #[test] + fn domains_url_and_sso_key_from_local_config() { + let mut file = EnvironmentsFile::default(); + file.environments.insert( + "dev".to_owned(), + EnvEntry { + api_url: "https://dev.example.invalid".to_owned(), + client_id: None, + auth_url: None, + token_url: None, + domains_api_url: Some("https://domains.dev.example.invalid".to_owned()), + api_key: Some("KEY".to_owned()), + api_secret: Some("SECRET".to_owned()), + }, + ); + let env = resolve_with("dev", &file, no_vars).expect("dev resolves"); + assert_eq!(env.domains_api_url, "https://domains.dev.example.invalid"); + assert_eq!(env.api_key.as_deref(), Some("KEY")); + assert_eq!(env.api_secret.as_deref(), Some("SECRET")); + // api_url is unaffected by the domains override. + assert_eq!(env.api_url, "https://dev.example.invalid"); + } + + #[test] + fn sso_key_and_domains_url_env_vars_override_config() { + let mut file = EnvironmentsFile::default(); + file.environments.insert( + "dev".to_owned(), + EnvEntry { + api_url: "https://dev.example.invalid".to_owned(), + client_id: None, + auth_url: None, + token_url: None, + domains_api_url: Some("https://from-file.example.invalid".to_owned()), + api_key: Some("file-key".to_owned()), + api_secret: None, + }, + ); + let var = |k: &str| match k { + "DEV_DOMAINS_API_URL" => Some("https://from-env.example.invalid".to_owned()), + "DEV_API_KEY" => Some("env-key".to_owned()), + "DEV_API_SECRET" => Some("env-secret".to_owned()), + _ => None, + }; + let env = resolve_with("dev", &file, var).expect("dev resolves"); + assert_eq!(env.domains_api_url, "https://from-env.example.invalid"); + assert_eq!(env.api_key.as_deref(), Some("env-key")); // env beats file + assert_eq!(env.api_secret.as_deref(), Some("env-secret")); + } + + #[test] + fn blank_sso_key_env_var_does_not_clobber_config() { + let mut file = EnvironmentsFile::default(); + let mut e = entry("https://dev.example.invalid"); + e.api_key = Some("file-key".to_owned()); + file.environments.insert("dev".to_owned(), e); + let var = |k: &str| (k == "DEV_API_KEY").then(|| " ".to_owned()); + let env = resolve_with("dev", &file, var).expect("dev resolves"); + assert_eq!(env.api_key.as_deref(), Some("file-key")); + } + #[test] fn missing_config_file_is_not_an_error() { // load_file resolves a real path; just assert it does not panic and that diff --git a/rust/src/main.rs b/rust/src/main.rs index 5a17245..a7587bc 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -3,6 +3,7 @@ mod api_explorer; mod application; mod auth; mod config; +mod domain; mod env; mod environments; mod extension; @@ -22,7 +23,7 @@ async fn main() -> ExitCode { .with_writer(std::io::stderr) .init(); - let auth_provider = Arc::new(auth::GoDaddyAuthProvider::new()); + let auth_provider = Arc::new(auth::CompositeAuthProvider::new()); let cli = Cli::new( CliConfig::new("gddy", "GoDaddy developer CLI", "gddy") @@ -40,6 +41,20 @@ async fn main() -> ExitCode { ) .help("Target environment (ote|prod)"), ) + .arg( + Arg::new("api-key") + .long("api-key") + .global(true) + .value_name("KEY") + .help("sso-key API key for domain commands (overrides config/env)"), + ) + .arg( + Arg::new("api-secret") + .long("api-secret") + .global(true) + .value_name("SECRET") + .help("sso-key API secret (used with --api-key)"), + ) })) .with_apply_flags(Arc::new(|matches, mw| { if let Some(env) = matches.get_one::("env") { @@ -53,6 +68,15 @@ async fn main() -> ExitCode { } mw.env = env.clone(); } + // Bridge the sso-key flags to the auth layer (the composite + // provider reads this for `domain:*` commands). Both are required + // together; a lone --api-key/--api-secret is ignored. + if let (Some(key), Some(secret)) = ( + matches.get_one::("api-key"), + matches.get_one::("api-secret"), + ) { + auth::set_api_key_override(key.clone(), secret.clone()); + } Ok(()) })) .with_root_next_actions(Arc::new(|| { @@ -66,6 +90,7 @@ async fn main() -> ExitCode { .with_module(actions_catalog::module()) .with_module(api_explorer::module()) .with_module(application::module()) + .with_module(domain::module()) .with_module(env::module()) .with_module(webhook::module()), ); From 15c7af1d729e61ec9773fff8a8d556eab02cbcab Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 10:57:04 -0700 Subject: [PATCH 02/14] review: enforce sso-key flag pairing, redact secrets in Debug, doc/log fixes Addresses Copilot review on PR #59: - --api-key/--api-secret now require each other (clap `requires`), so a lone flag errors at parse time instead of being silently ignored. - ResolvedEnv/ResolvedDomains/EnvEntry no longer derive Debug; hand-written Debug impls redact the sso-key api_key/api_secret fields to avoid credential leakage via {:?}. - auth debug log adds auth_url_from_env_var for consistency with client_id/token_url. - domain module docs corrected: the endpoints accept sso-key OR OAuth bearer. Co-Authored-By: Claude Opus 4.8 --- rust/src/auth.rs | 1 + rust/src/domain/mod.rs | 11 +++--- rust/src/environments/mod.rs | 66 ++++++++++++++++++++++++++++++++++-- rust/src/main.rs | 7 ++-- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 9cfe8b3..74797ac 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -76,6 +76,7 @@ fn log_resolved_oauth(env: &ResolvedEnv) { client_id = %client_id_ovr.as_deref().unwrap_or(&env.client_id), client_id_from_env_var = client_id_ovr.is_some(), auth_url = %auth_url_ovr.as_deref().unwrap_or(&env.auth_url), + auth_url_from_env_var = auth_url_ovr.is_some(), token_url = %token_url_ovr.as_deref().unwrap_or(&env.token_url), token_url_from_env_var = token_url_ovr.is_some(), redirect_uri = environments::REDIRECT_URI, diff --git a/rust/src/domain/mod.rs b/rust/src/domain/mod.rs index 78bee46..18bc704 100644 --- a/rust/src/domain/mod.rs +++ b/rust/src/domain/mod.rs @@ -1,10 +1,11 @@ //! `gddy domain` — domain availability and suggestions. //! -//! These endpoints (the public GoDaddy Domains API) authenticate with an -//! sso-key, not OAuth. The HTTP layer is the typed, spec-generated -//! [`domains_client`] crate; auth scheme selection (sso-key vs Bearer) is driven -//! by the credential the [`CompositeAuthProvider`](crate::auth::CompositeAuthProvider) -//! returns for `domain:*` commands. +//! These endpoints (the GoDaddy Domains API) accept either an sso-key API key or +//! an OAuth bearer token. The HTTP layer is the typed, spec-generated +//! [`domains_client`] crate; the auth scheme is chosen from the credential the +//! [`CompositeAuthProvider`](crate::auth::CompositeAuthProvider) returns for +//! `domain:*` commands — `sso-key` when a key is configured for the environment, +//! otherwise the OAuth bearer token. use cli_engine::{ CliCoreError, CommandContext, CommandResult, CommandSpec, GroupSpec, Module, NextAction, diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index e672f49..56ba96e 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -60,7 +60,10 @@ const BUILTINS: &[Builtin] = &[ ]; /// A fully-resolved environment: everything needed to talk to it. -#[derive(Debug, Clone)] +/// +/// `Debug` is hand-written (not derived) to redact the sso-key fields — see the +/// impl below — so a stray `{:?}`/`?env` never leaks credentials. +#[derive(Clone)] pub struct ResolvedEnv { pub name: String, pub api_url: String, @@ -79,13 +82,54 @@ pub struct ResolvedEnv { } /// The domain-command view of a resolved environment. -#[derive(Debug, Clone)] +/// +/// `Debug` is hand-written (see below) to redact the sso-key fields. +#[derive(Clone)] pub struct ResolvedDomains { pub base_url: String, pub api_key: Option, pub api_secret: Option, } +/// `Debug` wrapper that never prints a secret's value: shows `Some()` +/// or `None`, so credential-bearing structs can keep a useful `Debug` without +/// risking leakage via `{:?}` in logs/errors. +struct Redacted<'a>(&'a Option); + +impl std::fmt::Debug for Redacted<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Some(_) => f.write_str("Some()"), + None => f.write_str("None"), + } + } +} + +impl std::fmt::Debug for ResolvedEnv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedEnv") + .field("name", &self.name) + .field("api_url", &self.api_url) + .field("client_id", &self.client_id) + .field("auth_url", &self.auth_url) + .field("token_url", &self.token_url) + .field("domains_api_url", &self.domains_api_url) + .field("api_key", &Redacted(&self.api_key)) + .field("api_secret", &Redacted(&self.api_secret)) + .finish() + } +} + +impl std::fmt::Debug for ResolvedDomains { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedDomains") + .field("base_url", &self.base_url) + .field("api_key", &Redacted(&self.api_key)) + .field("api_secret", &Redacted(&self.api_secret)) + .finish() + } +} + /// Schema of the local environments file (see [`environments_path`]). #[derive(Debug, Clone, Default, Deserialize)] pub struct EnvironmentsFile { @@ -93,7 +137,7 @@ pub struct EnvironmentsFile { pub environments: BTreeMap, } -#[derive(Debug, Clone, Deserialize)] +#[derive(Clone, Deserialize)] pub struct EnvEntry { pub api_url: String, #[serde(default)] @@ -112,6 +156,22 @@ pub struct EnvEntry { pub api_secret: Option, } +// Hand-written so the sso-key fields are redacted (keeps `EnvironmentsFile`'s +// derived `Debug` working without risking credential leakage). +impl std::fmt::Debug for EnvEntry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EnvEntry") + .field("api_url", &self.api_url) + .field("client_id", &self.client_id) + .field("auth_url", &self.auth_url) + .field("token_url", &self.token_url) + .field("domains_api_url", &self.domains_api_url) + .field("api_key", &Redacted(&self.api_key)) + .field("api_secret", &Redacted(&self.api_secret)) + .finish() + } +} + #[derive(Debug, thiserror::Error)] pub enum EnvError { #[error("unknown environment {name:?}; known: {known}")] diff --git a/rust/src/main.rs b/rust/src/main.rs index a7587bc..b4e4012 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -46,6 +46,7 @@ async fn main() -> ExitCode { .long("api-key") .global(true) .value_name("KEY") + .requires("api-secret") .help("sso-key API key for domain commands (overrides config/env)"), ) .arg( @@ -53,6 +54,7 @@ async fn main() -> ExitCode { .long("api-secret") .global(true) .value_name("SECRET") + .requires("api-key") .help("sso-key API secret (used with --api-key)"), ) })) @@ -69,8 +71,9 @@ async fn main() -> ExitCode { mw.env = env.clone(); } // Bridge the sso-key flags to the auth layer (the composite - // provider reads this for `domain:*` commands). Both are required - // together; a lone --api-key/--api-secret is ignored. + // provider reads this for `domain:*` commands). clap enforces that + // the two flags are supplied together (`requires`), so checking + // both here is just defensive. if let (Some(key), Some(secret)) = ( matches.get_one::("api-key"), matches.get_one::("api-secret"), From 305314ad8c704b9ea649c9ce6ad72a2d1ce522c4 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 11:12:31 -0700 Subject: [PATCH 03/14] test: domain commands fail closed without an auth provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot round-2 review on PR #59: adds domain_commands_require_auth, mirroring application_list_requires_auth — builds the CLI with no auth provider and asserts `domain available`/`suggest` are rejected at credential resolution (exit 2, provider error) before the handler runs, guarding against accidentally marking them no_auth. Co-Authored-By: Claude Opus 4.8 --- rust/src/domain/mod.rs | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/rust/src/domain/mod.rs b/rust/src/domain/mod.rs index 18bc704..8eb49be 100644 --- a/rust/src/domain/mod.rs +++ b/rust/src/domain/mod.rs @@ -255,6 +255,7 @@ pub fn module() -> Module { #[cfg(test)] mod tests { use super::format_price; + use cli_engine::{Cli, CliConfig}; #[test] fn formats_micro_units_to_decimal() { @@ -263,4 +264,48 @@ mod tests { assert_eq!(format_price(Some(20_500_000)).as_deref(), Some("20.50")); assert_eq!(format_price(None), None); } + + /// The `domain` commands call the Domains API, so they must stay fail-closed. + /// Built with **no auth provider registered**, the engine's default + /// `AuthRequirement::Required` must reject them at credential resolution + /// (exit code 2, provider error) before the handler runs — guarding against + /// anyone marking them `no_auth(true)` and letting them run unauthenticated. + #[tokio::test] + async fn domain_commands_require_auth() { + const AUTH_FAILURE_EXIT: i32 = 2; + // No `--env` flag here: the global flag is registered in main.rs, not in + // this minimal test harness, and env is irrelevant since auth resolution + // fails before the handler runs. + for args in [ + [ + "gddy", + "domain", + "available", + "example.com", + "--output", + "json", + ], + ["gddy", "domain", "suggest", "coffee", "--output", "json"], + ] { + let cli = Cli::new( + CliConfig::new("gddy", "GoDaddy developer CLI", "gddy") + .with_default_auth_provider("godaddy") + .with_module(super::module()), + ); + let output = cli.run(args).await; + assert_eq!( + output.exit_code, AUTH_FAILURE_EXIT, + "{args:?} must fail closed at auth resolution, got: {}", + output.rendered + ); + let json: serde_json::Value = + serde_json::from_str(&output.rendered).expect("valid json output"); + let message = json["error"]["message"].as_str().unwrap_or_default(); + assert!( + message.contains("provider"), + "expected an auth-provider resolution error for {args:?}, got: {}", + output.rendered + ); + } + } } From 6f5df8f815a9fdec3783c3d613c274a10ee03031 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 13:02:00 -0700 Subject: [PATCH 04/14] review: scope domains-client lint allowances; pin swagger2openapi Addresses Copilot round-3 review on PR #59: - domains-client/src/lib.rs: move the #![allow(...)] onto a `generated` module wrapping the progenitor include! (re-exported via `pub use generated::*`), so the hand-written client_with_auth/BuildError are linted by the workspace again. - regenerate-spec.sh: pin swagger2openapi@7.0.8 so spec regeneration is deterministic (verified the committed domains.oas3.json is unchanged). Co-Authored-By: Claude Opus 4.8 --- .../domains-client/scripts/regenerate-spec.sh | 4 +++- rust/domains-client/src/lib.rs | 20 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/rust/domains-client/scripts/regenerate-spec.sh b/rust/domains-client/scripts/regenerate-spec.sh index 9cc6373..e80f97a 100755 --- a/rust/domains-client/scripts/regenerate-spec.sh +++ b/rust/domains-client/scripts/regenerate-spec.sh @@ -102,6 +102,8 @@ print(f" kept {len(paths)} paths, {len(out['definitions'])} definitions") PY echo "==> Converting Swagger 2.0 -> OpenAPI 3.0" -npx -y swagger2openapi "$trimmed_v2" -o "$oas3" +# Pin the converter so regeneration is deterministic across time (an unpinned +# `npx swagger2openapi` would float to the latest release and can drift/break). +npx -y swagger2openapi@7.0.8 "$trimmed_v2" -o "$oas3" rm -f "$trimmed_v2" echo "==> Wrote $oas3" diff --git a/rust/domains-client/src/lib.rs b/rust/domains-client/src/lib.rs index 5c986d4..ea86c3f 100644 --- a/rust/domains-client/src/lib.rs +++ b/rust/domains-client/src/lib.rs @@ -6,13 +6,21 @@ //! `reqwest::Client` (the CLI sets the `Authorization: sso-key …`/Bearer header //! itself). See `scripts/regenerate-spec.sh` to refresh the spec. //! -//! Generated code is exempt from the workspace's strict style lints. -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] -#![allow(rustdoc::all)] +//! The lint allowances are scoped to the generated module so the hand-written +//! code below (`client_with_auth`, `BuildError`) is still linted normally. -include!(concat!(env!("OUT_DIR"), "/codegen.rs")); +/// progenitor-generated client + types. Exempt from the workspace's strict +/// style/rustdoc lints (it's machine-generated); the rest of the crate is not. +mod generated { + #![allow(clippy::all)] + #![allow(dead_code)] + #![allow(unused_imports)] + #![allow(rustdoc::all)] + + include!(concat!(env!("OUT_DIR"), "/codegen.rs")); +} + +pub use generated::*; /// Error building the authenticated HTTP client. #[derive(Debug, thiserror::Error)] From 6bb349e41136e9a464ff96d1798d5d0f98effb0a Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 13:09:10 -0700 Subject: [PATCH 05/14] docs: clarify domain endpoints accept sso-key OR OAuth Addresses Copilot round-4 review on PR #59: reword the ResolvedEnv api_key field doc, the CompositeAuthProvider doc, and the environments.toml template so none imply OAuth is unavailable for domain commands or that api_key/api_secret affect non-domain commands. Docs only; no behavior change. Co-Authored-By: Claude Opus 4.8 --- rust/examples/environments.toml | 10 +++++----- rust/src/auth.rs | 11 ++++++----- rust/src/environments/mod.rs | 7 ++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/rust/examples/environments.toml b/rust/examples/environments.toml index 29f278d..ea6a2a8 100644 --- a/rust/examples/environments.toml +++ b/rust/examples/environments.toml @@ -12,11 +12,11 @@ # # ── Auth ─────────────────────────────────────────────────────────────────── # The `domain` commands hit the GoDaddy Domains API, which is OAuth-enabled on -# all environments (run `gddy auth login --env ` first). So no sso-key is -# needed for the domain commands. If you do need an sso-key for some other, -# not-yet-OAuth-enabled endpoint, add `api_key`/`api_secret` to the env (or pass -# `--api-key`/`--api-secret`); the CLI then sends `Authorization: sso-key …` for -# `domain:*` commands instead of the OAuth bearer. +# all environments (run `gddy auth login --env ` first), so no sso-key is +# needed. `api_key`/`api_secret` are optional and affect ONLY `domain:*` +# commands: when set (here, via `_API_KEY`/`_API_SECRET`, or +# `--api-key`/`--api-secret`), those commands send `Authorization: sso-key …` +# instead of the OAuth bearer. They do not affect any non-domain command. # # `client_id`: a custom env needs one for PKCE login; the per-env public OAuth # client ids are filled in below. Override via DEV_OAUTH_CLIENT_ID / diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 74797ac..d4b2270 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -162,11 +162,12 @@ fn api_key_override() -> Option<(String, String)> { /// `domain:*` commands whose target environment has an sso-key configured, /// returns that key instead. /// -/// The GoDaddy Domains API endpoints authenticate with -/// `Authorization: sso-key :`, not OAuth. Every other command — and -/// any domain command in an environment without a configured key — continues to -/// use OAuth (including scope step-up). Scoping the bypass to `domain:*` keeps it -/// from affecting unrelated commands. +/// The GoDaddy Domains API endpoints accept either an sso-key +/// (`Authorization: sso-key :`) or an OAuth bearer token. This +/// provider uses the sso-key only when one is configured for a `domain:*` +/// command's environment; every other command — and any domain command without a +/// configured key — uses OAuth (including scope step-up). Scoping the bypass to +/// `domain:*` keeps it from affecting unrelated commands. #[derive(Debug, Default)] pub struct CompositeAuthProvider { oauth: GoDaddyAuthProvider, diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index 56ba96e..e846877 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -74,9 +74,10 @@ pub struct ResolvedEnv { /// availability) live behind a different host than the OAuth/`api_url` /// service; this defaults to `api_url` when not overridden. pub domains_api_url: String, - /// sso-key API key/secret for the domain endpoints, which use - /// `Authorization: sso-key :` rather than OAuth. Optional: when - /// absent, domain commands fall back to the OAuth credential. + /// Optional sso-key for the domain endpoints (which accept either sso-key or + /// OAuth). When both are set, `domain:*` commands authenticate with + /// `Authorization: sso-key :`; when absent they use the OAuth + /// credential. pub api_key: Option, pub api_secret: Option, } From 5988ddb87ff0d138c9416e8dfe47044e6c8da952 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 13:16:20 -0700 Subject: [PATCH 06/14] fix: resolve sso-key as an atomic pair per layer Addresses Copilot round-5 review on PR #59: _API_KEY/_API_SECRET and the file's api_key/api_secret were applied independently, so an env-var key could combine with a file secret (or vice versa) into a bogus sso-key pair and confusing 401s. Resolve the pair atomically from a single layer (env-var pair wins over the file pair); a partial pair yields no sso-key (domain commands fall back to OAuth), matching the --api-key/--api-secret flag pairing. Adds tests. Co-Authored-By: Claude Opus 4.8 --- rust/src/environments/mod.rs | 61 ++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index e846877..df170f2 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -298,8 +298,6 @@ fn resolve_with( let mut auth_url: Option = None; let mut token_url: Option = None; let mut domains_api_url: Option = None; - let mut api_key: Option = None; - let mut api_secret: Option = None; // Layer 2: local config entry (overrides/defines). An empty/whitespace // api_url is ignored so it can't clobber a built-in default. @@ -315,8 +313,6 @@ fn resolve_with( auth_url = entry.auth_url.as_deref().and_then(clean_url); token_url = entry.token_url.as_deref().and_then(clean_url); domains_api_url = entry.domains_api_url.as_deref().and_then(clean_url); - api_key = entry.api_key.as_deref().and_then(non_empty); - api_secret = entry.api_secret.as_deref().and_then(non_empty); } // Layer 3: per-env `_*` overrides (highest precedence). Empty values @@ -328,12 +324,29 @@ fn resolve_with( if let Some(url) = var(&format!("{prefix}_DOMAINS_API_URL")).and_then(|v| clean_url(&v)) { domains_api_url = Some(url); } - if let Some(k) = var(&format!("{prefix}_API_KEY")).and_then(|v| non_empty(&v)) { - api_key = Some(k); - } - if let Some(s) = var(&format!("{prefix}_API_SECRET")).and_then(|v| non_empty(&v)) { - api_secret = Some(s); - } + + // The sso-key is a (key, secret) pair; resolve it atomically from a single + // layer — the env-var pair wins over the file pair — so we never mix layers + // (e.g. an env-var key with a file-provided secret), which would send a bogus + // `sso-key key:secret` and yield confusing 401s. A partial pair (only one of + // the two present in a layer) yields no sso-key, so domain commands fall back + // to OAuth. The `--api-key`/`--api-secret` flags enforce the same pairing. + let entry = file.environments.get(name); + let (api_key, api_secret) = match ( + var(&format!("{prefix}_API_KEY")).and_then(|v| non_empty(&v)), + var(&format!("{prefix}_API_SECRET")).and_then(|v| non_empty(&v)), + ) { + (Some(k), Some(s)) => (Some(k), Some(s)), + _ => match ( + entry.and_then(|e| e.api_key.as_deref()).and_then(non_empty), + entry + .and_then(|e| e.api_secret.as_deref()) + .and_then(non_empty), + ) { + (Some(k), Some(s)) => (Some(k), Some(s)), + _ => (None, None), + }, + }; // api_url is already trimmed/normalized by clean_url (and built-ins carry no // trailing slash), so callers concatenating paths never produce `//`. @@ -752,10 +765,38 @@ mod tests { let mut file = EnvironmentsFile::default(); let mut e = entry("https://dev.example.invalid"); e.api_key = Some("file-key".to_owned()); + e.api_secret = Some("file-secret".to_owned()); file.environments.insert("dev".to_owned(), e); + // A blank env-var key is ignored, so the complete file pair survives. let var = |k: &str| (k == "DEV_API_KEY").then(|| " ".to_owned()); let env = resolve_with("dev", &file, var).expect("dev resolves"); assert_eq!(env.api_key.as_deref(), Some("file-key")); + assert_eq!(env.api_secret.as_deref(), Some("file-secret")); + } + + #[test] + fn sso_key_pair_is_resolved_atomically_per_layer() { + // File has a complete pair; env supplies only the key. The env layer is + // incomplete, so we must NOT mix (env-key + file-secret) — the complete + // file pair is used instead. + let mut file = EnvironmentsFile::default(); + let mut e = entry("https://dev.example.invalid"); + e.api_key = Some("file-key".to_owned()); + e.api_secret = Some("file-secret".to_owned()); + file.environments.insert("dev".to_owned(), e); + let key_only = |k: &str| (k == "DEV_API_KEY").then(|| "env-key".to_owned()); + let env = resolve_with("dev", &file, key_only).expect("dev resolves"); + assert_eq!(env.api_key.as_deref(), Some("file-key")); + assert_eq!(env.api_secret.as_deref(), Some("file-secret")); + + // A partial pair within a single layer (file key, no secret) yields no + // sso-key at all, so domain commands fall back to OAuth. + let mut file2 = EnvironmentsFile::default(); + let mut e2 = entry("https://dev.example.invalid"); + e2.api_key = Some("lonely-key".to_owned()); + file2.environments.insert("dev".to_owned(), e2); + let env2 = resolve_with("dev", &file2, no_vars).expect("dev resolves"); + assert!(env2.api_key.is_none() && env2.api_secret.is_none()); } #[test] From 44eae11a8c08ff86f467b70c4ee315364ea297ab Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 13:22:26 -0700 Subject: [PATCH 07/14] review: reuse environments::env_prefix in auth debug log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Copilot round-6 review on PR #59: make `env_prefix` public and call it from `log_resolved_oauth` instead of re-deriving the uppercase/`-`→`_` prefix, so the convention has a single authority and can't drift. Co-Authored-By: Claude Opus 4.8 --- rust/src/auth.rs | 2 +- rust/src/environments/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/src/auth.rs b/rust/src/auth.rs index d4b2270..e125db2 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -66,7 +66,7 @@ fn log_resolved_oauth(env: &ResolvedEnv) { if !tracing::enabled!(tracing::Level::DEBUG) { return; } - let prefix = env.name.to_uppercase().replace('-', "_"); + let prefix = environments::env_prefix(&env.name); let override_var = |suffix: &str| std::env::var(format!("{prefix}_OAUTH_{suffix}")).ok(); let client_id_ovr = override_var("CLIENT_ID"); let auth_url_ovr = override_var("AUTH_URL"); diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index df170f2..1e485f6 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -191,7 +191,7 @@ pub enum EnvError { /// Environment-variable prefix for an env name, matching cli-engine's /// `PkceAuthProvider` derivation (uppercase, `-` → `_`). -fn env_prefix(name: &str) -> String { +pub fn env_prefix(name: &str) -> String { name.to_uppercase().replace('-', "_") } From 7f31de68429a07e26420b9d7f397670e0b16e7fd Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 13:36:01 -0700 Subject: [PATCH 08/14] chore: bump cli-engine 0.2.1 -> 0.2.2 Pull in the latest cli-engine patch release. No source changes required; build, clippy (-D warnings), and the test suite (149) all pass. Co-Authored-By: Claude Opus 4.8 --- rust/Cargo.lock | 5 +++-- rust/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 73e73c4..8641f55 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -542,9 +542,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cli-engine" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4015806946ade71addfa8ae7a869dfd061df2536dac41371efb0489bed9760" +checksum = "aac2356eabf48d9c8142a70e0da391f065986911dfcc8c49381fa4559795b98f" dependencies = [ "async-trait", "base64 0.22.1", @@ -562,6 +562,7 @@ dependencies = [ "sha2", "thiserror 2.0.18", "tokio", + "toml_edit 0.22.27", "tracing", "url", "zeroize", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6c6a425..eb09ddb 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" async-trait = "0.1" chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] } clap = { version = "4.5", features = ["std", "string"] } -cli-engine = { features = ["pkce-auth"], version = "0.2.1" } +cli-engine = { features = ["pkce-auth"], version = "0.2.2" } dirs = "6" domains-client = { path = "domains-client" } fancy-regex = "0.14" From fe1d8f6fa5fa08b879c38599b55b5555369ecb73 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 15:13:38 -0700 Subject: [PATCH 09/14] Don't expose environments --- rust/examples/environments.toml | 35 --------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 rust/examples/environments.toml diff --git a/rust/examples/environments.toml b/rust/examples/environments.toml deleted file mode 100644 index ea6a2a8..0000000 --- a/rust/examples/environments.toml +++ /dev/null @@ -1,35 +0,0 @@ -# gddy environments — copy/paste template for internal developers. -# -# Copy this file to your gddy config dir (only the entries you need): -# Linux/XDG : ~/.config/gddy/environments.toml (or $XDG_CONFIG_HOME/gddy/…) -# macOS : ~/Library/Application Support/gddy/environments.toml -# Windows : %APPDATA%\gddy\environments.toml -# -# Built-in `ote` and `prod` resolve without this file; you only need entries for -# the internal `dev`/`test` environments. Anything here can be overridden at -# runtime by `_API_URL`, `_DOMAINS_API_URL`, `_API_KEY`, -# `_API_SECRET` env vars (e.g. DEV_API_KEY), or `--api-key/--api-secret`. -# -# ── Auth ─────────────────────────────────────────────────────────────────── -# The `domain` commands hit the GoDaddy Domains API, which is OAuth-enabled on -# all environments (run `gddy auth login --env ` first), so no sso-key is -# needed. `api_key`/`api_secret` are optional and affect ONLY `domain:*` -# commands: when set (here, via `_API_KEY`/`_API_SECRET`, or -# `--api-key`/`--api-secret`), those commands send `Authorization: sso-key …` -# instead of the OAuth bearer. They do not affect any non-domain command. -# -# `client_id`: a custom env needs one for PKCE login; the per-env public OAuth -# client ids are filled in below. Override via DEV_OAUTH_CLIENT_ID / -# TEST_OAUTH_CLIENT_ID if needed. auth/token URLs derive from api_url -# (…/v2/oauth2/authorize, …/v2/oauth2/token). `domains_api_url` defaults to -# `api_url`, so a single host per env is enough. - -# Internal dev gateway. -[environments.dev] -api_url = "https://api.dev-godaddy.com" -client_id = "94488449-5769-4ecf-8bf4-9f8aa83859a3" - -# Internal test gateway. -[environments.test] -api_url = "https://api.test-godaddy.com" -client_id = "e710d8b9-f4e5-4178-b1bf-98dfcd15d4ed" From dc32a81ee5e8d078124055c6df9805a992d20214 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 15:48:34 -0700 Subject: [PATCH 10/14] refactor: generate domains-client with progenitor builder interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the progenitor build to InterfaceStyle::Builder so operations expose fluent, named setters (e.g. client.suggest().query(..).limit(..).send()) instead of positional args. Call sites in domain/mod.rs now read clearly without an IDE and only set the parameters they use — no more strings of `None` whose meaning is opaque in a diff. No behavior change (verified: request omits unset optional query params). fmt/clippy/test all pass (149). Co-Authored-By: Claude Opus 4.8 --- rust/domains-client/build.rs | 8 ++++++- rust/src/domain/mod.rs | 44 ++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/rust/domains-client/build.rs b/rust/domains-client/build.rs index 341aba9..2873365 100644 --- a/rust/domains-client/build.rs +++ b/rust/domains-client/build.rs @@ -14,7 +14,13 @@ fn main() -> Result<(), Box> { let spec_text = fs::read_to_string(spec_path)?; let spec: openapiv3::OpenAPI = serde_json::from_str(&spec_text)?; - let mut generator = progenitor::Generator::default(); + // Builder interface: each operation gets a fluent builder with named setters + // (e.g. `client.suggest().query(..).limit(..).send()`) instead of positional + // args, so call sites read clearly without an IDE and unused optional params + // are simply omitted rather than passed as `None`. + let mut settings = progenitor::GenerationSettings::new(); + settings.with_interface(progenitor::InterfaceStyle::Builder); + let mut generator = progenitor::Generator::new(&settings); let tokens = generator.generate_tokens(&spec)?; let ast = syn::parse2(tokens)?; let formatted = prettyplease::unparse(&ast); diff --git a/rust/src/domain/mod.rs b/rust/src/domain/mod.rs index 8eb49be..a45be55 100644 --- a/rust/src/domain/mod.rs +++ b/rust/src/domain/mod.rs @@ -125,12 +125,16 @@ pub fn module() -> Module { .filter(|&b| b); let client = make_client(&ctx).await?; - let resp = client - .available(check_type, &domain, for_transfer) - .await - .map_err(|e| { - CliCoreError::message(format!("domain availability check failed: {e}")) - })?; + let mut req = client.available().domain(domain.as_str()); + if let Some(ct) = check_type { + req = req.check_type(ct); + } + if let Some(ft) = for_transfer { + req = req.for_transfer(ft); + } + let resp = req.send().await.map_err(|e| { + CliCoreError::message(format!("domain availability check failed: {e}")) + })?; let body = resp.into_inner(); let mut result = json!({ @@ -223,19 +227,21 @@ pub fn module() -> Module { }; let client = make_client(&ctx).await?; - let resp = client - .suggest( - city, - country, - None, - None, - limit, - Some(&query), - None, - tlds.as_ref(), - None, - None, - ) + let mut req = client.suggest().query(query.as_str()); + if let Some(n) = limit { + req = req.limit(n); + } + if let Some(t) = tlds { + req = req.tlds(t); + } + if let Some(c) = country { + req = req.country(c); + } + if let Some(city) = city { + req = req.city(city); + } + let resp = req + .send() .await .map_err(|e| CliCoreError::message(format!("domain suggestion failed: {e}")))?; let suggestions: Vec = From 01fc6f663a5615fde0cf1409409c97c6812e714d Mon Sep 17 00:00:00 2001 From: Jay Gowdy Date: Fri, 12 Jun 2026 15:51:16 -0700 Subject: [PATCH 11/14] test: cover domains-client wiring; polish domain output + docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses deep-review findings on PR #59: - domains-client: add offline httpmock tests for the generated client — available/suggest request shape (query-param names, Authorization/ x-request-id/api-version headers) and response deserialization. The suggest test asserts each scalar lands in the correctly named query param, guarding the 10-positional-argument call against silent drift if the spec is regenerated (most params share Option types). - domain: extract the sso-key-vs-Bearer scheme choice into a pure authorization_header() and unit-test it; the prior inline logic had no coverage. - domain: emit `suggest` results as objects with a `domain` field and add .with_default_fields to both commands, so list output supports --fields/default-field projection (matches the application module). - domain: pin the suggest() positional-argument order in a comment and document format_price's intentional whole-cent truncation (+ test). - main: warn in --api-secret help that a value on the command line is visible in the process list / shell history. --- rust/Cargo.lock | 2 + rust/domains-client/Cargo.toml | 4 + rust/domains-client/src/lib.rs | 153 +++++++++++++++++++++++++++++++++ rust/src/domain/mod.rs | 54 ++++++++++-- rust/src/main.rs | 7 +- 5 files changed, 210 insertions(+), 10 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8641f55..ab14156 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -755,6 +755,7 @@ version = "0.1.0" dependencies = [ "bytes", "futures", + "httpmock", "openapiv3", "prettyplease", "progenitor", @@ -764,6 +765,7 @@ dependencies = [ "serde_json", "syn 2.0.117", "thiserror 2.0.18", + "tokio", ] [[package]] diff --git a/rust/domains-client/Cargo.toml b/rust/domains-client/Cargo.toml index 2240f91..3292389 100644 --- a/rust/domains-client/Cargo.toml +++ b/rust/domains-client/Cargo.toml @@ -24,3 +24,7 @@ openapiv3 = "2" serde_json = "1" prettyplease = "0.2" syn = "2" + +[dev-dependencies] +httpmock = "0.7" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/rust/domains-client/src/lib.rs b/rust/domains-client/src/lib.rs index ea86c3f..199a1a9 100644 --- a/rust/domains-client/src/lib.rs +++ b/rust/domains-client/src/lib.rs @@ -58,3 +58,156 @@ pub fn client_with_auth( .build()?; Ok(Client::new_with_client(base_url, http)) } + +#[cfg(test)] +mod tests { + use super::*; + use httpmock::prelude::*; + use serde_json::json; + + // These tests exercise the generated request/response wiring against a mock + // server: the query-parameter names (which guard the builder setters → + // wire-parameter mapping at the call sites), the `Authorization`/ + // `x-request-id`/`api-version` headers set by `client_with_auth`, and response + // deserialization. They run entirely offline. + + #[tokio::test] + async fn available_sends_correct_request_and_parses_response() { + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(GET) + .path("/v1/domains/available") + .query_param("domain", "example.com") + .query_param("checkType", "FULL") + .query_param("forTransfer", "true") + .header("authorization", "sso-key KEY:SECRET") + .header("x-request-id", "req-123") + .header("api-version", "1.0.0"); + then.status(200).json_body(json!({ + "domain": "example.com", + "available": false, + "definitive": true, + "price": 11_990_000, + "currency": "USD", + "renewalPrice": 21_990_000, + "period": 1 + })); + }) + .await; + + let client = client_with_auth( + &server.base_url(), + "sso-key KEY:SECRET", + "godaddy-cli/test", + "req-123", + ) + .expect("build client"); + + let body = client + .available() + .domain("example.com") + .check_type(types::AvailableCheckType::Full) + .for_transfer(true) + .send() + .await + .expect("request succeeds") + .into_inner(); + + mock.assert_async().await; + assert_eq!(body.domain, "example.com"); + assert!(!body.available); + assert!(body.definitive); + assert_eq!(body.price, Some(11_990_000)); + assert_eq!(body.currency, "USD"); + assert_eq!(body.renewal_price, Some(21_990_000)); + assert_eq!(body.period, Some(1)); + } + + #[tokio::test] + async fn available_with_bearer_scheme_sets_header() { + let server = MockServer::start_async().await; + let mock = server + .mock_async(|when, then| { + when.method(GET) + .path("/v1/domains/available") + .query_param("domain", "open.dev") + .header("authorization", "Bearer tok-abc"); + then.status(200).json_body(json!({ + "domain": "open.dev", + "available": true, + "definitive": true + })); + }) + .await; + + let client = client_with_auth( + &server.base_url(), + "Bearer tok-abc", + "godaddy-cli/test", + "req-1", + ) + .expect("build client"); + + let body = client + .available() + .domain("open.dev") + .send() + .await + .expect("request succeeds") + .into_inner(); + + mock.assert_async().await; + assert!(body.available); + // Optional fields absent in the response deserialize to None. + assert_eq!(body.price, None); + assert_eq!(body.currency, "USD"); // serde default + } + + #[tokio::test] + async fn suggest_maps_positional_args_to_named_query_params() { + let server = MockServer::start_async().await; + // Asserting each value lands in the correctly *named* query param guards + // the builder setter -> wire-parameter mapping (e.g. that `.city(..)` + // really sends `city=`, not some other param) across spec regenerations. + let mock = server + .mock_async(|when, then| { + when.method(GET) + .path("/v1/domains/suggest") + .query_param("query", "coffee") + .query_param("city", "Phoenix") + .query_param("country", "US") + .query_param("limit", "5") + .query_param("tlds", "com"); + then.status(200).json_body(json!([ + { "domain": "coffeehouse.com" }, + { "domain": "bestcoffee.com" } + ])); + }) + .await; + + let client = client_with_auth( + &server.base_url(), + "Bearer tok", + "godaddy-cli/test", + "req-2", + ) + .expect("build client"); + + let suggestions = client + .suggest() + .query("coffee") + .city("Phoenix") + .country(types::SuggestCountry::Us) + .limit(5) + .tlds(vec!["com".to_string()]) + .send() + .await + .expect("request succeeds") + .into_inner(); + + mock.assert_async().await; + let domains: Vec<&str> = suggestions.iter().map(|s| s.domain.as_str()).collect(); + assert_eq!(domains, ["coffeehouse.com", "bestcoffee.com"]); + } +} diff --git a/rust/src/domain/mod.rs b/rust/src/domain/mod.rs index a45be55..be1ebad 100644 --- a/rust/src/domain/mod.rs +++ b/rust/src/domain/mod.rs @@ -35,11 +35,29 @@ fn map_env_err(e: environments::EnvError) -> CliCoreError { } /// Convert a currency micro-unit amount (e.g. 11_990_000) to a decimal string -/// ("11.99"). Domain prices are returned in micro-units. +/// ("11.99"). Domain prices are returned in micro-units (1 unit = 1_000_000 +/// micros). +/// +/// Truncates (does not round) to whole cents: the API returns whole-cent prices +/// in practice, so the sub-cent digits are always zero; truncating keeps the +/// output a faithful, surprise-free rendering of the raw value rather than +/// inventing a rounded figure. See the `formats_micro_units_to_decimal` test for +/// the defined behavior on a (synthetic) sub-cent input. fn format_price(micros: Option) -> Option { micros.map(|m| format!("{}.{:02}", m / 1_000_000, (m.abs() % 1_000_000) / 10_000)) } +/// Pick the `Authorization` header value for a resolved credential: the `sso-key` +/// scheme for the [`SSO_KEY_PROVIDER`] bypass path, otherwise an OAuth `Bearer` +/// token. Pure so the scheme selection is unit-testable without a full context. +fn authorization_header(provider: &str, token: &str) -> String { + if provider == SSO_KEY_PROVIDER { + format!("sso-key {token}") + } else { + format!("Bearer {token}") + } +} + /// Build a Domains API client for the active environment, choosing the auth /// scheme from the resolved credential (sso-key for the bypass path, else /// Bearer). The credential is resolved through the registered composite provider. @@ -47,11 +65,7 @@ async fn make_client(ctx: &CommandContext) -> Result { let env = ctx.middleware.env.clone(); let domains = environments::resolve_domains(&env).map_err(map_env_err)?; let cred = ctx.credential().await?; - let authorization = if cred.provider == SSO_KEY_PROVIDER { - format!("sso-key {}", cred.token) - } else { - format!("Bearer {}", cred.token) - }; + let authorization = authorization_header(&cred.provider, &cred.token); let request_id = uuid::Uuid::new_v4().to_string(); domains_client::client_with_auth(&domains.base_url, &authorization, USER_AGENT, &request_id) .map_err(|e| CliCoreError::message(format!("failed to build domains client: {e}"))) @@ -78,6 +92,7 @@ pub fn module() -> Module { CommandSpec::new("available", "Check whether a domain is available") .with_system("domain") .with_tier(Tier::Read) + .with_default_fields("domain,available,definitive,price,currency") .with_scopes(&[DOMAINS_READ_SCOPE]) .with_arg( clap::Arg::new("domain") @@ -172,6 +187,7 @@ pub fn module() -> Module { CommandSpec::new("suggest", "Suggest available domains for a query") .with_system("domain") .with_tier(Tier::Read) + .with_default_fields("domain") .with_scopes(&[DOMAINS_READ_SCOPE]) .with_arg( clap::Arg::new("query") @@ -244,8 +260,13 @@ pub fn module() -> Module { .send() .await .map_err(|e| CliCoreError::message(format!("domain suggestion failed: {e}")))?; - let suggestions: Vec = - resp.into_inner().into_iter().map(|s| s.domain).collect(); + // Emit objects (not bare strings) so the list has a projectable + // `domain` field for `--fields`/default-field rendering. + let suggestions: Vec = resp + .into_inner() + .into_iter() + .map(|s| json!({ "domain": s.domain })) + .collect(); Ok( CommandResult::new(json!(suggestions)).with_next_actions(vec![ @@ -260,7 +281,8 @@ pub fn module() -> Module { #[cfg(test)] mod tests { - use super::format_price; + use super::{authorization_header, format_price}; + use crate::auth::SSO_KEY_PROVIDER; use cli_engine::{Cli, CliConfig}; #[test] @@ -269,6 +291,20 @@ mod tests { assert_eq!(format_price(Some(1_000_000)).as_deref(), Some("1.00")); assert_eq!(format_price(Some(20_500_000)).as_deref(), Some("20.50")); assert_eq!(format_price(None), None); + // Sub-cent micros truncate toward the lower cent (documented behavior): + // 1_005_000 micros = 1.005 -> "1.00", never "1.01". + assert_eq!(format_price(Some(1_005_000)).as_deref(), Some("1.00")); + } + + #[test] + fn authorization_header_picks_scheme_from_provider() { + // sso-key bypass path -> `sso-key KEY:SECRET`. + assert_eq!( + authorization_header(SSO_KEY_PROVIDER, "KEY:SECRET"), + "sso-key KEY:SECRET" + ); + // Any other provider (OAuth/PKCE) -> `Bearer `. + assert_eq!(authorization_header("godaddy", "tok123"), "Bearer tok123"); } /// The `domain` commands call the Domains API, so they must stay fail-closed. diff --git a/rust/src/main.rs b/rust/src/main.rs index b4e4012..daa7aba 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -55,7 +55,12 @@ async fn main() -> ExitCode { .global(true) .value_name("SECRET") .requires("api-key") - .help("sso-key API secret (used with --api-key)"), + .help( + "sso-key API secret (used with --api-key). Note: a \ + value passed on the command line is visible in the \ + process list and shell history; prefer _API_SECRET \ + or the environments config file", + ), ) })) .with_apply_flags(Arc::new(|matches, mw| { From 9ed897c357e3ffeece1eff37a4902468cc5fbadf Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 15:55:29 -0700 Subject: [PATCH 12/14] review: sign-correct price formatting; recover poisoned override lock Addresses Copilot round-7 review on PR #59: - format_price formats the sign explicitly and uses unsigned_abs, so sub-unit negatives render correctly (-500_000 -> "-0.50", not "0.50") and i64::MIN can't overflow. Adds negative-value tests. - The api-key override Mutex now recovers the inner value on poison (with a tracing::warn) instead of silently dropping the override, which would have confusingly switched a domain command from sso-key to OAuth. Co-Authored-By: Claude Opus 4.8 --- rust/src/auth.rs | 18 ++++++++++++++---- rust/src/domain/mod.rs | 17 +++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/rust/src/auth.rs b/rust/src/auth.rs index e125db2..b05916b 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -146,16 +146,26 @@ fn api_key_override_cell() -> &'static std::sync::Mutex API_KEY_OVERRIDE.get_or_init(|| std::sync::Mutex::new(None)) } +/// Lock the override cell, recovering the inner value if the mutex was poisoned +/// by an unrelated panic. The data is a plain `Option<(String, String)>` that's +/// always left consistent, so recovery is safe — and dropping the override on a +/// poisoned lock would silently switch a domain command from sso-key to OAuth, +/// which is exactly the confusing failure we want to avoid. +fn lock_api_key_override() -> std::sync::MutexGuard<'static, Option<(String, String)>> { + api_key_override_cell().lock().unwrap_or_else(|poisoned| { + tracing::warn!("api-key override lock was poisoned; recovering the stored value"); + poisoned.into_inner() + }) +} + /// Record an sso-key from the `--api-key`/`--api-secret` flags. Highest /// precedence for domain-command auth (beats `_API_KEY` and config). pub fn set_api_key_override(key: String, secret: String) { - if let Ok(mut guard) = api_key_override_cell().lock() { - *guard = Some((key, secret)); - } + *lock_api_key_override() = Some((key, secret)); } fn api_key_override() -> Option<(String, String)> { - api_key_override_cell().lock().ok().and_then(|g| g.clone()) + lock_api_key_override().clone() } /// Auth provider that composes [`GoDaddyAuthProvider`] (OAuth/PKCE) but, for diff --git a/rust/src/domain/mod.rs b/rust/src/domain/mod.rs index be1ebad..96b2d75 100644 --- a/rust/src/domain/mod.rs +++ b/rust/src/domain/mod.rs @@ -34,7 +34,6 @@ fn map_env_err(e: environments::EnvError) -> CliCoreError { CliCoreError::message(e.to_string()) } -/// Convert a currency micro-unit amount (e.g. 11_990_000) to a decimal string /// ("11.99"). Domain prices are returned in micro-units (1 unit = 1_000_000 /// micros). /// @@ -43,8 +42,19 @@ fn map_env_err(e: environments::EnvError) -> CliCoreError { /// output a faithful, surprise-free rendering of the raw value rather than /// inventing a rounded figure. See the `formats_micro_units_to_decimal` test for /// the defined behavior on a (synthetic) sub-cent input. +/// +/// The sign is formatted explicitly (and `unsigned_abs` avoids `i64::MIN` +/// overflow) so sub-unit negatives like `-500_000` render as `-0.50`, not `0.50`. fn format_price(micros: Option) -> Option { - micros.map(|m| format!("{}.{:02}", m / 1_000_000, (m.abs() % 1_000_000) / 10_000)) + micros.map(|m| { + let sign = if m < 0 { "-" } else { "" }; + let abs = m.unsigned_abs(); + format!( + "{sign}{}.{:02}", + abs / 1_000_000, + (abs % 1_000_000) / 10_000 + ) + }) } /// Pick the `Authorization` header value for a resolved credential: the `sso-key` @@ -290,6 +300,9 @@ mod tests { assert_eq!(format_price(Some(11_990_000)).as_deref(), Some("11.99")); assert_eq!(format_price(Some(1_000_000)).as_deref(), Some("1.00")); assert_eq!(format_price(Some(20_500_000)).as_deref(), Some("20.50")); + // Negatives keep their sign, including sub-unit amounts. + assert_eq!(format_price(Some(-11_990_000)).as_deref(), Some("-11.99")); + assert_eq!(format_price(Some(-500_000)).as_deref(), Some("-0.50")); assert_eq!(format_price(None), None); // Sub-cent micros truncate toward the lower cent (documented behavior): // 1_005_000 micros = 1.005 -> "1.00", never "1.01". From ae491db1c3c4f137e72a115df1ef9d42275954d8 Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 16:56:10 -0700 Subject: [PATCH 13/14] security: drop --api-key/--api-secret CLI flags (CWE-214) Passing the sso-key as command-line arguments exposes it via the process list and shell history (CWE-214). Remove the global --api-key/--api-secret flags and the flag->override bridge before they ship in this PR. The sso-key now comes only from the existing, non-CWE-214 sources: the _API_KEY/_API_SECRET env vars or the gitignored environments.toml entry. - main.rs: remove the two flag definitions and the apply_flags bridge. - auth.rs: remove the API_KEY_OVERRIDE static + set_api_key_override/api_key_override and the lock helper; sso_key_credential now reads only environments::resolve_domains. Env-var/config resolution, the sso-key vs Bearer scheme selection, and the pure credential-factory tests are unchanged. Co-Authored-By: Claude Opus 4.8 --- rust/src/auth.rs | 42 +++--------------------------------------- rust/src/main.rs | 31 ------------------------------- 2 files changed, 3 insertions(+), 70 deletions(-) diff --git a/rust/src/auth.rs b/rust/src/auth.rs index b05916b..ebdb4ff 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -134,40 +134,6 @@ impl AuthProvider for GoDaddyAuthProvider { /// client selects the `sso-key` Authorization scheme instead of Bearer. pub const SSO_KEY_PROVIDER: &str = "sso-key"; -/// Process-global sso-key supplied via the `--api-key`/`--api-secret` flags. -/// -/// The flags are bridged here (rather than to `_API_KEY` env vars, which -/// edition-2024 makes `unsafe` to set) by [`set_api_key_override`] during flag -/// application; the composite provider consults it before the per-env config. -static API_KEY_OVERRIDE: std::sync::OnceLock>> = - std::sync::OnceLock::new(); - -fn api_key_override_cell() -> &'static std::sync::Mutex> { - API_KEY_OVERRIDE.get_or_init(|| std::sync::Mutex::new(None)) -} - -/// Lock the override cell, recovering the inner value if the mutex was poisoned -/// by an unrelated panic. The data is a plain `Option<(String, String)>` that's -/// always left consistent, so recovery is safe — and dropping the override on a -/// poisoned lock would silently switch a domain command from sso-key to OAuth, -/// which is exactly the confusing failure we want to avoid. -fn lock_api_key_override() -> std::sync::MutexGuard<'static, Option<(String, String)>> { - api_key_override_cell().lock().unwrap_or_else(|poisoned| { - tracing::warn!("api-key override lock was poisoned; recovering the stored value"); - poisoned.into_inner() - }) -} - -/// Record an sso-key from the `--api-key`/`--api-secret` flags. Highest -/// precedence for domain-command auth (beats `_API_KEY` and config). -pub fn set_api_key_override(key: String, secret: String) { - *lock_api_key_override() = Some((key, secret)); -} - -fn api_key_override() -> Option<(String, String)> { - lock_api_key_override().clone() -} - /// Auth provider that composes [`GoDaddyAuthProvider`] (OAuth/PKCE) but, for /// `domain:*` commands whose target environment has an sso-key configured, /// returns that key instead. @@ -211,12 +177,10 @@ impl CompositeAuthProvider { }) } - /// Resolve the sso-key for a domain command (flag override → per-env config) - /// and turn it into a credential. + /// Resolve the sso-key for a domain command from the environment's config + /// (`_API_KEY`/`_API_SECRET` env vars or the `environments.toml` + /// entry) and turn it into a credential. fn sso_key_credential(env: &str, command: &str) -> Option { - if let Some((key, secret)) = api_key_override() { - return Self::sso_key_credential_from(env, command, Some(&key), Some(&secret)); - } let domains = environments::resolve_domains(env).ok()?; Self::sso_key_credential_from( env, diff --git a/rust/src/main.rs b/rust/src/main.rs index daa7aba..907b111 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -41,27 +41,6 @@ async fn main() -> ExitCode { ) .help("Target environment (ote|prod)"), ) - .arg( - Arg::new("api-key") - .long("api-key") - .global(true) - .value_name("KEY") - .requires("api-secret") - .help("sso-key API key for domain commands (overrides config/env)"), - ) - .arg( - Arg::new("api-secret") - .long("api-secret") - .global(true) - .value_name("SECRET") - .requires("api-key") - .help( - "sso-key API secret (used with --api-key). Note: a \ - value passed on the command line is visible in the \ - process list and shell history; prefer _API_SECRET \ - or the environments config file", - ), - ) })) .with_apply_flags(Arc::new(|matches, mw| { if let Some(env) = matches.get_one::("env") { @@ -75,16 +54,6 @@ async fn main() -> ExitCode { } mw.env = env.clone(); } - // Bridge the sso-key flags to the auth layer (the composite - // provider reads this for `domain:*` commands). clap enforces that - // the two flags are supplied together (`requires`), so checking - // both here is just defensive. - if let (Some(key), Some(secret)) = ( - matches.get_one::("api-key"), - matches.get_one::("api-secret"), - ) { - auth::set_api_key_override(key.clone(), secret.clone()); - } Ok(()) })) .with_root_next_actions(Arc::new(|| { From e45f33091d81dc4b293039aa0cad44c49c0d50aa Mon Sep 17 00:00:00 2001 From: Jacob Page Date: Fri, 12 Jun 2026 17:01:34 -0700 Subject: [PATCH 14/14] docs: drop stale --api-key/--api-secret flag reference The atomic-pair comment in environments::resolve_with referenced the --api-key/--api-secret flags removed earlier in this PR. Reworded to describe only the env-var/config layers (the actual sources). Co-Authored-By: Claude Opus 4.8 --- rust/src/environments/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/src/environments/mod.rs b/rust/src/environments/mod.rs index 1e485f6..c9afdcd 100644 --- a/rust/src/environments/mod.rs +++ b/rust/src/environments/mod.rs @@ -330,7 +330,7 @@ fn resolve_with( // (e.g. an env-var key with a file-provided secret), which would send a bogus // `sso-key key:secret` and yield confusing 401s. A partial pair (only one of // the two present in a layer) yields no sso-key, so domain commands fall back - // to OAuth. The `--api-key`/`--api-secret` flags enforce the same pairing. + // to OAuth. let entry = file.environments.get(name); let (api_key, api_secret) = match ( var(&format!("{prefix}_API_KEY")).and_then(|v| non_empty(&v)),