# Domain Purchase Buy an external domain (e.g. `yourapp.com`) **in the end user's name**. Cohesivity is the reseller (we own the wholesale account at Name.com); the end user is the ICANN registrant of record. Name.com's CORE API backs the v1 implementation — Cohesivity adds a 10% markup over Name.com's retail price, the user pays in INR via Razorpay. Unlike the rest of the offerings catalog, this is **not** provisioned through `/api/resources`. It's a paid one-shot: agents quote candidates, the human pays through a Razorpay-hosted checkout, and Cohesivity registers the domain server-side after payment captures. ## Lifecycle 1. `POST /api/domains/check` — agent submits up to 20 candidate names; Cohesivity returns availability + price + a 10-minute signed quote token for each available one. 2. **Agent collects the user's contact info** (full name + email default from the Cohesivity Google-OAuth profile; the agent must additionally collect: address1, city, state, zip, country, phone). 3. `POST /api/domains/purchase` — agent picks one (passing the quote token AND the registrant contact); Cohesivity validates the contact, mints a Razorpay Payment Link, returns a `cohesivity.ai/t/` checkout URL. 4. Human pays via the URL (no Cohesivity sign-in required to pay). 5. Razorpay webhook → Cohesivity re-checks availability, calls Name.com to register **with the user's contact**, inserts a row in `tenant_domains`. If registration fails or the domain was taken in between, the payment is automatically refunded. 6. **Name.com emails the user** to verify their email address. The user must click the link within 15 days or ICANN policy requires their domain to stop resolving. Cohesivity does not send this email — Name.com does, directly. 7. `GET /api/domains` / `GET /api/domains/` — agent inspects what's on file, including the registrar order id, expiry, and `registrant_email`. ## Pricing - Wholesale comes from Name.com's `:checkAvailability` endpoint (USD). - Cohesivity applies a flat **10% markup** on the wholesale USD cost, then converts to INR paise at the configured FX rate (`DOMAIN_USD_INR_RATE_PAISE_PER_CENT`, default 83 paise per USD cent ≈ ₹83/$1). - The user-visible INR price is shown in the `/api/domains/check` response and embedded in the signed quote token. The token expires 10 minutes after issue — agents must re-check before purchase if the human takes longer than that to decide. - **Registration term**: most TLDs are 1-year; `.ai` is 2-year minimum per the registry. The `years` field in the check response tells you which; the quoted price covers the full term. - **v1 is one-shot — no renewals.** Cohesivity disables Name.com's auto-renew immediately after register, and the domain expires naturally at the end of the term. The user-billed renewal flow is a v2 surface; until then, agents should warn the user that the domain will not auto-renew. ## Supported TLDs Name.com supports 300+ TLDs including `.com`, `.dev`, `.app`, `.net`, `.org`, `.xyz`, `.ai`, `.io`, `.uk`, `.co`. **Not supported in this iteration:** `.in` (Name.com's API drops it from the response — confirmed against both sandbox and production). **Premium / aftermarket domains** are also gated out for v1; check responses surface them with `reason: "premium_not_supported"`. ## Endpoint reference ### Check availability curl -s -X POST https://cohesivity.ai/api/domains/check \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"domains":["yourapp.com","yourapp.dev","yourapp.app"]}' Returns one entry per requested name: { "registrar": "namecom", "currency": "INR", "markup_percent": 10, "quote_validity_ms": 600000, "domains": [ { "name": "yourapp.com", "available": true, "years": 1, "purchase_inr_paise": 118591, "currency": "INR", "quote_token": "eyJh...HS256.eyJ...exp", "quote_valid_until_ms": 1715543700000, "renewal_supported": false }, { "name": "yourapp.dev", "available": false, "reason": "taken" }, { "name": "best.com", "available": false, "reason": "premium_not_supported" } ] } `reason` values when unavailable: `taken`, `tld_unavailable`, `premium_not_supported`, `invalid_format`, `unknown`. ### Purchase The user's ICANN registrant contact is required in the body. firstName / lastName / email default from the Cohesivity Google-OAuth profile when omitted; the agent must collect the address fields and phone. curl -s -X POST https://cohesivity.ai/api/domains/purchase \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "quote_token": "eyJh...HS256.eyJ...exp", "registrant": { "firstName": "Asha", /* optional, defaults to OAuth name */ "lastName": "Patel", /* optional */ "email": "a@example.com", /* optional, defaults to OAuth email */ "phone": "+919876543210", /* REQUIRED, E.164 (dots/spaces/dashes are stripped) */ "address1": "12 MG Road", /* REQUIRED */ "address2": "Apt 4B", /* optional */ "city": "Bangalore", /* REQUIRED */ "state": "Karnataka", /* REQUIRED */ "zip": "560001", /* REQUIRED */ "country": "IN", /* REQUIRED, ISO 3166-1 alpha-2 */ "companyName": "Patel Inc" /* optional */ } }' Returns a Razorpay-backed checkout link AND a `wait` blob. Hand the `checkout_url` to the human; no Cohesivity sign-in is required to pay. Cohesivity registers the domain automatically on receipt of the Razorpay `payment_link.paid` webhook. If the registrant contact is missing fields or malformed, the response is `400 invalid_registrant_contact` with per-field errors and no Razorpay link is minted. ### Wait for payment + registration The `/api/domains/purchase` response includes a `wait` blob identical in shape to the topup + subscription wait blobs. Run the bash one-liner in `wait.command` after handing the user the checkout URL — it long-polls `GET /api/wait` (55s per call, looped) until the Razorpay webhook fires AND Name.com confirms registration. Terminal statuses: - `completed` — domain registered. Payload includes `domain_name`, `expires_at`, `registrar_order_id`, `registered_at`. - `denied` — registration failed (availability race after payment, or Name.com 4xx). Payment is auto-refunded. Payload includes `reason: "registration_failed_or_unavailable"`. - `expired` — the 30-minute wait-token TTL elapsed without the human paying. Mint a fresh purchase. - `pending` — still waiting (re-run; the agent's 5×55s loop in `command` handles this automatically). ### List domains owned by this tenant curl -s https://cohesivity.ai/api/domains \ -H "Authorization: Bearer " Returns every `tenant_domains` row keyed to the calling tenant, newest-first. The full registrant contact is **not** in the list response (PII redaction); only `registrant_email` is surfaced. ### Get a single domain curl -s https://cohesivity.ai/api/domains/yourapp.com \ -H "Authorization: Bearer " Returns 404 if the domain is not owned by this tenant (whether it doesn't exist or belongs to someone else — the response shape is intentionally the same). ## Common Mistakes - **Not telling the user about the 15-day verification email.** ICANN requires the registrant to confirm their email by clicking a link Name.com sends them after registration. **If they don't click within 15 days, their domain stops resolving.** Surface this in the UX immediately after purchase, and again in a follow-up if you can detect they haven't clicked. **Note**: in the Name.com sandbox the verification email is not actually sent (the sandbox is a functional API simulator — registrations, prices, and orders are realistic, but real-world side effects like emails, DNS propagation, and ICANN registry updates are mocked). Production registrations DO send the real ICANN verification email to the registrant within ~5 minutes. - **Holding a quote token longer than 10 minutes.** The signed token carries an `exp`; `/api/domains/purchase` rejects past it. Re-call `/api/domains/check` for a fresh quote. - **Sending phone in the wrong format.** Name.com's E.164 regex is strict: `^\+[1-9]\d{7,14}$` — no dots, no parens, no spaces. We strip those before validating, but the trailing must be all digits with a `+` shape. `+1.555.123.4567` becomes `+15551234567`; `(555) 123-4567` (no country code) is rejected. - **Trying to register `.in`.** Name.com's API doesn't support it. Surface a different TLD to the human; don't loop on the unsupported one. - **Re-trying purchase after webhook success.** If `GET /api/domains/` returns `200`, the domain is registered. A second `POST /api/domains/purchase` for the same domain will reject with `409 already_registered`. The webhook is the authoritative settlement path. - **Treating the domain as auto-renewing.** It does not. v1 disables auto-renew on Name.com's side right after register so we don't silently consume Cohesivity's wholesale balance. The user must rebuy before the term ends; surface this clearly in your UX ("domain expires on YYYY-MM-DD; renewals will be available before then"). ## Limitations (v1) - `.in` is silently dropped by Name.com's API. Surfaces as `tld_unavailable`. - Premium / aftermarket domains are refused at `/check`. Surface as `premium_not_supported`. - **No renewals.** Auto-renew is disabled at Name.com post-register; domains expire at the end of the term. A user-billed renewal flow is on the v2 roadmap. - Transfers (in and out) are not yet exposed through Cohesivity — handled manually via the Name.com dashboard. - The verification email goes to the user from Name.com directly. We don't pre-verify on their behalf (would require approved-reseller status at Name.com plus 2-year log retention — out of v1 scope). ## DNS records Manage A / AAAA / CNAME / MX / TXT / etc. records on a domain you own. All four endpoints require a claimed tenant + ownership of the domain (the `tenant_domains` row must exist for the calling tenant with `status=active`). **Supported types**: `A`, `AAAA`, `ANAME`, `CNAME`, `MX`, `NS`, `SRV`, `TXT`, `CAA`. **TTL** must be 300–86400 seconds; default 300. **priority** is required for `MX` and `SRV` records. ### List records curl -s https://cohesivity.ai/api/domains/yourapp.com/dns \ -H "Authorization: Bearer " Returns `{ tenant_id, domain_name, records: [...] }` — full Name.com record shape per entry (id, host, type, answer, ttl, priority). ### Create record curl -s -X POST https://cohesivity.ai/api/domains/yourapp.com/dns \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"host":"www","type":"CNAME","answer":"yourapp.com.","ttl":300}' Returns `201` with the created record (includes the assigned `id` — keep it; you need it for update/delete). ### Update record curl -s -X PUT https://cohesivity.ai/api/domains/yourapp.com/dns/ \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"host":"@","type":"A","answer":"192.0.2.42","ttl":300}' Full record body, same shape as create. Returns the updated record. ### Delete record curl -s -X DELETE https://cohesivity.ai/api/domains/yourapp.com/dns/ \ -H "Authorization: Bearer " Returns `{ deleted: true, record_id }`. ### DNS common mistakes - **Apex records**: use `"host": "@"`, NOT `""` or the bare domain name. Name.com rejects the latter two. - **Wildcards**: use `"host": "*"` for `*.yourapp.com`, or `"host": "*.sub"` for `*.sub.yourapp.com`. - **TTL minimum** is 300 seconds. Anything lower is rejected. - **MX / SRV priority** is required and must be 0–65535. Typical MX uses 10/20/30 for primary/secondary/tertiary. - **Propagation**: DNS is eventually consistent at recursive resolvers. Allow ~5 minutes after a write before testing with `dig`. ## Nameservers Repoint a Cohesivity-purchased domain to a DNS host other than Name.com (e.g. Cloudflare DNS, an external registrar's NS, your own resolver). Two endpoints — read and write. Setting nameservers transfers DNS authority off Name.com, so the records under `/api/domains//dns` stop being authoritative until/unless you repoint back to `dns1.name.com`/`dns2.name.com`. ### Get current nameservers curl -s https://cohesivity.ai/api/domains/yourapp.com/nameservers \ -H "Authorization: Bearer " Returns `{ tenant_id, domain_name, nameservers: [...], propagation_hint }`. ### Set nameservers curl -s -X POST https://cohesivity.ai/api/domains/yourapp.com/nameservers \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"nameservers":["ns1.dns.cloudflare.com","ns2.dns.cloudflare.com"]}' Body: a non-empty array of valid hostnames (max 13 entries, per registry policy). Idempotent — sending the same list again is a no-op at Name.com. Each entry is validated as a hostname; invalid entries surface as `400 invalid_nameservers` with per-index errors. **Propagation**: nameserver changes usually take 5–30 minutes; some recursive resolvers may cache for up to 48 hours. ## One-call Vercel attach (`/api/vercel/domains`) Attach a domain — whether Cohesivity-purchased or brought from an external registrar — to a tenant's Vercel project in a single call. The body's `source` field discriminates between the two paths: - **`source: "cohesivity-purchased"`** (default) — Cohesivity owns the registrar relationship via Name.com. We add the host to Vercel, create the A/CNAME records at Name.com on your behalf, poll Vercel's verify endpoint for ~10 s, and return the verification + SSL state. - **`source: "byod"`** — bring-your-own-domain. Your DNS is at an external registrar and Cohesivity cannot write records there. We add the host to Vercel, capture the records Vercel wants, and hand the list back to you in `records_to_add` so you can configure them at your own registrar. Verification then happens through the `/api/wait` long-poll. Same caller account scoping for both paths — a domain in tenant A can be attached to tenant B's Vercel project if both tenants are owned by the same Cohesivity account. ### Attach (Cohesivity-purchased) curl -s -X POST https://cohesivity.ai/api/vercel/domains \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"domain_name":"yourapp.com","include_www":true}' Body fields: - `domain_name` (required) — must be a Cohesivity-purchased active domain on the caller's account. - `tenant_id` (optional) — defaults to the calling tenant. Must be a tenant in the caller's account. - `include_www` (optional, default `false`) — when true, also attaches `www.` and creates the matching CNAME. - `source` (optional, default `"cohesivity-purchased"`) — pass `"byod"` for the BYOD path documented below. `201` on fresh attach, `200 idempotent:true` if the same domain is already attached to the same tenant + project. Both shapes carry: { "tenant_id": "sweet-oriole-naming", "domain_name": "yourapp.com", "vercel_project_id": "prj_...", "source": "cohesivity-purchased", "include_www": true, "verified": true, "verification_records": [...], "managed_dns_record_ids": ["...", "..."], "attached_at": "2026-05-15T...", "dns_propagation_hint": "DNS A/CNAME records usually propagate in 1–5 minutes...", "ssl_issue_hint": "SSL certificates usually issue within 1–5 minutes after DNS verifies." } `verified: true` means Vercel has confirmed the DNS records point at it; the Let's Encrypt cert is issued asynchronously by Vercel within ~1–5 minutes after that, but is **not** directly observable from this API. Confirm cert issuance with a TLS handshake (`curl -sI https:///`) — Vercel will respond with HTTP headers once the cert lands. If verify is still pending after ~10 s, `verified` is `false` — re-check with `GET /api/vercel/domains/`. ### Attach (bring-your-own-domain) For a domain whose DNS is at an external registrar (Cloudflare, GoDaddy, Squarespace, etc.), pass `source: "byod"`. Cohesivity adds the host to Vercel, captures the records Vercel demands, and hands the list back to you. You then add those records yourself at your registrar; Cohesivity never writes upstream DNS for BYOD. curl -s -X POST https://cohesivity.ai/api/vercel/domains \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"domain_name":"yourapp.com","source":"byod","include_www":true}' Returns `201` (or `200 idempotent:true` on re-attach) with `verified: false` and the records the human must create at their registrar: { "tenant_id": "sweet-oriole-naming", "domain_name": "yourapp.com", "vercel_project_id": "prj_...", "source": "byod", "include_www": true, "verified": false, "records_to_add": [ { "host": "@", "type": "A", "value": "76.76.21.21", "ttl": 300 }, { "host": "www", "type": "CNAME", "value": "cname.vercel-dns.com.", "ttl": 300 } ], "records_to_remove": [], "attached_at": "2026-05-15T...", "dns_propagation_hint": "DNS A/CNAME records usually propagate in 1–5 minutes...", "ssl_issue_hint": "SSL certificates usually issue within 1–5 minutes after DNS verifies.", "wait": { "endpoint": "...", "auth_header": "Authorization: Bearer ...", "command": "...", "type": "domain_byod", "expires_at": "..." } } Hand the `records_to_add` list to the human. They add the records at their registrar. Then run the `wait.command` bash one-liner to long-poll `/api/wait` until Vercel's verify endpoint flips to `verified: true`. Terminal statuses on the wait: - `completed` — Vercel verified the DNS records point at it. The Let's Encrypt cert is issued asynchronously by Vercel within ~1–5 minutes; this API does **not** surface a separate cert-issued flag (`verified: true` only confirms routing). To confirm the cert is live, do a TLS handshake against the domain (`curl -sI https:///`) — Vercel responds with HTTP headers once the cert lands. - `expired` — the wait token's 1-hour TTL elapsed without verification. The `vercel_custom_domains` row persists; the agent can either re-call `POST /api/vercel/domains` (idempotent re-attach mints a fresh wait token) or poll `GET /api/vercel/domains/` periodically. - `pending` — still waiting (re-run; the agent's 5×55s loop in `command` handles this automatically). Vercel may return extra `TXT` verification rows alongside the apex `A` and `www` `CNAME` — those appear in `records_to_add` too and must also be added at the registrar. They're typically only emitted when Vercel needs domain-ownership proof (most often when the domain was previously attached to another Vercel project). ### Get binding curl -s https://cohesivity.ai/api/vercel/domains/yourapp.com \ -H "Authorization: Bearer " Returns the current binding (including `verified` + `ssl`). `404` if no binding exists on the caller's account. ### Detach curl -s -X DELETE https://cohesivity.ai/api/vercel/domains/yourapp.com \ -H "Authorization: Bearer " Removes the Vercel binding (apex + `www` if it was added). On the `cohesivity-purchased` path, also deletes the DNS records Cohesivity auto-created during attach; any DNS records you added manually under `/api/domains//dns` are untouched. On the `byod` path, Cohesivity does not touch your external DNS — the response includes `records_to_remove` listing what you should clean up at your own registrar. { "detached": true, "domain_name": "yourapp.com", "source": "byod", "dns_errors": [], "vercel_errors": [], "records_to_remove": [ { "host": "@", "type": "A", "value": "76.76.21.21" }, { "host": "www", "type": "CNAME", "value": "cname.vercel-dns.com." } ] } Non-empty `dns_errors` or `vercel_errors` arrays indicate cleanup state to investigate. ### Vercel-attach common errors - **`409 domain_already_attached`** — the domain is attached to a different Vercel project on this same Cohesivity account. The response includes `current_binding` and `next_steps`. Detach the current binding first, then re-attach to the new tenant. - **`409 domain_already_attached_other_account`** — a `vercel_custom_domains` row exists for this domain under a different Cohesivity account. The response intentionally does not leak the other account's `tenant_id` or `vercel_project_id`. The user (or whoever holds that other account) must detach there first. - **`409 domain_already_attached_external`** — Vercel reports the host is in use by a project outside Cohesivity (e.g. a manual setup from before this flow existed, or a different team's project). Remove it from the other Vercel project, then retry. - **`409 domain_is_cohesivity_purchased`** (BYOD path only) — the domain is in `tenant_domains` for this account, meaning the user already owns it through Cohesivity. Use the cohesivity-purchased path instead (omit `source` or pass `source: "cohesivity-purchased"`). - **`409 domain_attached_via_different_source`** (BYOD path only) — the domain is already attached to the same tenant + Vercel project on the `cohesivity-purchased` path. Detach first if you want to switch to BYOD. - **`409 no_vercel_project`** — the target tenant doesn't have an active Vercel hosting project. Provision one via `POST /api/vercel/provision` first. - **`404 domain_not_owned`** (cohesivity-purchased path only) — the domain isn't in `tenant_domains` for this account. If the user's domain lives at an external registrar, retry with `source: "byod"`. - **`403 cross_account_forbidden`** — the body's `tenant_id` resolves to a different Cohesivity account. ## Auth All endpoints require a `coh_management_key` (`Authorization: Bearer ...`). **Lifecycle gate**: `/api/domains/check` is open to ephemeral tenants — browsing pricing is harmless and useful as a "should I claim before I commit?" signal. Every other domain endpoint (purchase, list, get, the four DNS routes) requires a **claimed** tenant. Ephemeral tenants auto-terminate at the 72-hour claim window and the `tenant_domains` row would CASCADE-delete on termination, but the Name.com registration would survive — the user would have paid for a domain we no longer have a record of. Gating purchase + management to claimed tenants closes that hole. If a call returns `403 tenant_must_be_claimed`, claim the tenant first using the `claim_url` in the response.