# Custom domains and allowed origins

> The DNS records (CNAME and TXT) for serving your site on your own domain (www.example.com), what to do with the bare domain, and what each status and error means. How to register the domains your forms accept (allowed origins) and how to fix 403 origin_not_allowed. Includes steps for AI agents.

URL: https://siteon.dev/docs/domains

Siteon is infrastructure for AI-built websites: it adds hosting, a CMS your client can edit, contact forms, analytics and control from AI agents (CLI and MCP) around static HTML built with AI or by hand, so it is ready to ship.

This page covers two things.

| You want to | Read |
|---|---|
| Show a site hosted on Siteon on your own domain (`www.example.com`) | [Custom domains](#custom-domain) |
| Fix forms that fail with `403` / `origin_not_allowed`, or register the domain of the site your form lives on | [Allowed origins for forms](#allowed-origins) |

When a custom domain becomes active on Siteon hosting, it is added to the allowed origins automatically. If you only put a form on a site hosted elsewhere (Cloudflare Pages, Vercel and so on), you only need the allowed origins.

## Custom domains (coming soon)

> Custom domain requests are not open yet (coming soon). This section describes the steps for when they open. Until then you can publish at `https://<name>.siteun.com/`.

On your own domain, **`www.example.com` is the main host (primary) and `example.com` (without www) redirects to `www`**.

### Before you start

- The site must be on a **paid plan**. Lite allows 1, Standard 3 and Business 10. `www.example.com` and `example.com` together count as one. Not available on Free (`plan_required`)
- **Hosting must be enabled** for the site (`siteon hosting enable`; see [Hosting and delivery](https://siteon.dev/docs/hosting))
- In the admin, the site's owner can do this. With an API key (CLI, MCP) you need the `domains:write` scope. It is not granted by default, so choose it explicitly when you create the key (`siteon login --scopes +domains:write`)
- You must be able to edit the domain's DNS. The company you bought the domain from (the registrar) is not always where the DNS is edited: edit it **where the nameservers point**
- An account can request up to 5 domains a day (days counted in Japan time)

### Steps

1. In the admin, open "Publishing" in the sidebar, type `www.example.com` in the "Custom domains" box and press "Add" (CLI: `siteon domains add www.example.com --site <site-id>`)
2. Add the **two records** shown on screen (`TXT` and `CNAME`) to your domain's DNS ([Records to add](#dns-records))
3. Wait. Siteon checks the DNS and the certificate automatically every 5 minutes; once both are in place the status becomes "Active" and the site is served on your domain. Reopening "Publishing" in the admin checks again on the spot (CLI: `siteon domains status`)
4. Redirect `example.com` (without www) to `www.example.com` ([The bare domain](#apex))

If the records cannot be verified within 72 hours of the request, the request is cancelled (the status becomes "Failed"). Add the domain again in that case.

### Records to add

For a request for `www.example.com` you add these two records. The value changes with every request, so **always copy exactly what the screen (or the CLI / API response) shows**.

| Type | Name (host) | Value |
|---|---|---|
| `TXT` | `_sitekit-verify.www.example.com` | The value shown on screen, starting with `sitekit-verify=` |
| `CNAME` | `www.example.com` | `cname.siteun.com` |

- **The TXT record proves ownership.** It is required for every request and its value changes with each one. **Do not delete it** after the domain is active: Siteon keeps checking every 5 minutes, and if the TXT record disappears the domain stops being served at once (and drops out of the allowed origins)
- **The CNAME record points traffic at Siteon.** Any record that `www.example.com` has now (`A`, `AAAA`, another `CNAME`) cannot coexist with the CNAME. Remove it, then add the CNAME. The old site disappears as soon as you switch the CNAME, and `https://` may not open for a few minutes until the certificate is issued. **Add the TXT record first and switch the CNAME at a quiet time** to keep the gap short
- Most DNS screens append the domain (`.example.com`) to the name field automatically. Then enter only `www` and `_sitekit-verify.www`. Entering `www.example.com` there would create `www.example.com.example.com`
- Leave mail (`MX`) and any other records that are not `www` alone
- For internationalized domain names, request and enter the `xn--` form (Punycode)

After adding them, you can check that the DNS serves them:

```sh
dig +short CNAME www.example.com                  # OK if it prints cname.siteun.com.
dig +short TXT _sitekit-verify.www.example.com    # OK if it prints the value on screen
```

#### Examples by DNS provider

Screen and menu names change from time to time. Whatever the provider, you **add two rows in its DNS records screen: "type CNAME, host `www`, value `cname.siteun.com`" and "type TXT, host `_sitekit-verify.www`, value as shown on screen"**.

- **Cloudflare** — In the domain's "DNS" → "Records", use "Add record". CNAME: Name `www`, Target `cname.siteun.com`, and **set Proxy status to "DNS only" (grey cloud)**. TXT: Name `_sitekit-verify.www`, Content the value on screen
- **Onamae.com** — In Onamae.com Navi's DNS features ("DNS settings", "DNS record settings"), add host `www`, TYPE `CNAME`, VALUE `cname.siteun.com`, and host `_sitekit-verify.www`, TYPE `TXT`, VALUE the value on screen. This screen works when the domain uses Onamae.com's nameservers
- **Muumuu Domain** — In "Muumuu DNS" custom settings, add subdomain `www`, type `CNAME`, content `cname.siteun.com`, and subdomain `_sitekit-verify.www`, type `TXT`, content the value on screen
- **Xserver Domain** — In the domain's "DNS record settings", add host `www`, type `CNAME`, content `cname.siteun.com`, and host `_sitekit-verify.www`, type `TXT`, content the value on screen. If the nameservers point at a rental server (such as Xserver), use that server's DNS record settings instead
- **Sakura Domain** — In the member menu's "zone edit" for the domain, add entry `www`, type `CNAME`, value `cname.siteun.com.` (some screens need the trailing dot), and entry `_sitekit-verify.www`, type `TXT`, value the value on screen
- **Squarespace (formerly Google Domains)** — In the domain's "DNS" settings, under custom records, add Host `www`, Type `CNAME`, Data `cname.siteun.com`, and Host `_sitekit-verify.www`, Type `TXT`, Data the value on screen

With Cloudflare's proxy turned on (orange cloud), requests to `www.example.com` pass through your own Cloudflare settings before reaching Siteon, which can stall certificate issuance and the checks. Siteon already serves your domain and its certificate through Cloudflare, so there is no need to turn the proxy on.

### The bare domain (example.com)

Ordinary DNS does not allow a CNAME on `example.com` itself (the apex). Redirect it to `www.example.com` in one of these two ways.

**A. Your DNS can do CNAME flattening (ALIAS, ANAME)**

1. Add `www.example.com` first (if you add only `example.com` with no primary host, there is nothing to redirect to and it answers `404`)
2. Add `example.com` as well. Together with `www.example.com` it counts as one, so it uses no extra slot. It becomes "Redirects to primary" automatically
3. Add the `TXT` record shown (`_sitekit-verify.example.com`) and a `CNAME` on the apex (name `@` or blank) → `cname.siteun.com`. Remove any `A` / `AAAA` records on the apex (`MX` and `TXT` can stay)
4. Once active, `https://example.com/…` redirects to `https://www.example.com/…` (path and query preserved)

**Cloudflare** flattens apex CNAMEs as standard, so this method works there. For other providers, check their documentation for whether they offer ALIAS / ANAME.

**B. Your registrar's URL forwarding**

Use your DNS provider's "URL forwarding" (web forwarding, redirect settings and so on) to forward `example.com` to `https://www.example.com`. Do not add `example.com` to Siteon (it uses no slot). Forwarding is the provider's own service, so whether `https://example.com` opens without a certificate error and whether the path is carried over depends on that provider.

You can also make `example.com` the main host instead of `www` (mark it primary when you add it; `--primary` in the CLI). We recommend `www` as the main host because it has fewer DNS restrictions.

### What the statuses mean

| Status (API value) | Meaning | What to do / how long |
|---|---|---|
| Received (`requested`) | The request was received and serving is being prepared | Just wait. Usually seconds to a few minutes until "Waiting for DNS" |
| Waiting for DNS (`pending_dns`) | The CNAME or TXT record cannot be verified yet | Add both records. DNS changes take from minutes to a few hours to show up (depends on the provider and the TTL); the next 5-minute check then moves it on |
| Waiting for the certificate (`pending_ssl`) | DNS is verified and the `https://` certificate is being issued | Just wait. Usually minutes to tens of minutes |
| Active (`active`) | The site is served on this domain, and the domain is in the allowed origins automatically | Keep the TXT and CNAME records. Each passing 5-minute check extends it by 72 hours |
| Failed (`failed`) | Not verified within 72 hours of the request, so the request was cancelled | Add it again (the TXT value is new, so replace the old TXT record with the new value) |

- If the TXT or CNAME record disappears after "Active", the status goes back to "Waiting for DNS" at once and the domain stops being served (it also leaves the allowed origins). Put the record back and the next check makes it "Active" again, with no new request
- If the check cannot run because of a temporary problem on Siteon's or Cloudflare's side, serving does not stop (for up to 72 hours since the last successful check)
- Times are rough guides. With slow DNS providers it can take around half a day

### Removing a domain and changing the main host

- **Remove** — "Remove" under "Publishing" → "Custom domains" in the admin (CLI: `siteon domains remove www.example.com --yes`). The domain stops being served at once and this cannot be undone. To use it again, add it and verify the DNS again (with a new TXT value). The slot frees up once Siteon has finished cleaning up (usually a few minutes). Delete DNS records you no longer need yourself. If you remove the main host, a domain set to redirect (`example.com`) has nowhere to go and answers `404`
- **Change the main host** — `siteon domains primary <domain> --yes` (MCP: `domains_primary`). Choose an active domain. The other domains redirect to the new main host at once, and the pages' canonical URL changes
- While a custom domain is the active main host, the shared subdomain (`<name>.siteun.com`) keeps serving, but it is set to stay out of search (`noindex`) and names the custom domain as the canonical URL

### Errors

The admin shows a plain-language message; the CLI, API and MCP put a code in `error`.

| Code | Meaning | Fix |
|---|---|---|
| `plan_required` (402) | The site's plan has no custom domains (Free) | Move the site to a paid plan (Lite or higher) |
| `quota_hostnames` (402) | The plan's slots are used up (`limit` and `used` are returned) | Remove a domain you do not use, or upgrade. A just-removed domain still counts for a few minutes until clean-up finishes |
| `invalid_hostname` (400) | The domain is malformed (`reason` is `bad_format`), or it is a siteun.com name (`base_domain`) | Enter it as `www.example.com`, without `https://`, a trailing `/` or a path. Allowed characters are lowercase letters, digits and hyphens (internationalized names in their `xn--` form) |
| `hostname_blocked` (400) | Names that combine a brand name with words like login or verify (shapes common in impersonation) cannot be registered | Use a different domain |
| `hostname_reserved` (409) | Another account has requested or is using the domain (`reserved_until` shows the deadline of a pending request) | If the domain is yours, request it after the deadline (72 hours from that request). If no deadline is shown (it is active elsewhere), contact Siteon |
| `hostname_in_use` (409) | It is registered on another site in the same account (`site_id`) | Remove it from that site first |
| `too_many_domain_requests` (429) | An account can request up to 5 domains a day | Try again the next day (after the date changes in Japan time) |
| `hosting_disabled` (409) | Hosting is not enabled for the site | Run `siteon hosting enable` first |
| `billing_inactive` (402) | Payment could not be confirmed | Check billing in the admin |
| `account_held` / `transferring` (409) | Publishing is held for the account / the site is being transferred | Check the hold notice email / try again after the transfer |
| `hostname_not_live` (409) | The domain you tried to make primary has failed or was removed | Start again by adding it |
| `site_changed` (409) | Another operation got there first | Read the state again and retry |
| `insufficient_scope` (403) | The API key lacks `domains:write` | Create a key with `domains:write` (`siteon login --scopes +domains:write`) |
| `confirmation_required` (400) | Removing a domain or changing the main host with an API key needs confirmation | Ask the person first, then pass `confirm: true` (`--yes` in the CLI) |

## Allowed origins for forms

The allowed origins (`allowed_origins` in the API) are **the domains of the pages that may carry this site's forms**. Submissions sent from pages on any other domain are refused. This stops other people from putting your form on their own site and using up your submission quota. Page-view counting uses the same list.

- In the admin: the site's "Settings" → "Domains and site name", under "Domains that may submit forms". One per line, then "Save". The site's owner can change it. It takes effect as soon as you save
- In the admin you can leave out `https://` (`localhost` is taken as `http://`). If you type a path, only the domain part is registered
- Up to 20 entries
- **Only exact matches are accepted.** `https://www.example.com` and `https://example.com` are different, so add both if you use both. `http://` versus `https://` and different port numbers are different too. Wildcards like `*` cannot be used (preview URLs that change every time, such as Cloudflare Pages' `<hash>.<project>.pages.dev`, must be added one by one, or use a fixed preview URL)
- Submissions without an `Origin` header (curl, servers) are accepted regardless of the allowed origins

### Origins added automatically

These two are accepted without being in the list (they do not appear in the admin's box):

- The site's shared subdomain on Siteon hosting (`https://<name>.siteun.com`)
- Custom domains that are "Active" (`https://www.example.com`, and a redirecting `https://example.com` too). They drop out automatically when they stop being active. `auto_origins` in `siteon domains status` shows what is in right now

### What Free sites can register

A Free site can only register development and preview domains. Anything else (your own domain) is refused with `402 upgrade_required`; move the site to a paid plan first.

```text
localhost
127.0.0.1
*.pages.dev
*.vercel.app
*.netlify.app
*.github.io
*.workers.dev
```

`*.pages.dev` here means "names of this shape can be registered". What is accepted is still only the individual domains you put in the list.

### Changing them with the API

With an API key (scope `sites:write`) you **replace** the site's allowed origins. The list you send becomes the new list, so send the current list (readable with `GET`, scope `sites:read`) plus your additions.

```sh
curl -X PATCH "https://cdn.siteon.dev/v1/accounts/<account-id>/sites/<site-id>" \
  -H "Authorization: Bearer stk_..." \
  -H "Content-Type: application/json" \
  -d '{"allowed_origins":["https://www.example.com","https://example.com","https://my-shop.pages.dev"]}'
```

- Write each one as `https://www.example.com`. A trailing `/`, a path or uppercase letters are refused (`400`), as are more than 20 entries
- With a personal key (one made by `siteon login`), you must be an owner or admin of the account
- Sending a non-development domain for a Free site gives `402 upgrade_required`

### Fixing 403 origin_not_allowed

If you see either of the following, **the domain of the page with the form is not in the allowed origins**. The submission is not saved and does not count toward your quota.

- The form says it could not be sent, and the browser's developer tools console shows a CORS error (no `Access-Control-Allow-Origin` header, for example). For submissions from the Siteon script, the browser stops at its pre-flight check, so the submission is never sent
- With your own sending code (`fetch`, a plain `<form>` post and so on), the response is `403` with `{"ok":false,"error":"origin_not_allowed"}`

1. Open the page with the form and run `location.origin` in the developer tools console to get the page's exact domain (for example `https://www.example.com`)
2. Compare it with "Settings" → "Domains and site name" → "Domains that may submit forms" in the admin. Check `www` or not, `http` versus `https`, and the port
3. If it is missing, add it and save. It takes effect at once, so reload the page and submit again
4. If you cannot add it
   - "Production domains are available on a paid plan" (`402 upgrade_required`) — the site is on Free. Move it to a paid plan, then add the domain
   - "Up to 20 domains" — remove the ones you do not use
5. For a custom domain on Siteon hosting, you do not need to add it: it is added automatically once "Active". If it is still "Waiting for DNS" or similar, see [What the statuses mean](#status) and fix the domain instead

## When an AI agent uses this page

Steps for when a person asks "set up my custom domain" or "my form returns 403". The MCP tools are in preparation (coming soon) (until then, walk the person through the admin).

### When asked to set up a custom domain

1. Confirm with the person: the domain (as `www.example.com`), that they own it, which company hosts its DNS (Cloudflare, Squarespace and so on), and that the site is on a paid plan with hosting enabled. **Never request a domain the person does not own**
2. Call `domains_add` (`site`, `hostname: "www.example.com"`). If the key lacks `domains:write` you get `insufficient_scope`; ask the person to create a new key with `siteon login --scopes +domains:write`
3. Show the person `dns.txt` (`name`, `value`) and `dns.cname` (`name`, `target`) from the response **exactly as returned**. Add the form the name field takes at their provider (`_sitekit-verify.www` and `www`; see [Records to add](#dns-records) and [Examples by DNS provider](#dns-examples)). On Cloudflare, tell them to use "DNS only". Tell them to add the records before `expires_at` (72 hours later)
4. If the response contains `next_steps`, **follow `next_steps`** over this page
5. When the person says the records are in, call `domains_status` (it checks on the spot). Leave 1 to 5 minutes between repeated calls
6. Next step by status ([What the statuses mean](#status))
   - `requested` — wait a little and call `domains_status` again
   - `pending_dns` — look at `last_check_detail`. `txt` of `mismatch` / `nxdomain` means the TXT name or value was copied wrong; `cf` of `ok` with a `cf_status` other than `active` means the CNAME does not point at us. Ask the person for the `dig` output. If the records are fresh, wait for DNS to update
   - `pending_ssl` — wait (minutes to tens of minutes)
   - `active` — done. Ask the person to open `https://www.example.com/` to check, and to keep the TXT and CNAME records
   - `failed` — 72 hours passed. After confirming with the person, call `domains_add` again and have them replace the TXT record with **the new value**
7. Ask about the bare domain ([The bare domain](#apex)). If their DNS can do CNAME flattening (Cloudflare and others), confirm `www` is already added, then call `domains_add` with `hostname: "example.com"`. Otherwise, guide them to forward it to `https://www.example.com` with the registrar's URL forwarding
8. Look up error codes in [Errors](#domain-errors). Call `domains_remove` and `domains_primary` only after telling the person what will happen and getting an explicit yes, with `confirm: true`

### When told a form returns 403

1. Check that the symptom is `403` / `origin_not_allowed` or a CORS error in the console ([Fixing it](#origin-not-allowed)). `429` or `400` has a different cause ([Common errors](https://siteon.dev/docs/troubleshooting))
2. Ask the person for the exact domain (`location.origin`) of the page with the form
3. Read the site's `allowed_origins` with `list_sites` and look for an exact match (`www` or not, `http` / `https`, port)
4. If it is missing, ask the person to add it under "Settings" → "Domains and site name" in the admin. To add it through the API, confirm with the person first and `PATCH` the current list plus the new entry ([Changing them with the API](#origins-api)). A Free site cannot take your own domain (`upgrade_required`), so explain that a paid plan is needed
5. For a custom domain on Siteon hosting, do not add it to the allowed origins; check its state with `domains_status` instead (it is added automatically once active)
