Sites

A site is the top-level object you own — a single blog, with its own domain, languages, theme, and nav/footer chrome. See Subdomains and Your Own Domain for how domain and prefix determine where a site is actually reachable.

Every endpoint below requires the Authorization: Bearer <key> header (see Authentication) and only ever operates on sites the key's owner has access to.

Create a site

POST /sites
{
  "name": "My Blog",
  "domain": "my-blog",
  "description": "Optional description",
  "stylesUrl": null,
  "customCss": null,
  "availableThemes": ["LIGHT"],
  "languages": ["ENGLISH"],
  "config": {}
}

domain without dots is treated as a subdomain label (my-blog.writeinone.com) and the site is created VERIFIED immediately. A full domain (containing a dot) is treated as a custom domain and starts out NOT_VERIFIED — see Your Own Domain.

availableThemes is any of LIGHT, DARK; languages is any of ENGLISH, SPANISH. Both default to a single-item list (LIGHT, ENGLISH) if omitted.

Returns the created Site.

List your sites

GET /sites

Returns every site you have access to, as a plain JSON array (no pagination).

Get a site

GET /sites/{id}

Update a site

PATCH /sites/{id}
{
  "name": "New name",
  "domain": "example.com",
  "prefix": "/blog",
  "requestVerification": true,
  "stylesUrl": "https://example.com/blog.css",
  "customCss": null,
  "availableThemes": ["LIGHT", "DARK"],
  "languages": ["ENGLISH", "SPANISH"],
  "config": {}
}

All fields are optional — only send what changes; omitted fields are left untouched. customCss is the one exception: sending "" explicitly clears it, since null means "don't touch."

Set requestVerification: true when moving to a custom domain, or to force re-verification of one that's already set, to kick off DNS/proxy verification (see Your Own Domain). Changing domain on a subdomain site parks the old label in a reservation for other users (see Subdomains).

prefix only applies to custom domains — it's forced back to "" for managed subdomains regardless of what you send. Must match ^/?[a-zA-Z0-9-]{0,20}$ (alphanumeric and dashes, max 20 characters).

Delete a site

DELETE /sites/{id}

Deletes the site and its posts. If the site used a subdomain label, that label is parked in a reservation window before it can be claimed by anyone else.

Check subdomain availability

GET /sites/subdomain             # rules: min/max length, base domain
GET /sites/subdomain?name=foo    # whether "foo" is available

Always returns 200, even when unavailable — see Subdomains for the response shape. Useful for validating a label client-side before calling POST /sites or PATCH /sites/{id}.

The Site object

{
  "id": 42,
  "userId": 7,
  "name": "My Blog",
  "domain": "my-blog.writeinone.com",
  "prefix": "",
  "description": null,
  "stylesUrl": null,
  "customCss": null,
  "availableThemes": ["LIGHT"],
  "languages": ["ENGLISH"],
  "config": { "faviconUrl": null, "headHtml": null, "bodyHtml": null, "en": {}, "es": {} },
  "status": "VERIFIED",
  "createdAt": "2026-01-10T09:00:00Z",
  "updatedAt": "2026-01-10T09:00:00Z",
  "verifyDate": "2026-01-10T09:00:00Z"
}

status is VERIFIED or NOT_VERIFIED.

config

A small JSON object for per-language site chrome:

{
  "faviconUrl": "https://example.com/favicon.ico",
  "headHtml": null,
  "bodyHtml": null,
  "en": {
    "title": "My Blog",
    "description": "Notes on building things",
    "footer": "© 2026 My Blog",
    "nav": [{ "label": "About", "url": "/en/about" }]
  },
  "es": { "footer": "", "nav": [] }
}

Every nav[].url must start with http://, https://, or / — anything else is rejected with 400 BAD_REQUEST. It's intentionally minimal; anything more elaborate belongs in customCss (max 25,000 characters) or stylesUrl.

Errors

StatusCodeWhen
400VALIDATION_ERRORPOST /sites body fails field validation (e.g. blank name)
400BAD_REQUESTinvalid nav link URL, customCss too long, domain is a reserved/home domain
404SITE_NOT_FOUNDsite doesn't exist or you don't have access to it
409SITE_DOMAIN_TAKENcustom domain already claimed by another site
409SUBDOMAIN_NOT_ALLOWEDlabel fails length/charset rules or is reserved
409SUBDOMAIN_HELDlabel is inside another user's reservation window

See Errors for the response envelope and the full list of codes.