# Domains

Every domain in the workspace is one entry, keyed by its root (`GET /api/domains`), with one plain-word state and the next step. www is served together with the apex.

## In plain words

Your own web address, like yourshop.com. Search for one, buy it from your balance and it is set up to point at your project, or connect one you already own.

For example: A bakery buys freshbakes.com, and its site answers there.

## Buy, connect or transfer?

| You want to | Do this |
| --- | --- |
| A new name | Buy it here. It is paid from the Balance, and can connect to a project in the same call. |
| Use a name you own elsewhere | Connect it: add it to a project and switch its nameservers to the two Harakumo gives you. |
| Move a name you own to Harakumo | Transfer it in with its auth code; the price includes a year of renewal. |

## Buying

1. Search: `GET /api/domains/search?q=myshop` (or `myshop.org` to search that name first). Each result has `available` (null means it could not be checked — not taken), `sellable`, `price_cents`, `renewal_price_cents` and, for a name you can buy, `lock_lifts_at`.
2. Buy: `POST /api/domains/registrations { domain, projectId? }`. The live price is checked again; if it is above the price you saw (`expectedPriceCents`) nothing happens.
3. With a projectId the domain connects automatically and goes live within minutes to hours as DNS settles; the owner gets an email when it is live.

- Endings sold today: .com, .dev, .app, .io, .ai, .cloud. Automatic purchases above $100 a year are refused.
- Every domain is paid from the Balance at the price shown before you buy, on every plan. There is no cap on how many you buy — each is paid for.
- A Balance too small answers 402 `insufficient_balance` with the shortfall and `add_funds_url`, the page where funds are added; nothing is charged. The card on file is charged only when the owner, signed in to the dashboard, asks (`payWithCard: true`): see Who pays, and how. That answer offers the card (`pay_with_card`) only to the owner signed in to the dashboard; anyone else, an AI connector or API key included, gets `pay_with_card: null` and is told the owner pays by card on the Domains page.
- Purchases count toward the workspace's monthly spending limit, if one is set.
- Search results and renewal prices come from the registrar's price list, which Harakumo refreshes once a day; a purchase checks the live price again before anything is charged.
- Harakumo registers the domain through its registrar account and manages it for your workspace. Harakumo is the registrant of record (the registration lists Harakumo, not you, as the holder, and your details are not sent to the registrar); your workspace is the record that the domain is yours, and you can transfer it out to hold it in your own name. Registries lock a domain for 60 days after registration or transfer.
- That lock means a domain you buy cannot move to another registrar until `lock_lifts_at`, 60 days after the purchase; it works as usual meanwhile. The buy dialog says so before you pay, and search results, the dry run (`dryRun: true`), the purchase answer and every registration carry the date.
- Prices are in US dollars and do not include taxes such as sales tax, VAT or GST.

## Who pays, and how

- Anyone who manages domains: developers, admins and owners (in the dashboard, or through their AI connector with write access) and full workspace API keys. AI connectors and API keys always pay from the Balance.
- The card is the owner's, so it takes the owner in person. An AI connector, the owner's own included, is a long-lived token another app holds, and an API key belongs to an app: asked to use the card, any of them that manages domains gets `card_requires_owner` (402, or 403 for `cardBackup`) with `dashboard_url`, the Domains page where the owner can do it, and nothing is charged or changed. A read-only connection or read key is told it is read-only.
- When the Balance is short, an AI connector or API key gets `add_funds_url` to hand to the person, who adds funds in a browser; then the call is made again. An owner's AI connector can also make a checkout link for that (`create_add_funds_link`).
- In a workspace another workspace's plan covers, its domains are paid from its own Balance, under its own spending limit, not from the paying workspace's (which pays for its AI): `add_funds_url` opens that own Balance (Billing → Balance), `add_funds_api` is `POST /api/billing/balance { "cents": 1000, "own": true }` (it charges the card on file, so it takes the owner signed in: an AI connector or API key that calls it gets `dashboard_only`, whose `dashboard_url` is that own Balance), and the checkout link takes `own: true` (`create_add_funds_link`, or `POST /api/billing/balance/checkout { "cents": 1000, "own": true }`), which a connector can make for the owner to open and pay.

| Call | From the Balance | With the card on file |
| --- | --- | --- |
| Buy: `POST /api/domains/registrations` | Anyone who manages domains | `payWithCard: true`, or `autoRenewCard: true` for its renewals: the owner signed in to the dashboard |
| Renew now: `PATCH /api/domains/registrations/:id { action: "renew" }` | Anyone who manages domains | `payWithCard: true`: the owner signed in to the dashboard |
| Auto-renew: `PATCH /api/domains/registrations/:id { action: "auto-renew", enabled }` | Anyone who manages domains | `cardBackup: true`: the owner signed in to the dashboard |
| Transfer in: `POST /api/domains/transfers` | Anyone who manages domains | `payWithCard: true`: the owner signed in to the dashboard |

## Connecting a domain you own

1. Add it to a project: `POST /api/projects/:id/domains { hostname, connect: true }`. Harakumo checks no other workspace owns that root domain. Adding the domain itself (example.com) serves www.example.com too, unless www is added as a hostname of its own; a subdomain serves only itself.
2. Harakumo copies the records your domain answers with today (email included) into its DNS, and returns two nameservers. Review the copied records in DNS before switching.
3. Set those nameservers at your registrar. The domain goes live on its own within about 10 minutes of the change being visible — there is nothing to press.

> Switching nameservers moves all DNS for the root domain, even when you only connect a subdomain. Connecting by CNAME is not available yet.

## DNS records

