← Back to blog

How to Let Your Users Buy Domains Inside Your SaaS (2026 Guide)

Jonathan Geiger·
custom domainstutorialbuy domain api

If your SaaS lets customers connect their own domain (shop.acme.com, hello.janesbakery.com, whatever), you've probably watched the same support thread play out a hundred times.

A user signs up. They want their site on their own domain. You point them at Namecheap or GoDaddy with a help article that says "buy any domain, then come back and add the CNAME". Half of them don't come back for a week. A third of those come back asking what a CNAME is. The rest finally make it, but only after rage-Googling whether @ means the root or the subdomain.

You can shortcut the entire dance by selling the domain inside your product.

This guide walks through what that looks like in 2026: what stack actually supports it without an ICANN reseller account, how the money flow works, and how to ship it in an afternoon.

The pattern, in one diagram

Jane (end-user)   →   Your SaaS              →   Domainee API    →   Registrar
                      ↑                          ↑                   ↑
                      sets her price             charges your card   does the actual
                      keeps her margin           wholesale + $1      ICANN registration

Three parties bill the next one up the chain. Your end-user never sees Namecheap or whoever's underneath. They buy from you. They get a confirmation email from you. Their domain works.

You don't need a reseller agreement with anyone. You don't have to fund a registrar account. You don't write XML.

Why most teams skip this (and why that's a mistake)

The historical reason "buy domain inside the app" was rare in SaaS: setting it up was genuinely painful. You'd need:

  • An ICANN-accredited registrar relationship, or a wholesale reseller contract with one
  • A funded balance at that registrar
  • Code to wrap their API (often XML-over-GET or some other 2008 special)
  • A WHOIS contact pipeline
  • A refund process for when the registration fails after you've charged
  • A way to bill your customer for what you spent

That's a multi-week build before the first end-user clicks "buy". So teams ship "go register at GoDaddy and come back". Activation drops. Support load goes up.

In 2026 that's no longer true. A half-dozen vendors abstract the whole stack behind a REST API. You stop building plumbing and ship the feature.

The minimum viable Buy-a-Domain UI

Three states in your dashboard:

  1. Search: input box, your user types a hostname. You quote the live price.
  2. Confirm: show the price and a "Register" button. Click it.
  3. Done: domain is registered, your user sees "✓ janesbakery.com is yours".

That's it. The hard part lives in the API call. The UI is one form.

What the API call looks like

Using Domainee's Buy-a-Domain API, this is the entire dance.

Step 1: Quote the price (no charge)

When your user types a hostname in the search box, fire one call:

curl https://api.domainee.dev/v1/domain-purchases/check \
    --get --data-urlencode 'hostname=janesbakery.com' \
    -H "Authorization: Bearer $DOMAINEE_API_KEY"

Response:

{
  "hostname": "janesbakery.com",
  "available": true,
  "premium": false,
  "pricing": {
    "wholesaleCents": 1418,
    "feeCents": 100,
    "totalCents": 1518,
    "currency": "USD"
  }
}

wholesaleCents is what the registrar charges. feeCents is Domainee's flat $1. totalCents is what you'll pay. You then mark that up however you want for your end-user. $19.99 is a common choice. Some platforms go $29.99 and call it "premium support included".

If you want to be transparent, show the breakdown. If you want to be a website builder, show one round number.

Step 2: Register (one POST, all the magic)

Your user clicks "Register". You fire:

curl -X POST https://api.domainee.dev/v1/domain-purchases \
    -H "Authorization: Bearer $DOMAINEE_API_KEY" \
    -d '{
      "hostname": "janesbakery.com",
      "years": 1,
      "registrant": {
        "firstName": "Jane",
        "lastName": "Smith",
        "email": "jane@bakery.example",
        "phone": "+1.5551234567",
        "address1": "123 Main St",
        "city": "Portland",
        "stateOrProvince": "OR",
        "postalCode": "97201",
        "country": "US"
      },
      "customerReference": "user_jane_42"
    }'

That's it. Domainee does four things:

  1. Charges your saved Stripe card $15.18 off-session.
  2. Registers janesbakery.com at the upstream registrar with Jane as the legal owner of record.
  3. Returns a purchase record with the registration ID and expiry.
  4. Fires a domain_purchase.completed webhook to your endpoint.

If the registrar refuses for any reason (insufficient funds, premium price changed, banned TLD), Domainee automatically refunds the Stripe charge and fires domain_purchase.failed instead. You don't have to write refund code.

What the response looks like

{
  "purchase": {
    "id": "f8a0c1b9-1234-…",
    "hostname": "janesbakery.com",
    "status": "completed",
    "totalCents": 1518,
    "registrar": "namecheap",
    "registrarDomainId": "182739",
    "expiresAt": "2027-05-17T15:42:11.000Z",
    "customerReference": "user_jane_42"
  }
}

customerReference is the part most people overlook. It's an opaque string you stash at buy time, usually your internal user ID. Later, when Jane logs into your dashboard and wants to see "my domains", you query by that reference and skip building your own join table:

curl "https://api.domainee.dev/v1/domain-purchases?customerReference=user_jane_42" \
    -H "Authorization: Bearer $DOMAINEE_API_KEY"

Same pattern Stripe uses with metadata.

"But who owns the domain legally?"

