One document, three parts. Section numbers carry their part's letter, so every reference names the part it points into — §S2, §P3, §R4.2.
| Part | Sections | Covers |
|---|---|---|
| Part S — Search | §S1 – §S4 | Which domain to buy: a named domain, ideas from the user's business or site, suggestions, unsupported extensions. Start here. |
| Part P — Purchase Path | §P1 – §P10 | Buying a domain from Wix |
| Part R — Reference | §R1 – §R10 | Endpoints, product IDs, URL templates, site resolution, error handling, currency |
| Not here | — | A request that doesn't say whether to buy or use a domain the user has, and a domain that turns out to be taken: Connect Domain. Putting a domain the account holds on a site: Assign Domain. A domain registered elsewhere, or a subdomain: Connect External Domain or Sub Domain |
Before any API call, read Part R. It holds every endpoint, product ID, URL template, site-resolution rule and error rule used by both of the other parts. Do not guess a URL or a field name — look it up there.
Part S takes a request to buy a domain to the point where one chosen domain is known to be buyable, then goes to §P1. Covers US-03 – US-12 for the purchase path.
The user wants to buy, register or get a domain, or ideas for one. Typical openers: "buy me a domain", "I want to purchase a domain", "get me mybusiness.com", "I need a domain for my pancakes restaurant".
⛔ Availability checks are never done by this recipe. Every check of whether a domain is available goes through Check Domain Availability. Read that skill, run the check as it describes, then come back with the result: §R4.1 maps it. Never call the availability endpoints from this recipe.
The limits on a taken domain — no WHOIS, no owner, no broker or valuation advice — are Connect Domain §A1.1, and they hold here too.
UX rule for everything below: keep it natural, never expose internal mechanics, and reveal information only when it changes what the user has to do next. See §R9.
Take the first row that matches, reading top-down. A domain the user named wins over everything else in this table — including a site they named in the same breath.
| Situation | Action |
|---|---|
| User named a specific domain — even if they named a site too | Normalize it (§R1). A subdomain (more than one label before the TLD, read greedily: .co.uk, never .uk, so shop.maximz.fr is one and mybrand.co.uk is not) is not registered on its own → Connect External Domain or Sub Domain, with no availability check. Anything else → run the availability check, first, before any site work, and read the result with §R4.1 |
| User gave brand/business context but no domain — "My business is called Green Sprout Bakery", "I need a domain for my pancakes restaurant" | Do not ask. Build a query from that context → §S3 |
| No domain, but they pointed at a site — "I want a domain for my wix site", "I need a domain for my site" | Do not ask for a domain yet. The site is the context → §S1.2 |
| Nothing at all — "I want to buy a domain", "help me get a domain" | Ask, verbatim: "What domain do you have in mind?" Then take the answer as either a specific domain (→ the named-domain row) or a description (→ §S3). |
| The user already has a domain and wants it on their site, or isn't sure whether to buy one | Go to Connect Domain and follow it. |
The user answering with a description rather than a domain ("Something with 'cozycuts' in it") is normal — treat it as a query and go to §S3.
⚠️ A named domain is checked before anything else happens, and the answer is the first thing the user hears. No site resolution, no fetching list of sites, no plan check and no list of sites comes in front of it. "I want to buy mybakery.com for my Wix site" names a site, and the site waits its turn: the check runs, the user is told "mybakery.com is available!" or "mybakery.com is already registered.", and only then does §P1 resolve the site. Asking which of ninety-three sites to use before saying whether the domain can even be bought answers a question the user has not reached yet.
This section is only for a message that named no domain, with PURCHASE intent. A user who has a domain to connect goes to Connect Domain instead. If a domain was named as well — "buy mybakery.com for my wix site" — §S1's named-domain row owns it: the check runs first, and the site is resolved afterwards by §P1. Resolving a site here would put a list in front of the availability answer.
"I want a domain for my wix site" names no domain, and it is not nothing. Two things arrive with it, and the old reading of this row threw both away:
So resolve the site now, before the domain, and let it do both jobs:
| Site resolution (§R3) | Then |
|---|---|
Case A — a siteId is in context | Take the site's name as the §S3 query. Suggestions and the site named, one message. |
| Case B — exactly one site | Same. Name the site so a wrong guess is visible. |
| Case B — several sites | Ask which one, with §R3 Case B's copy. That question has to be asked anyway, and its answer is also the query — §S3 runs on the reply. |
| Case C — not logged in | No account, so no site name to read. Do not front-load the login prompt — fall through to the "What domain do you have in mind?" row above. The site intent is remembered, so §R3's Case C prompt lands at its normal time, once a domain is in hand. |
| Case B — no sites at all | Nothing to name and nothing to connect to. Fall through to the bare "What domain do you have in mind?" row above, and treat the purchase as standalone. |
Copy when a single site is resolved — §S3's list with its own opening:
Here are a few available options for {site name}:
Want one of these, or do you have something else in mind? I can also show more options if none feel right.
⚠️ A site name is not always a business name. Auto-generated names (Evl 99faa20b7b2b,
My Site 3, a bare hex string) and placeholders carry no brand signal, and querying §S3 on one
returns noise. When the name has no real words in it, treat it as no context: keep the resolved
siteId, then ask "What domain do you have in mind?" — the site still gets used for the plan check
and the auto-connect line, just not for suggestions.
⚠️ This is the one place the purchase path resolves a site before the domain, and only because the
user put the site in the request. §P1.1's rule — do not fetch list of sites unless the user asked
about a site — is satisfied here, not bypassed. Carry the resolved siteId into §P1; the site is
already settled, so the plan check is the next step and the question is never re-asked.
Say, verbatim:
{displayName} is available!
Then:
siteId is already in context, do not send "…is available!" on its own — continue straight
into the site + plan outcome (§P1) in the same message.When no siteId is in context but the user did ask for a site, the availability answer and §P1's
site question belong in the same message — the answer first, the question behind it:
mybakery.com is available! You have 93 Wix sites. Which one should it go on?
One turn instead of two, and the ordering is still answer-then-question.
⚠️ Suppressing the standalone line is a merge, never a deletion. The user is told in plain language that the domain can be bought — exactly once, in the first message that follows the check. Both continuations carry it, and the merged forms live in §P1.3a and §P2.3, Premium and Free respectively. If you are about to send a message that offers a plan, a price or a bundle and the user has not yet heard "{displayName} is available!", the line was dropped rather than merged. Say it.
Always paging.limit=3. Three options is the contract — never dump ten.
Query construction:
| Coming from | Query |
|---|---|
| A taken domain | The SLD only — mybusiness.com → mybusiness |
| An unsupported TLD | The SLD only — myshop.ws → myshop |
| A business idea or brand context | The description — pancakes restaurant, Green Sprout Bakery |
Keep the query short — roughly 12–15 characters works best. Drop filler words
(thebestflowershopintown → bestflowers), collapse repetition (gevageva… → geva), abbreviate
(professional → pro).
Present them as a plain bulleted list of suggestions[].domain, then invite both a pick and more:
Here are a few available options that fit your brand:
Want one of these, or do you have something else in mind? I can also show more options if none feel right.
When the suggestions come from business context rather than a typed domain, open with "Here are a few available options based on your business name:" instead.
Rules:
.com general, .shop / .store
e-commerce, .me personal branding).Offer more only when pagingMetadata.hasNext is true. To actually fetch them, repeat the same
query and paging.limit=3 and add paging.cursor={pagingMetadata.cursors.next} from the previous
response. hasNext alone does not page — without the cursor you get page 1 again.
Here are more available options:
Repeat with each new cursor for as long as the user keeps asking.
The picked domain is the chosen domain. Do not check its availability.
Now go to §P1 and follow it. A user who came from a connect request is now picking a domain to buy: treat it as a purchase.
Detected as an HTTP 400 body with details.applicationError.code === "DOMAINS_UNSUPPORTED_TLD" —
there is no availability object on this response. See §R4.1.
Fetch 3 alternatives with the SLD only as the query (§S3) and show them in the same message:
We don't currently support .ws domains. Here are some available alternatives:
User picks one → skip the availability check → Now go to §P1 and follow it.
A user who wanted to connect that domain, not buy one, is answered by Connect Domain §A6b.
Entry preconditions — you arrive here from Part S, or from Connect Domain, with:
available: true from the availability check, or it came
from the suggest endpoint (suggestions are always available, never re-check one).If either is missing, go back to §S1–§S4 and settle it first.
Terminal states — this path ends in exactly one of:
| Terminal state | Reached when |
|---|---|
| Checkout link | The user completed registration details and the cart is built (§P9) |
| Bundle link | A Free-tier user chose plan + domain together (§P2) |
| Error fallback | Two failed attempts — §R8 |
Read Part R before any call. Endpoints, product IDs and URL templates all live there.
Covers US-18 – US-32.
Domain purchase does not require a Wix site. Apply the site-resolution rules in §R3 — Case A (siteId in context), Case B (logged in → fetch list of sites), Case C (not logged in → inline login prompt), Case D (logged in, ask once).
Do not fetch list of sites unless the user asked about a site or answered yes to the question.
siteId and the site's display name; go to §P1.2.Read payload.premiumState. allowedDomain = premiumState !== "FREE".
Do this silently. Record whether the site was Premium or Free here — §P7 compares against it.
Say, verbatim:
Great — after purchasing, {displayName} will auto-connect to {site name}.
When the site was already known from context — which is exactly when §S2 suppressed the standalone "is available!" line — merge the two into one message:
{displayName} is available! After purchasing, it will auto-connect to {site name}.
Go to §P3.
The site exists but has no Premium plan. The user gets a real choice: upgrade and take the domain as part of the plan, or buy the domain on its own.
The "free for the first year" promise is TLD-dependent — never promise it blindly.
coupons_applicable: true → eligible. Anything else → not eligible. If the call is unavailable,
fall back to the static TLD list in §R7.
The getPremiumSale context call (wix.premium.store.v1.sales/GetSale) runs automatically.
saleDetails with data → a sale is active. Useful fields: saleDiscount,
saleEndDate, remainingDays.saleDetails → no sale. Say nothing about sales.If a siteId was in context, this message owes the user the availability line. §S2 held back
the standalone "{displayName} is available!" so it could be merged into the site + plan outcome, and
this is the branch that merges it — §P1.3a does the same job on a Premium site. Lead with it and
let the rest of the copy stand:
{displayName} is available! {site name} doesn't have a premium plan yet, and connecting a domain requires one. Upgrade and you can get it free for the first year — or buy it on its own at full price and connect it after you upgrade. Want me to generate a bundle link?
The not coupon-eligible variant takes the same opener. The just-logged-in variant does not — that user came through the login prompt, which only ever appears after "is available!" has already gone out on its own.
Coupon-eligible TLD — verbatim:
{site name} doesn't have a premium plan yet, and connecting a domain requires one. Upgrade and you can get {displayName} free for the first year — or buy it on its own at full price and connect it after you upgrade. Want me to generate a bundle link?
Not coupon-eligible — the free-year clause would be false, so drop that clause and keep the rest:
{site name} doesn't have a premium plan yet, and connecting a domain requires one. Upgrade and you can connect {displayName} to your site — or buy it on its own at full price and connect it after you upgrade. Want me to generate a bundle link?
If a sale is active, append one line to whichever version you used:
There's also {saleDiscount}% off Premium plans right now — {remainingDays} days left.
If the user just logged in or created an account in response to the connect prompt (§R3 Case C), they have already said yes to connecting. Skip the question and deliver the offer with the link in the same message — the connect constraint still gets stated:
Welcome! Your new site doesn't have a premium plan yet, and connecting a domain requires one. Upgrade and you can get {displayName} free for the first year →
[Get domain free with a site plan](fallback::https://github.com/wix/skills/blob/28ae41a9245a4acc6b00cc87fb7e1a8571d18935/skills/wix-manage/references/domains/%7Bbundle%20URL%7D*original::{bundle URL})
Why the constraint is in the sentence. US-19 asks for an informed decision, and the fact that makes this a decision rather than an upsell is that the standalone route leaves the domain unconnected until the user upgrades.
Two things the copy is careful not to do. It does not list what Premium includes — §P10 still forbids that, and "connecting a domain requires one" is a constraint, not a feature pitch. And it frames buying alone as deferred, not forbidden: buy now, upgrade later, connect then. That is what actually happens, so "you can't" would be wrong.
Here's where you can pick a plan and get {displayName} free for the first year →
Use the full domain in domainName, the resolved siteId in siteGuid. When the TLD is not
coupon-eligible, use the label [Get your site live at this domain] instead — do not promise a free
year.
This is a terminal state. Stop here. Do not continue to §P3, do not build a cart.
Reply "No problem." and continue to §P3 as a standalone purchase. Do not re-pitch the upgrade.
Do not restate the connect constraint here either. §P2.3 put it in front of the choice, which is where it changes a decision. Repeating it after the user has decided is a second pitch wearing a warning's clothes.
Keep the siteId in memory — §P7 re-checks the plan, and the user may upgrade in another tab.
But treat the purchase as standalone for now: no msid on the checkout link unless §P7 changes it.
Include the leading dot in tld.
products[0].productId — the cart needs it in §P8. Never hardcode it.products[0].pricingDetails[] as a table, and only those entries.Valid cycles are a subset of 1, 2, 3, 5 and 10 years, and the subset varies by TLD — some start at 2, some stop at 3, some go to 10. Do not add a row the API did not return, and do not drop one it did.
Sort the rows ascending by cycle.cycleDuration.count. The API does not return them in order —
.org and .biz both come back 10, 5, 2, 3, 1. Rendering the array order gives the user a jumbled
table.
Each pricingDetails[] entry carries several price objects. Read these and no others:
| Column | Field | Verified value (.biz, 1 year) |
|---|---|---|
| Duration | cycle.cycleDuration.count + .unit | 1 YEAR → 1 year |
| You pay now | finalPrice.total.formattedAmount | $7.90 |
| Renews at | renewalPrice.total.formattedAmount | $35.90 |
Always formattedAmount — it arrives with the currency symbol and the right number of decimals.
Never take amount and format it yourself, and never do arithmetic on either.
renewalPrice.total is the charge for the same period again — for a 2-year row it is the next
2-year charge, not a yearly rate. The header wording is what carries that; if it ever proves
confusing, fix the header, do not add a footnote after the table.
⛔ Do not read these, all of which are in the same response and all of which will produce a wrong
or unexplainable number: catalogPrice (the undiscounted list price — showing it next to "You pay
now" invents a discount claim), initialPurchasePrice (identical to finalPrice.total in every row
observed — if it ever differs, that is new information to check, not a free choice), lowestCyclePrice
(its name and its value are consistent with two different meanings in the data observed, so it is
unverified), and any monthly* or subtotal* variant. savings appears on some rows and not others —
never render it.
Message shape — one row per returned pricingDetails[] entry and no other rows:
No problem. How many years would you like to register {displayName} for?
| Duration | You pay now | Renews at |
|---|---|---|
| {cycle 1 duration} | {cycle 1 finalPrice} | {cycle 1 renewalPrice} |
| {cycle 2 duration} | {cycle 2 finalPrice} | {cycle 2 renewalPrice} |
| … one row per entry the API returned, shortest first … |
At checkout, prices may be shown in your local currency.
(Open with "No problem." only when the user has just declined a site or a bundle. Otherwise open with "How many years would you like to register it for?".)
That closing line is fixed and it is required — this is the first place the user sees a price, and every message that shows one carries it. Wording and the full rule: §R10.
Ask the user to pick. If they have no preference, default to the shortest available period.
Why the renewal column is not optional. The first-term price is discounted and the renewal price
is not — for .biz that is $7.90 now against $35.90 at renewal, a 4.5× step the user finds out
about a year later. A single-price table is accurate and still misleading.
Every number in that table comes from this response and this response only. There is no fallback
price list anywhere in this skill or in your own knowledge. A .com does not
have a price you already know.
| Situation | What to do |
|---|---|
| Call succeeded | Render its rows. Nothing else. |
A row has finalPrice but no renewalPrice | Leave that cell empty. Never fall back to catalogPrice, and never carry a renewal figure across from another row. Not observed in any response so far — if it happens, it is an unknown, and an unknown price stays blank. |
| No products returned | "Wix doesn't support purchasing this TLD. Try a different extension like .com, .net, or .org." Then offer suggestions (§S3). |
| Call failed | Retry once silently, then the escalation ladder (§R8). |
| Call cannot be made at all — no Wix session, no authenticated tool available in this channel | Stop the flow. Say: "I can't pull up live pricing right now. You can see current prices and register the domain here → https://manage.wix.com/account/domains" Do not continue to §P4. |
That last row is the one that bites. When the offering call is unavailable, the tempting move is to show a plausible table and carry on — a fabricated price the user may act on. A missing price ends the flow; it never gets filled in from memory.
If you find yourself about to write $7.90, $14.95, or any other specific figure that did not
arrive in an API response during this conversation, you are hallucinating pricing. Stop.
Address fields are flat on each contact — streetAddress, city, country, postalCode.
There is no nested address object.
Show them and ask for explicit confirmation. Never skip this — never assume.
I have your contact details on file — would you like to use these?
Ask for every field, one per line:
I'll need a few details to register the domain:
Field rules:
+{countryCode}.{number}, e.g. +1.5551234567, +972.533456789.IL, "United States" → US).The user may answer in a single line ("Alex Johnson, alex@cozycuts.com, +1.5032218800, 310 Oak Ave, Portland, US, 97201") — parse it, do not make them re-enter field by field.
Do this immediately once contacts are confirmed or collected — before asking about privacy — so validation problems surface while the user is still thinking about their details.
Generate a random UUID as sessionId (wsess). It links the contacts to the cart; keep it, §P8
needs the same value.
Use the same contact for registrant, admin and tech — standard for individual registrations.
Name only the fields that are actually wrong, in plain language. Never show the raw error, a field path, or a code.
There's an issue with one of your details — can you double-check this?
Take the correction, retry the upsert, and carry on. Some TLDs need extra fields — .com.br an
identification number, .it an entity type. If the error is about a TLD-specific requirement,
explain what is needed and ask for it.
On success, go to §P6.
The options come from the offering call with the addon product type (§R4.2), scoped to the domain's TLD — the same body shape as §P3, leading dot included:
⛔ Never call it without the tld. The paid addons vary by TLD — .com.au, .com.br, .info
and .biz, for example, have no Privacy + DNSSEC. An unscoped answer is not an answer for this
domain.
Match each entry in products[] by its ID against the product-ID table at the end of this section.
Offer only the paid addons whose IDs came back. "No protection" is always offered.
| Paid addons returned | What to show |
|---|---|
| Privacy + DNSSEC and Privacy only | All three options, Privacy + DNSSEC marked (recommended) |
| Privacy only | Privacy only (recommended) and No protection. No DNSSEC line, no mention of it |
| Privacy + DNSSEC only | Privacy + DNSSEC (recommended) and No protection |
| Neither | Skip the question. Say once that the registration will be public ("Privacy protection isn't available for .{TLD}, so the registration will be public.") and go to §P7 — no addon line item in §P8 |
⛔ Never put an addon product ID in the cart that this TLD-scoped call did not return. An addon
the response left out is not offered at all — not without a price, not as "recommended", not on
request. Checkout rejects it (MissingDomainBundleMetadata) and the user is left with a cart that
cannot be paid.
The addon response has the same shape as §P3's — one pricingDetails[] entry per cycle, with
finalPrice and renewalPrice on each. The registration period is already chosen by the time you
get here, so read the row whose cycle.cycleDuration.count matches the chosen term and take
finalPrice.total.formattedAmount from it. Never sum, never divide, never take the 1-year row and
multiply.
Message shape — one line per option the response allows, built from the table above. The full form, when both paid addons came back:
How would you like to protect your registration?
At checkout, prices may be shown in your local currency.
When Privacy + DNSSEC did not come back, its line is gone and Privacy only takes the recommendation:
How would you like to protect your registration?
At checkout, prices may be shown in your local currency.
The currency note is required here too (§R10). It qualifies the {price} figures and not
"Free" — free is not a currency amount. So it appears only when at least one priced option is
shown; a message with no price in it carries no note.
{price} carries the term, and the term is whatever the user picked:
| Chosen term | {price} reads |
|---|---|
| 1 year | {finalPrice}/yr |
| N years | {finalPrice} for {N} years |
⛔ /yr on a multi-year term is wrong here for the same reason it is wrong in §P9. The addon
figure is the charge for the whole period — verified live: Privacy + DNSSEC comes back $12.90 for
the 1-year cycle and $38.70 for the 3-year one. Writing "$38.70/yr" triples the price in the
user's head. (Those two figures illustrate the shape and are never printed to a user — §P9's closing
rule.)
{price} is whatever the addon offering call returned for that product ID. "No protection" is the
only one whose price is known ahead of time — it is Free. A paid addon is shown only because the
response returned it, so its price comes from that same entry. If a returned addon has no row for the
chosen term, do not offer it. The same rule as §P3 applies — a figure you did not receive in this
conversation is a fabrication, and the wording above is a template, not a price list.
| Choice | Addon product ID |
|---|---|
| Privacy + DNSSEC | f8211619-d9f6-4312-9d03-f2958bbd08aa |
| Privacy only | 22a84545-4ac0-4490-a434-45a1ebc479fb |
| No protection | b9d89ff0-f29b-4bfd-a3f0-6e34ae65120d |
Addon product type for all three: b3d86a1d-9db3-4f69-bd54-c132808856b1.
The user may pick any option shown (US-26) — recommend, never push. "No protection" is a legitimate answer; accept it without a warning.
Run this only when a siteId was resolved in §P1 and the site was Free at that point. Skip it
for standalone purchases with no site, and for sites already Premium.
The user may have upgraded in another tab while answering the questions above. Re-check silently, just before building the cart:
| Result | Effect |
|---|---|
| Still Free | Nothing changes. Standalone checkout, no msid. |
| Now Premium | Tell the user, and add msid={siteId} to the checkout URL. |
When it flipped to Premium, add one line to the final message — after "Setting up your order..." and before the summary:
Setting up your order...
{site name} is now on a Premium plan — {displayName} will auto-connect after purchase.
Here's your summary: …
Never mention the check itself, and never mention it when nothing changed.
Use CallWixSiteAPI (with siteId) when a site is in play, otherwise ManageWixSite. Both carry
Wix session auth — add no headers.
Three calls, in order, silently:
POST https://manage.wix.com/_api/premium-cart/v1/carts/active/cancel · Body {}
Succeeds even when there is no active cart. This is what stops leftovers from an earlier session
ending up in the user's order.GET https://manage.wix.com/_api/premium-cart/v1/carts/activePATCH https://manage.wix.com/_api/premium-cart/v1/carts/active/add-itemscore: "true" goes on the domain line item only.| Condition | Checkout URL |
|---|---|
| A site is attached — Premium in §P1.3a, or flipped to Premium in §P7 | https://manage.wix.com/cart/checkout?msid={siteId} |
| Standalone — no site, or the site is still Free (US-31) | https://manage.wix.com/cart/checkout |
Final message:
Setting up your order...
Here's your summary:
At checkout, prices may be shown in your local currency.
Here's your checkout link → [Complete your purchase](fallback::https://github.com/wix/skills/blob/28ae41a9245a4acc6b00cc87fb7e1a8571d18935/skills/wix-manage/references/domains/%7Bcheckout%20URL%7D*original::{checkout URL})
The currency note sits between the summary and the link, so the link is still the last thing in the message (§R10). This is the screen the user actually reads before paying, so it is the one place the note matters most.
For "No protection", the second line reads - **No protection** — Free. It has no renewal clause —
$0.00 does not renew at anything worth saying. When the cart has no addon line item (§P8), the
summary has no second line.
renewalPrice.total is the charge for the same period again, not a yearly rate. So the suffix
depends on the term:
| Term | The line reads |
|---|---|
N = 1 | - **{displayName}** — 1 year — {finalPrice} (renews at {renewalPrice}/yr) |
N > 1 | - **{displayName}** — {N} years — {finalPrice} (renews at {renewalPrice} every {N} years) |
⛔ Never write /yr on a multi-year term. For a 3-year .biz the renewal figure is $107.50 —
the next three-year charge. "renews at $107.50/yr" overstates it threefold. This is the single
most likely way to get this section wrong, and it is wrong in the direction that scares a user off a
purchase they were about to complete.
Both are the ones §P3 already fetched for the chosen cycle —
finalPrice.total.formattedAmount and renewalPrice.total.formattedAmount from the
pricingDetails[] row the user picked. Reuse them:
The addon carries renewalPrice too — verified live against the addon product type
(b3d86a1d-9db3-4f69-bd54-c132808856b1), same pricingDetails[] shape as the domain, one row per
cycle. So the addon line gets the same treatment as the domain line: same two term forms, same
prohibition on /yr for a multi-year term, same reuse rule (§P6 already read the row for the chosen
cycle).
One observed difference, and it is not a reason to change the shape: on all three addon products the
renewal figure equals the first-term figure — Privacy + DNSSEC is $12.90 now and $12.90 at
renewal, Privacy only $9.90 and $9.90. Unlike the domain, the addon has no introductory discount.
Print the clause anyway. "$12.90 (renews at $12.90/yr)" is the useful answer to "does this one
jump too?", and suppressing it whenever the numbers match would make its absence look like missing
data rather than a flat price.
Why the summary carries a renewal at all. §P3's table already showed it, and this is the last
screen before payment — the one the user actually reads. A summary that shows only "$7.90" on a
domain that renews at $35.90 is accurate and still leaves the user surprised in twelve months.
The number is already in hand; the only work here is printing it with the right period attached.
⛔ Every figure in this section is an observation about shape, not a price. $107.50, $12.90,
$9.90, $7.90, $35.90 — all of them are here to show which period a number covers and how far a
renewal can step. None of them is ever printed to a user. §P3's rule is the whole rule: a figure
that did not arrive in an API response during this conversation is a fabrication, and that applies
to numbers read out of this document exactly as it applies to numbers read out of your own
knowledge.
The user's only remaining step is payment; say nothing more.
Terminal state. Stop here.
❌ Never:
renewalPrice.total is the charge for
the same period again — /yr belongs on a 1-year term and nowhere else (§P3, §P6, §P9).getPremiumSale did not report.msid on a standalone checkout link, or leave it off when a Premium site is attached.✅ Always:
wsess across the intent save and both cart line items.siteId from §P1 through §P9.Read this part before making any API call. It is the single source of truth for endpoints, product IDs, URL templates, site resolution and error handling. Parts S and P both defer to it — where they disagree with this part, this part wins.
Covers US-13, US-14, US-15, US-16, US-37, US-38, US-39.
A taken domain (US-09, US-40, US-41) is answered by Connect Domain §A4b, which looks it up in the user's account. US-42 – US-46 (putting a domain the account holds on a site, changing its role, moving it) are Assign Domain's. This skill never writes to a user's site.
Apply before any API call, every time a domain is read from the user:
http:// and https://www.example.com/shop → example.com).com (example → example.com)The normalized value is {domain} everywhere below. Always use the full domain in URLs and
API calls — never the truncated display form.
Only the visible label is ever truncated. Two values exist for every domain:
| Value | Use |
|---|---|
domainName | Full normalized domain — URLs, API calls, cart metadata |
displayName | What the user sees in link text and summaries |
Rule: if the full domain is 20 characters or fewer, displayName = domainName.
If it is longer than 20 characters:
| Full domain | Length | displayName |
|---|---|---|
short.com | 9 | short.com |
myamazingbusiness.com | 21 | myamaz...iness.com |
verylongdomainname.com | 22 | verylo...nname.com |
Both paths need to know which Wix site (siteId, also called msid) is in play. The rules are
the same; when they run differs:
siteId already in context (US-13)The system prompt, environment or dashboard context already provides a siteId / msid.
→ Use it directly. Do NOT fetch list of sites. Do not ask the user to pick a site.
The user naming a site is not the same thing. If they say "my site is called My Bistro" but
no siteId is in context, you still need to fetch list of sites to turn that name into an ID — go to
Case B. What their answer buys you is skipping the "which site?" question, not the lookup. Match
the name against the returned sites; if nothing matches, list what came back and ask.
A roster in context is not a resolved siteId either. The host may preload part of the
account's site list — ten of ninety-three, say — into the system prompt or site context. That is
not Case A. It names no site for this domain, it may not even contain the right one, and it
hands you no siteId for the task. Go to Case B and fetch list of sites. Every prohibition on that
call is about a siteId you already hold, never about a list you happened to be shown.
siteId in context→ Fetch list of sites.
| Result | Action |
|---|---|
| Exactly one site (US-14) | Use it directly, no question asked. Name it in your next message so the user can spot a mistake. |
| 2 – 8 sites (US-15) | List them all, numbered, in the same message as the question: "You have a few Wix sites — which one should this domain be connected to?" ("a couple" when there are exactly two.) Wait for the pick. |
| More than 8 | Still a list, never a count — name the total, show five, invite a name. Copy below. Wait for the pick. |
| No sites | Purchase path → continue standalone. Connect path → Connect External Domain or Sub Domain §7. |
More than eight sites. A numbered wall of ninety-three is unusable, and a bare count is worse. Name the total, show five, and leave the door open:
You have 93 Wix sites. Which one should {displayName} go on?
Or tell me the name and I'll find it.
Show the five most recently updated if the response gives you that; otherwise the first five it returned. A name in reply is matched against the whole result you already have — do not fetch list of sites a second time to look it up.
⛔ Never announce the count and stop. "You have 93 sites — would you like me to list them?" asks permission for the step you were already told to take. The user has asked for their domain on a site; the list is what moves that forward and a count moves nothing. It costs a turn and answers nothing.
Do not ask a yes/no question about sites — the user cannot answer it usefully. Show the optional inline prompt instead, verbatim:
If you'd like to connect this domain to a Wix site — [Log in or create a Wix account]
siteId, no premium
pitch, no voucher check. Reply "No problem." and continue.The destination behind [Log in or create a Wix account] is supplied by the host channel's
sign-up/login affordance. In a channel with no widget runtime, link to https://www.wix.com/.
Purchase path only. Ask once, verbatim:
Would you like to connect this domain to one of your Wix sites?
siteId. Reply "No problem." and continue.Once a siteId is resolved, remember it for the rest of the conversation and reuse it in the
premium check, the bundle link and the checkout link.
These are open endpoints. Send a plain GET with query parameters: no headers, no tokens, no
scopes. If you get 403 / access_denied, you added auth that does not belong — remove it and
retry.
Do not use the GetSuggestedDomains tool. Always use the v2 endpoint below.
Check domain availability — every check in this flow uses Check Domain Availability (request, response, errors); never call the endpoints from here. In this flow its results map as follows:
| Result | Meaning here |
|---|---|
availability.available: true | Buyable → §S2 |
availability.available: false and availability.premium: false | Taken → Connect Domain §A4b, with intent PURCHASE. |
availability.premium: true, whatever available says | A premium domain, buyable → §S2. Say nothing about "premium": its prices come from the offering call (§P3). |
HTTP 400, details.applicationError.code === "DOMAINS_UNSUPPORTED_TLD" | Unsupported TLD → §S4. The availability object is absent. |
availability.premium is a pricing tier of the domain, not the Wix Premium plan of a site. Site
Premium comes only from premiumStatus (§R4.2).
Suggest domains
Accepts free text — business descriptions, keywords and brand concepts, not just domain names.
| Parameter | Notes |
|---|---|
query | Keywords, business idea or brand concept. When alternatives for a taken/unsupported domain: use the SLD only (for mybusiness.com, query mybusiness). |
paging.limit | Always 3 on the first page. See §S3. |
paging.cursor | Opaque cursor for the next page — see below. |
tlds | Optional TLD filter, repeatable, no leading dots (&tlds=com&tlds=net). |
Paging is cursor-based. pagingMetadata.hasNext tells you whether more exist; it does not
fetch them. To get the next page, repeat the same query and paging.limit and add
paging.cursor=<pagingMetadata.cursors.next>. Never re-issue the same request without the cursor —
you will get the first page again.
Every returned suggestion is already available. Never re-check availability for a suggestion. Never show a "premium" column or flag premium suggestions.
Use CallWixSiteAPI (with siteId) when a site is resolved, otherwise ManageWixSite. Both carry
Wix session auth automatically — do not add headers.
| Purpose | Call |
|---|---|
| List the user's sites | fetch list of sites |
| Site Premium status | GET https://manage.wix.com/_api/premium-store/plans/premiumStatus?metaSiteId={siteId} |
| Domain cycles + pricing | POST https://manage.wix.com/_api/premium-purchase-platform-serverless/v1/offering/72af0602-1321-4897-8299-f507480b2bb8Body: { "purchaseContext": { "params": { "tld": ".{TLD}" } } } |
| Privacy addon pricing | Same offering endpoint with the addon product type id b3d86a1d-9db3-4f69-bd54-c132808856b1Body: { "purchaseContext": { "params": { "tld": ".{TLD}" } } } — the returned products[] is the set of addons this TLD offers (§P6) |
| TLD coupon eligibility | com.wixpress.premium.domain.tlds.DomainTld/ListTldsBody: { "filter": { "tlds": ["{TLD_WITHOUT_DOT}"] } } |
| Active Premium sale | getPremiumSale context call → wix.premium.store.v1.sales/GetSale |
| Existing contacts | GET https://manage.wix.com/v1/domain-registration-intents/preview/{domain} |
| Save contacts | POST https://manage.wix.com/v1/domain-registration-intents/upsert |
| Cancel active cart | POST https://manage.wix.com/_api/premium-cart/v1/carts/active/cancel · Body {} |
| Get fresh cart | GET https://manage.wix.com/_api/premium-cart/v1/carts/active |
| Add items to cart | PATCH https://manage.wix.com/_api/premium-cart/v1/carts/active/add-items |
| Does this domain belong to the user | Not here — Connect Domain §A4b |
| Put a domain the account holds on a site | Not here — Assign Domain |
Premium status: read payload.premiumState. allowedDomain = premiumState !== "FREE".
allowedDomain: true → Premium site. allowedDomain: false → Free site.
The offering endpoint is the getDomainCycles call — the same one the production get-domain
purchase flow uses. Noted for whoever next reconciles this skill against production; it changes
nothing about how the call is made.
Both offering variants return the same envelope — products[].pricingDetails[], one entry per
cycle, each carrying finalPrice and renewalPrice. Verified live for the domain type and the addon
type alike. renewalPrice.total is always the charge for the same period again, never a yearly
rate (see §R9).
| Item | ID |
|---|---|
| Domain product type | 72af0602-1321-4897-8299-f507480b2bb8 |
| Domain product id | Read products[0].productId from the offering response — never hardcode |
| Addon product type (all three) | b3d86a1d-9db3-4f69-bd54-c132808856b1 — not every TLD offers every paid addon (§P6) |
| Privacy + DNSSEC | f8211619-d9f6-4312-9d03-f2958bbd08aa |
| Privacy only | 22a84545-4ac0-4490-a434-45a1ebc479fb |
| No protection | b9d89ff0-f29b-4bfd-a3f0-6e34ae65120d |
Use these exactly. Do not invent variants, reorder into a different form, or drop parameters.
| Name | Template |
|---|---|
| Checkout — with site | https://manage.wix.com/cart/checkout?msid={siteId} |
| Checkout — standalone | https://manage.wix.com/cart/checkout |
| Bundle / plan+domain | https://manage.wix.com/premium-pricing/wix/select-plan?domainName={domain}&referralAdditionalInfo=add-domain-purchase-intent&siteGuid={siteId}&showDomain=true |
| Fallback domain search | https://manage.wix.com/account/domains |
Notes:
premium-pricing/wix/select-plan form above. An older
premium-domains/split-page?domainName=… form also exists — do not use it.Used by §P2 to decide whether the "free for the first year" benefit can be
promised. Authoritative source is DomainTld/ListTlds → coupons_applicable: true. Use this static
list only as a fallback when that call is unavailable:
.com, .com.au, .co.uk, .net, .org, .art, .at, .co.at, .be, .biz, .blog, .ca,
.ch, .club, .coach, .com.br, .de, .fit, .fitness, .fr, .fun, .in, .co.in,
.org.in, .firm.in, .gen.in, .ind.in, .net.in, .info, .live, .nl, .online,
.pictures, .rocks, .site, .space, .shop, .store, .studio, .training, .work,
.xyz, .yoga
Extract the TLD greedily from the right: site.co.uk → .co.uk, not .uk.
Any failed call: retry once, silently. Say nothing to the user, before or after. Do not narrate "let me try that again", do not mention that anything failed. If the retry succeeds, continue as if nothing happened.
| Stage | Trigger | Say (verbatim) |
|---|---|---|
| 1 | Call fails, retry also fails | Something went wrong on our end. Please try again in a moment. |
| 2 | The user tries again and it fails again | Having trouble loading this — try again. You can also search for a domain directly → https://manage.wix.com/account/domains |
The fallback link appears at stage 2, not stage 1. Do not skip stage 1.
| Error | Action |
|---|---|
DOMAINS_UNSUPPORTED_TLD | Not an error to the user — a branch. See §S4. |
403 / access_denied on domain-search v2 | You added auth to a public endpoint. Remove all headers and retry. |
| Offering returns no products | "Wix doesn't support purchasing this TLD. Try a different extension like .com, .net, or .org." |
| Intent upsert validation error | Show which fields are wrong, ask for corrections, retry. See §P5. |
Cart add-items fails | Confirm productId came from the offering response, not hardcoded. Retry once. |
| Anything else, persistent | Escalation ladder above. |
❌ Never:
{placeholder}
braces for exactly this reason. A concrete figure appearing in a template, a transcript, or your
own knowledge of what domains usually cost is not data — it is a fabrication with a currency
symbol in front of it. If the call that supplies a price could not be made, the flow stops
(§P3). It does not continue with a plausible guess.pricingDetails[] — finalPrice
and renewalPrice alike, domain and addon alike — is the charge for one whole cycle. A 3-year
row's $107.50 is one three-year charge. /yr is correct on a 1-year term and on nothing else;
for anything longer the wording is "every {N} years" or "for {N} years"
(§P6, §P9). This is arithmetic the user does in their head, and the error is
always in the expensive direction.monthlyTotal and
lowestCyclePrice precisely because someone will be tempted; both are unverified in this skill and
neither is a substitute for saying which period the price covers. Print the period, not a rate.availability.premium for anything but the premium-domain row in §R4.1, or as the site's Premium status.siteId is already in context. A partial roster the host preloaded is
not that — see Case A.✅ Always:
siteId for the rest of the conversation.Every price this skill shows comes from the offering call as formattedAmount, which arrives with
its own currency symbol already attached (§P3). Checkout is a separate surface and may present the
charge in the user's own currency, so the figure in the conversation and the figure at the till can
differ in currency. The skill cannot tell in advance, so it says exactly that much and no more.
One fixed sentence covers it, and it is the only thing this skill ever says about currency:
At checkout, prices may be shown in your local currency.
It goes in the same message as the prices it qualifies, once, wherever a currency amount is shown to the user — the period table, the privacy options and the pre-checkout summary (§P3, §P6 and §P9). In the summary it sits above the checkout link, so the link stays last.
❌ Never:
Why this sits after Forbidden Actions rather than in numerical order. Renumbering §R9 would invalidate every reference to it across the document. A new section appended at the end costs nothing and breaks nothing.
Last updated: 15 November 2026