Any domain whose DNS Harakumo hosts — bought, transferred in or connected by nameservers — has editable records at `/api/domains/zones/{root}/dns` (GET, POST, PATCH, DELETE), and the same DNS panel under Manage on the Domains page. Record types: A, AAAA, CNAME, MX, TXT and NS. SRV and CAA are not supported yet.

**Add an MX record**

```bash
curl -X POST https://harakumo.com/api/domains/zones/myshop.com/dns \
  -H "Authorization: Bearer $HARAKUMO_API_KEY" -H "content-type: application/json" \
  -d '{ "type": "MX", "name": "@", "content": "mail.example.net", "priority": 10 }'
```

> Want email from this domain? Mail → Sending domains sets it up (automatically when its DNS is here).

## Renewal

- Bought domains auto-renew by default (`autoRenew: false` at purchase to opt out). Renewal happens in the last 7 days before expiry, at `renewal_price_cents`, paid from the Balance. Warnings go out 30, 7 and 1 days before.
- The card covers a renewal only if the owner, signed in to the dashboard, turned on card backup (`autoRenewCard: true` at purchase, or `PATCH { action: "auto-renew", enabled: true, cardBackup: true }`). Renewals set up by developers, API keys, AI connectors or transfers pay from the Balance only.
- A domain past expiry is marked expired; renewing it within 30 days points it back at its DNS.
- Every renewal is paid at its renewal price, on every plan. The renew answer says what was charged and where from (`chargedCents`, `paidFrom`), and the expiry email quotes the price of an automatic renewal.
- Renewals above $100 a year are never automatic: the daily renewal, the API, AI connectors and other roles stop there (403 `over_cap`, with `dashboard_url`), and the owner is emailed. The workspace owner, signed in to the dashboard, renews it by hand on the Domains page at the price shown; that call carries `expectedPriceCents` at or above the price (409 `confirm_price` without it, 409 `price_moved` when the price went up).

## Stop serving, DNS only, other nameservers

- `DELETE /api/domains/registrations/:id` (or `PATCH { action: "detach" }`) stops serving the project. DNS, email and renewal stay.
- `PATCH { action: "host-dns" }` hosts DNS here without a project; `{ action: "set-nameservers", nameservers: [...] }` points the domain at another provider; `{ action: "connect", projectId }` serves another project.
- Deleting a project detaches its domains; bought domains stay in the workspace and keep renewing.
- `GET /api/domains/registrations/:id?include=registrar` adds `at_registrar`: what the registrar holds for the domain right now, read live — its transfer lock (true, false, or null when it could not be read) and the nameservers it points at, whether those are this domain's Harakumo DNS and who runs them. The Domains page shows the same for every domain the workspace holds. The lock is not changed here: a transfer-out request has it lifted.

## Transfers

- In: unlock the domain at your current registrar, get its auth code, and `POST /api/domains/transfers { domain, authCode, dryRun: true }` to validate and quote without paying. Then send it again without dryRun (with `expectedPriceCents`). The Balance pays once (the card only when the owner, signed in to the dashboard, sends `payWithCard: true`); the transfer completes in 5–7 days.
- Out: `PATCH /api/domains/registrations/:id { action: "transfer-out" }`. The auth code arrives within five calendar days (usually sooner), by email and in the dashboard. The domain keeps working until it has moved.

## Limits

- Your own domains connected per plan: 2 hobby, 20 pro, 100 team, 500 scale.
- Up to 10 new DNS zones per workspace a day, and 20 a day across all the workspaces one person owns. Connecting a domain the workspace already holds is never refused by either.
- Domain routes are workspace-wide: use a workspace key. Adding a hostname to a project (`/api/projects/:id/domains`) accepts that project's key.

## Reference

**SDK**

```js
const { results } = await hk.domains.search('myshop');     // [{ domain, available, sellable, price_cents, renewal_price_cents }]
const pick = results.find(r => r.available && r.sellable);
const { registration, paidFrom } = await hk.domains.register(pick.domain, { projectId: project.id, expectedPriceCents: pick.price_cents });
// 402 insufficient_balance → err.body.shortfall_usd, err.body.add_funds_url

await hk.domains.add(project.id, { hostname: 'app.myshop.dev' });   // a domain you own; connect: see REST
const { registration: one } = await hk.domains.registration(registration.id);   // renewal_price_cents, lock_lifts_at
const check = await hk.domains.transferIn('myshop.com', { authCode: 'EPP-CODE', dryRun: true });
```

**CLI**

```bash
harakumo domains buy myshop.com 12    # from the Balance; the CLI never pays with the card
```

**REST**

```http
GET    /api/domains                         # every domain, one per root, with state, next step and renewal
GET    /api/domains/search?q=myshop
POST   /api/domains/registrations           { "domain", "projectId"?, "expectedPriceCents"?, "autoRenew"?, "autoRenewCard"?, "payWithCard"? }
GET    /api/domains/registrations           # each with renewal_price_cents, lock_lifts_at and state
GET    /api/domains/registrations/:id       # one, with renewal_price_cents and lock_lifts_at; ?include=registrar adds at_registrar
PATCH  /api/domains/registrations/:id       { "action": "connect", "projectId" } | "detach" | "host-dns" | "set-nameservers" | "renew" | "auto-renew" | "transfer-out" | "cancel-transfer-out"
DELETE /api/domains/registrations/:id       # stop serving (DNS and renewal stay)
POST   /api/projects/:id/domains            { "hostname", "connect": true }   # → nameservers
POST   /api/domains/:id                     # check a connected domain now (the sweep also does)
GET|POST|PATCH|DELETE /api/domains/zones/:root/dns
POST   /api/domains/transfers               { "domain", "authCode", "projectId"?, "dryRun"?, "expectedPriceCents"? }
GET    /api/domains/transfers · PATCH /api/domains/transfers/:id { "action": "check"|"replace-code"|"cancel" }
```
