Buy a domain
POST /v1/domain-purchases — charge your card and register a hostname for your end-user.
POST /v1/domain-purchases
Charges your workspace's saved Stripe card, then registers the hostname at the upstream registrar with the contact you supply. Returns the purchase record on success. Auto-refunds and reports failure if the registrar refuses after the charge succeeded.
Request
curl -X POST https://api.domainee.dev/v1/domain-purchases \
-H "Authorization: Bearer $DOMAINEE_API_KEY" \
-H "content-type: application/json" \
-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"
}'
Body parameters
| Field | Required | Default | Notes |
|---|---|---|---|
hostname | ✅ | — | The domain to register. Lowercased + trimmed automatically. |
years | 1 | Integer 1–10. How long to register for. | |
registrant | ✅ | — | Contact info for the legal owner of record. See below. |
enableWhoisPrivacy | true | Free WHOIS privacy at the registrar. Set false only if you need public WHOIS. | |
autoRenew | false | Renew at the registrar automatically before expiry. | |
maxTotalCents | — | Safety ceiling — if live price exceeds this, we return 409 price_changed before charging. Recommended on every call to protect against premium-price surprises. | |
customerReference | — | Opaque string echoed back on reads + webhooks. Stash your internal user-id, order-id, etc. — then filter the list endpoint by it later. | |
autoConnect | — | When present, after registration we ALSO provision the hostname on Domainee's edge and set the registrar DNS to CNAMEs (apex + www) pointing at us. One API call ships a working live domain. Replaces the whole record set — add MX / TXT records afterwards. See below. |
autoConnect object
"autoConnect": {
"originUrl": "https://janesbakery.acmesites.app", // required
"keepHost": false // optional, default false
}
| Field | Required | Notes |
|---|---|---|
originUrl | ✅ | Where the edge proxies requests. Must be https://. |
keepHost | Forward the customer's original Host header. Default false. |
If autoConnect is set, the response's purchase.connectedDomainId field
is populated with the matching /v1/domains row id, and the domain
resolves to your app within a minute of DNS propagation.
If any step in the chain fails (edge provisioning, DNS update), the
entire transaction rolls back — the Stripe charge is refunded, the
purchase row is marked refunded, and domain_purchase.failed fires.
Registrant object
All fields are forwarded to the registrar verbatim. Your end-user is the legal owner of record, not you. ICANN requires the address fields to be real — fake data risks suspension of the domain.
| Field | Required | Notes |
|---|---|---|
firstName | ✅ | |
lastName | ✅ | |
email | ✅ | Registrar may send verification mail here. |
phone | ✅ | E.164 with dot: +1.5551234567. |
address1 | ✅ | |
address2 | ||
city | ✅ | |
stateOrProvince | ✅ | |
postalCode | ✅ | |
country | ✅ | ISO 3166-1 alpha-2 (e.g. "US"). |
organization | Set for business-owned domains. |
Response — 201 Created
{
"purchase": {
"id": "f8a0c1b9-1234-…",
"workspaceId": "ws_…",
"hostname": "janesbakery.com",
"years": 1,
"wholesaleCents": 1418,
"feeCents": 100,
"totalCents": 1518,
"currency": "USD",
"registrar": "namecheap",
"registrarDomainId": "182739",
"status": "completed",
"registrant": {
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@bakery.example",
"country": "US"
},
"whoisPrivacyEnabled": true,
"autoRenew": false,
"expiresAt": "2027-05-17T15:42:11.000Z",
"customerReference": "user_jane_42",
"createdAt": "2026-05-17T15:42:09.000Z",
"updatedAt": "2026-05-17T15:42:11.000Z"
}
}
Errors
| Code | Status | When |
|---|---|---|
unavailable | 409 | Hostname is already registered or not available for this TLD. |
price_changed | 409 | Live price exceeded maxTotalCents. No charge made. |
billing_required | 402 | The Stripe charge failed — declined card, no card on file, 3DS required, insufficient funds. No registration attempted. |
registration_failed | 502 | We charged your card but the registrar refused. We automatically refunded the Stripe charge and marked the row as refunded. The error message contains the registrar's reason. |
bad_request | 400 | Validation failure — missing/malformed body. |
Side effects
- A row is written to
domain_purchasesfor your workspace. - Your workspace's Stripe card is charged the full
totalCents. - The hostname is registered at the upstream registrar with the supplied contact as the legal owner.
- One of two webhook events fires:
domain_purchase.completed— happy pathdomain_purchase.failed— charge failed or registrar refused
Idempotency
Send an Idempotency-Key header to dedupe retries:
Idempotency-Key: 8a04...
We cache the response for 24 hours per workspaceId × method × path × key.
Retries with the same key return the original response without re-charging
or re-registering.
After the purchase
Without autoConnect, the domain is registered but not yet
configured to serve your app's content. Call
POST /:id/connect when
you're ready; it provisions the hostname on the edge and sets the DNS in
one call.
Either way, connecting replaces the domain's entire DNS record set with
our CNAMEs. If the domain needs email (MX, SPF, DKIM, DMARC) or other
records, add them after connecting via
GET /:id/dns +
PUT /:id/dns. Records set
before connecting are removed.