Jane does. The contact info you pass in the registrant object is what gets filed at the registrar and what WHOIS reports (WHOIS privacy on, but the underlying record is Jane's). If Jane ever leaves your platform, she can transfer the domain out. That's how it should work.

Some platforms try to be clever and register the customer's domains under their own corporate name. Don't. It feels like control and it's actually a customer-retention bomb. The day a customer realizes they don't legally own the domain they've been paying you for is the last day they're your customer.

The money flow, step by step

Let's price out a real .com purchase to make this concrete:

StepWho paysWho keepsAmount
Wholesale (Namecheap → Domainee)DomaineeNamecheap$9.18
API charge (Domainee → you)YouDomainee$1.00
Retail (You → Jane)JaneYouhowever you priced it

If you charge Jane $19.99:

  • Jane pays you $19.99
  • You pay Domainee $10.18 (wholesale + $1)
  • You keep $9.81

That's your margin per purchase. Multiply by however often your users actually want a custom domain. If 10% of new signups buy one, you're netting roughly $10 every tenth signup, and you've removed the "go register at GoDaddy" friction from the activation funnel at the same time.

Connecting the domain to your app

Buying the domain is half the job. To actually serve content on janesbakery.com, the DNS needs to point at your app.

The cleanest way: pass autoConnect on the buy call.

curl -X POST https://api.domainee.dev/v1/domain-purchases \
    -H "Authorization: Bearer $DOMAINEE_API_KEY" \
    -d '{
      "hostname": "janesbakery.com",
      "registrant": { ... },
      "autoConnect": {
        "originUrl": "https://janesbakery.acmesites.app"
      }
    }'

What that does behind the scenes:

  1. Charges your card (wholesale + $1)
  2. Registers the domain with your end-user as the legal owner
  3. Provisions the hostname on Domainee's edge (TLS cert minted on first request)
  4. Sets the DNS at the registrar to CNAME → edge.domainee.dev

One API call, one webhook, one transaction. If any step fails the whole thing rolls back: the Stripe charge is refunded, no domain row is left in a half-state.

If your user is buying the domain for something other than your app (email, parking, side project they haven't decided about yet), skip autoConnect and just let them have the domain. Later, if they want to connect it, call POST /v1/domain-purchases/:id/connect and we'll do the same chain.

What Acme can build for Jane after the buy

The point of this API isn't just the buy. It's that Jane never has to log into Namecheap to manage her own domain.

Eight more endpoints to build the rest of the dashboard:

With those, Acme can build a domain settings page that looks like every other domain UI Jane has ever used — minus the part where she had to register and remember a Namecheap password.

Renewals: manual or automatic, you pick

Two patterns, both supported.

Manual. When Jane pays you for another year, you call POST /:id/renew { "years": 1 }. We charge your card, extend at the registrar, fire domain_purchase.renewed.

Auto. Set autoRenew: true at purchase time (or flip it later via PATCH). A background worker runs daily, finds domains expiring within 30 days, and renews them for you. If your card declines at renewal time the domain runs out the clock and domain_purchase.renewal_failed fires so you can email Jane to fix her payment with you before the domain disappears.

Which to use comes down to your billing relationship. If Jane has a yearly subscription with you, autoRenew is the right answer — flip it on at purchase time and forget about it. If she's pay-per-renewal, keep it off and call POST /:id/renew explicitly when she pays.

What to build in your dashboard

A few patterns worth stealing.

Suggest alternatives when the name is taken

When Jane searches janesbakery.com and it's gone, suggest .bakery, .shop, .cafe variants. Same /check endpoint, fire 5–10 in parallel.

Schedule renewal reminders

Subscribe to domain_purchase.completed and schedule yourself a reminder email a month before expiresAt. Or just turn on auto-renew and forget about it.

Send your own receipts

When the webhook fires, send Jane a receipt with the price you charged her, not the wholesale price. Cuts your support load.

Catch failed purchases

Subscribe to domain_purchase.failed and surface the error in your UI with a "try a different name" CTA. The most common failure mode is premium pricing. Someone searches startup.ai, the registrar wants $500, the charge aborts. You want that to be a friendly UI moment, not an opaque error toast.

What you don't have to build

Things that look like one-day projects and become two-month projects if you go DIY:

  • 3DS / SCA handling on the charge (off-session payment intents handle it via Stripe's machinery)
  • Idempotency keys for retries (Domainee surfaces the standard Stripe-style header)
  • Refund logic on registrar failure (automatic)
  • WHOIS privacy enrollment (free, on by default)
  • ICANN registrant email verification (registrar handles it directly with your end-user)
  • Premium price reveal protection (pass maxTotalCents and we abort before charging if the live price exceeds your ceiling)

Wrapping up

The "Buy a Domain" feature used to be the kind of thing only Wix, Squarespace, and Lovable shipped, because only those teams had the registrar relationships to make it work. In 2026 that's no longer true. The underlying plumbing is now a public API. You can ship it in an afternoon, charge whatever margin makes sense for your audience, and stop bleeding activation to "go register at GoDaddy".

The full reference for the API lives at /docs/api/domain-purchases. The dedicated landing page with code examples is at /buy-domain-api. Grab an API key from /sign-up and the first quote is free. You're not committing to a registrar account or paying a monthly fee just to test the surface.

If you ship this, tell us about it. We're curating a list of products using the Buy-a-Domain API.