# Site

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /site/get-org/{domainName}

**Resolve a domain to an organization**

`operationId: SiteController_getOrgByDomainName`

Maps a hostname back to the org that owns it. This is the first hop in serving a request for a custom domain — before a page can be rendered, the tenant has to be identified from the host header.

#### Signature

```http
GET /site/get-org/{domainName} (domainName: string) -> The owning organization
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/get-site-by-hostname/{hostname}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `domainName` | path | string | yes | Hostname. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The owning organization |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/company/{domainName}

**Get the public company profile for a domain**

`operationId: SiteController_getPublicCompany`

The org behind a domain (or behind the `orgid` header when the domain segment is left off), reduced to what a site may publish about itself: `{ name, email, displayName }`. An empty object when the org has none of them — never an error. Tax id, legal name and entity type sit on the same record and are never returned.

#### Signature

```http
GET /site/company/{domainName} (domainName: string) -> Public company profile
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/get-org/{domainName}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `domainName` | path | string | yes | Hostname or org id. Optional — without it the `orgid` header is used. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Public company profile |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "name": "acme",
  "email": "hello@acme.com",
  "displayName": "Acme Coffee"
}
```

## GET /site/get-site/{configSiteName}/{domainName}

**Get site information**

`operationId: SiteController_getSite`

Fetches a site by its configured name, optionally narrowed by domain. `domainName` is an optional path segment — the route also matches without it.

#### Signature

```http
GET /site/get-site/{configSiteName}/{domainName} (configSiteName: string, domainName: string) -> The site
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The fields on `site.data` that most often trip people up: `homePage` matches the home page record's **`name`**, not its slug — a page named `home` with slug `index` is NOT found by `homePage: "index"`, and the site serves its fallback at `/` with no error.
- `data.favicon` must be a plain STRING url. An object is silently ignored and the site keeps the platform default — no icon link is emitted at all. `data.logo` may be an object (`{path,url}`) because it is normalised; the two fields are not symmetrical.
- `hostName` is the platform host and always current; `domain` is the custom domain and sits behind a cache. Verify a deploy on `hostName` first — if it shows the change and the custom domain does not, that is the cache, and redeploying will not help.
- `seo.noIndex` is the site-wide kill switch and outranks any per-page value; it is the same flag robots.txt reads.
- Set `hideSiteHeader` and `hideSiteFooter` when your pages carry their own chrome, or the page renders two headers.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_OR_DOMAIN_REQUIRED | Either siteName or domainName is required | Neither identifier resolves to anything. | Supply a valid site name. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/get-site-by-hostname/{hostname}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `configSiteName` | path | string | yes | Configured site name. |
| `domainName` | path | string | yes | Optional hostname. |
| `shared-host` | query | any | — | Shared host |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The site |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Either siteName or domainName is required — Neither identifier resolves to anything. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/get-site-by-hostname/{hostname}

**Get a site by hostname**

`operationId: SiteController_getSiteByDomainName`

Resolves a hostname straight to its site configuration — the lookup a renderer does per request. The hostname segment is optional in the route, but omitting it leaves nothing to resolve.

#### Signature

```http
GET /site/get-site-by-hostname/{hostname} (hostname: string) -> The site
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_OR_DOMAIN_REQUIRED | Either siteName or domainName is required | No hostname was supplied. | Pass the hostname in the path. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/get-org/{domainName}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `hostname` | path | string | yes | Hostname. |
| `shared-host` | query | any | — | Shared host |
| `domainName` | path | any | — | Domain name |
| `configSiteName` | path | any | yes | Site configuration name |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The site |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Either siteName or domainName is required — No hostname was supplied. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/get-site-host-mappings/{siteName}

**Get a site's host mappings**

`operationId: SiteController_getSiteHostMappings`

Every hostname routed to a site — the default platform domain plus any custom domains. This is the source of truth for what actually reaches the site; a domain configured at the registrar but absent here does not resolve.

#### Signature

```http
GET /site/get-site-host-mappings/{siteName} (siteName: string, keyword?: string, fields?: string) -> Host mappings
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/fix-domains`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `siteName` | path | string | yes | Site name. |
| `keyword` | query | string | — | Filter mappings. |
| `fields` | query | string | — | Comma-separated fields to return. |
| `shared-host` | query | any | — | Shared host |
| `domainName` | path | any | — | Domain name |
| `configSiteName` | path | any | yes | Site configuration name |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Host mappings |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/base-url

**Get the org's public site URL**

`operationId: SiteController_getSiteBaseUrl`

The origin every customer-facing link should be built on, from the one resolver all public links use: the site's custom domain, then its platform host, then the org's own host when it has no site. `siteName` narrows it to one site; without it the org's default site answers. Build links on this rather than assembling a host on the client.

#### Signature

```http
GET /site/base-url (siteName?: string) -> { url }
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `siteName` | query | string | — |  |
| `feature` | query | "forms" | — | Resolve the default site’s configured form page instead of its origin. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { url } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "url": "https://shop.example.com"
}
```

## GET /site/org-sites/{siteOrgId}

**List an organization's sites**

`operationId: SiteController_getOrgSites`

Every site belonging to an org. `siteOrgId` is a path parameter separate from the `orgid` header, which is what lets an operator list another org's sites.

#### Signature

```http
GET /site/org-sites/{siteOrgId} (siteOrgId: string) -> Sites
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `503` | ORG_UNREADABLE | Could not read organization '<orgId>' | The org record could not be read — an infrastructure problem, not a client one. | Retry; escalate if it persists. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/create-site`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `siteOrgId` | path | string | yes | Org whose sites to list. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Sites |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |
| `503` | Could not read organization '<orgId>' — The org record could not be read — an infrastructure problem, not a client one. |

## POST /site/attach-domain

**Attach a domain to a site**

`operationId: SiteController_registerSiteDomainName`

Routes a hostname to a site. DNS still has to point at the platform for the domain to resolve — attaching here creates the mapping, it does not configure the registrar.

#### Signature

```http
POST /site/attach-domain (body) -> The mapping result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `DELETE /site/unregister-site-domain/{siteId}/{domainName}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The domain and target site.

```json
{
  "siteName": "acme-shop",
  "domain": "shop.example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The mapping result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/{site}/features/{feature}/link

**Link a site feature from the footer or menu**

`operationId: SiteController_linkFeature`

Adds a link to a feature's page (for example Gift cards → `/gift-card`) to **every page** of the site — into its `<footer>`, or with `where: "nav"` into its `<nav>` (else `<header>`) — styled like the links already there. `remove: true` takes it out again. The feature must be switched on in Site Features and have a page; `unsubscribe` and `gift-card` have built-in pages. Pages that already link to it, or have no footer/menu, are left alone and reported.

#### Signature

```http
POST /site/{site}/features/{feature}/link (site: string, feature: string, body) -> { href, where, label, updated, alreadyLinked, noPlace, removed } — page names in each list
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- Edits the HTML of every page of the site.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SITE_NOT_FOUND | Site acme-shop not found | No site with that name in the org. | — |
| `400` | FEATURE_OFF | Turn gift-card on in Site Features first | Linking a feature that is not enabled on the site. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `site` | path | string | yes | Site name. |
| `feature` | path | string | yes | Site feature key. |

### Request body

Where, and what to call it.

```json
{
  "where": "footer",
  "label": "Gift cards"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { href, where, label, updated, alreadyLinked, noPlace, removed } — page names in each list |
| `400` | Turn gift-card on in Site Features first — Linking a feature that is not enabled on the site. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Site acme-shop not found — No site with that name in the org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "href": "/gift-card",
  "where": "footer",
  "label": "Gift Cards",
  "updated": [
    "home",
    "about"
  ],
  "alreadyLinked": [
    "contact"
  ],
  "noPlace": [],
  "removed": []
}
```

## POST /site/make-site-template

**Register a site as a template**

`operationId: SiteController_makeSiteTemplate`

Records an existing site in the shared org as a reusable template, so `POST /site/create-site` can name it and start from a copy of its pages.

The template is a pointer, not a snapshot: it stores which org owns the site and what it is called, and applying it reads that site live. Editing the source site updates the template.

#### Signature

```http
POST /site/make-site-template (body) -> The registered template and its stored content
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- Only the shared org may register a template — the registry is a curated gallery.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | NOT_SHARED_ORG | Not allowed, invalid org | The caller is not the shared org. | Register templates from the shared org. |
| `404` | SITE_NOT_FOUND | Site not found | No site by that name exists in the owning org. | Check `site` and `orgId`. |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /site/create-site`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The site to register, and the org that owns it.

```json
{
  "site": "storefront-basic",
  "orgId": "appmint"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The registered template and its stored content |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Not allowed, invalid org — The caller is not the shared org. |
| `404` | Site not found — No site by that name exists in the owning org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/create-site

**Create a site**

`operationId: SiteController_createSite`

Provisions a new site: the site record, its default platform domain and its host mappings. Must start with a letter and contain only lowercase letters, numbers and hyphens.

The name becomes part of the default domain and is not renameable afterwards — changing it later means creating a new site and moving the domains.

#### Signature

```http
POST /site/create-site (body) -> The created site
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- The site name is fixed once created.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_SITE_NAME | Invalid site name. Must start with a letter and contain only lowercase letters, numbers, and hyphens | The site name breaks the naming rule. | Must start with a letter and contain only lowercase letters, numbers and hyphens. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/clone-site`
- `POST /site/add`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The site to create.

```json
{
  "siteName": "acme-shop",
  "title": "Acme Shop",
  "templateName": "storefront"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created site |
| `400` | Invalid site name. Must start with a letter and contain only lowercase letters, numbers, and hyphens — The site name breaks the naming rule. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/clone-site

**Clone a site**

`operationId: SiteController_cloneSite`

Copies a site — content and configuration — from a source org and site to a destination. All four of `sourceOrgId`, `sourceSiteName`, `destOrgId` and `destSiteName` are required; the destination is created, not merged into.

#### Signature

```http
POST /site/clone-site (body) -> The cloned site
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- Copies across orgs — check the destination org is the intended one.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | CLONE_FIELDS_REQUIRED | sourceOrgId, sourceSiteName, destOrgId, destSiteName are required | Any of the four is missing. | Supply all four. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/create-site`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Source and destination.

```json
{
  "sourceOrgId": "org_4821",
  "sourceSiteName": "acme-shop",
  "destOrgId": "org_7712",
  "destSiteName": "acme-shop-copy"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cloned site |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | sourceOrgId, sourceSiteName, destOrgId, destSiteName are required — Any of the four is missing. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /site/delete-site/{siteOrgId}/{siteId}

**Delete a site**

`operationId: SiteController_deleteSiteHostMapping`

Deletes a site and its host mappings.

**Known defect:** the handler declares both path parameters with the same name (`siteOrgId`), so `siteId` is never bound — the second segment is ignored and the value used for both is the first. Pass the id you intend in the **first** segment until this is fixed.

#### Signature

```http
DELETE /site/delete-site/{siteOrgId}/{siteId} (siteOrgId: string, siteId: string) -> The delete result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- Both path parameters are declared as `siteOrgId`; the second segment is not bound.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/remove`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `siteOrgId` | path | string | yes | Org id — and, because of the defect below, the value used for the site id too. |
| `siteId` | path | string | yes | Site id. Currently ignored by the handler. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The delete result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/change-site-domain-names

**Rename site domains in bulk**

`operationId: SiteController_changeSiteDomainNames`

Renames domain mappings in bulk — the body is an **array**, each entry naming the site plus the old and new hostname. Used when a domain moves.

Each rename takes the old hostname out of service the moment it is applied, so send them when DNS for the new names is already in place.

#### Signature

```http
POST /site/change-site-domain-names (body) -> Per-rename results
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The body is an array, not an object.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/fix-domains`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The renames.

```json
[
  {
    "siteName": "acme-shop",
    "oldName": "old.example.com",
    "newName": "shop.example.com"
  }
]
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | Per-rename results |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /site/unregister-site-domain/{siteId}/{domainName}

**Unregister a site domain**

`operationId: SiteController_unregisterSiteDomainName`

Removes a hostname mapping. Traffic to that hostname stops reaching the site immediately — visitors get whatever the platform serves for an unmapped host.

#### Signature

```http
DELETE /site/unregister-site-domain/{siteId}/{domainName} (siteId: string, domainName: string) -> The result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `ContentAdmin`.

#### Notes

- Takes the hostname offline at once.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_ID_OR_DOMAIN_REQUIRED | Site ID or domain name is required | Either segment is empty. | Supply both. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/attach-domain`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `siteId` | path | string | yes | Site id. |
| `domainName` | path | string | yes | Hostname to remove. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site ID or domain name is required — Either segment is empty. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/page/{hostName}/{siteName}/*

**Render a site page by path**

`operationId: SiteController_getPage`

Renders a page at an arbitrary depth — the trailing wildcard captures the whole remaining path, so `/a/b/c` resolves as one page path rather than three parameters.

#### Signature

```http
GET /site/page/{hostName}/{siteName}/* (hostName: string, siteName: string, path: string) -> The rendered page payload
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/page/{hostName}/{siteName}/*`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `hostName` | path | string | yes | Hostname being served. |
| `siteName` | path | string | yes | Site name. |
| `0` | path | string | — | Additional path parameters |
| `path` | path | string | yes | Wildcard — the full remaining page path. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The rendered page payload |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/page/{hostName}/{siteName}/*

**Render a site page with a body**

`operationId: SiteController_getPagePost`

The POST form of page rendering, for pages driven by submitted data — a form post or a search. Same resolution as the GET form, with the body available to the page.

#### Signature

```http
POST /site/page/{hostName}/{siteName}/* (hostName: string, siteName: string, path: string, body) -> The rendered page payload
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/page/{hostName}/{siteName}/*`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `hostName` | path | string | yes | Hostname being served. |
| `siteName` | path | string | yes | Site name. |
| `0` | path | string | — | Additional path parameters |
| `path` | path | string | yes | Wildcard — the full remaining page path. |

### Request body

Data for the page.

```json
{
  "q": "cola"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The rendered page payload |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/page/{hostName}/{siteName}

**Render a site page**

`operationId: SiteController_getPageIndex`

Renders the root page of a site for a given host. Query parameters are passed through to the page, so they reach page-level data resolution.

#### Signature

```http
GET /site/page/{hostName}/{siteName} (hostName: string, siteName: string) -> The rendered page payload
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- A `page` record can hold its content in four shapes, and the renderer prefers them in this order: `data.screens[]`, then `data.html`, then `data.content`, then `data.sections[]`.
- **`data.screens` overrides `data.html`.** Any page ever saved in the visual editor carries `screens`, so later writes to `data.html` succeed with 201 and change nothing on screen — with no warning anywhere. When a deploy script takes ownership of a page, clear the other shapes in the same write: `{"data.html": "…", "data.screens": [], "data.sections": [], "data.content": ""}`.
- Page CSS and JavaScript live in a **top-level `style` object on the page record — a sibling of `data`**, with slots `javascript`, `css`, `scriptLinks` and `styleLinks`. `data.style` is read by nothing: a payload written there is stored, returns 201 and never runs.
- `style.javascript` is injected into the document HEAD, so it executes before the body is parsed and before the browser runtime mounts. Code there needs a readiness gate rather than `DOMContentLoaded`, which may have fired before the script was injected.
- A `<script>` with a body inside `data.html` never executes — the HTML is parsed to a virtual DOM with `blockTextElements: { script: false }`, which discards script bodies. The element survives as an empty node. Inline event-handler attributes DO run, because attributes survive parsing.
- `<link rel="icon">` tags in page HTML are deliberately stripped so a template cannot shadow the real favicon, which is owned by the site record.
- An unknown path renders the site fallback with a **200**, not a 404 — so a status code is not a validity check. Verify a deployed route by its `<h1>`.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/page/{hostName}/{siteName}/*`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `hostName` | path | string | yes | Hostname being served. |
| `siteName` | path | string | yes | Site name. |
| `0` | path | any | — | Additional path parameters |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The rendered page payload |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/page-data/{datatype}/{dataId}

**Get page data**

`operationId: SiteController_getPageData`

Fetches the record backing a page — a product for a product page, a post for an article page. `dataId` is optional: omit it for the collection, supply it for one record.

#### Signature

```http
GET /site/page-data/{datatype}/{dataId} (datatype: string, dataId: string) -> The page data
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/page/{hostName}/{siteName}/*`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `datatype` | path | string | yes | DataType name. |
| `dataId` | path | string | yes | Record id. Omit for the collection. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The page data |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/site-map

**Create a site map**

`operationId: SiteController_createSiteMap`

Generates the sitemap for a site — what search engines crawl.

#### Signature

```http
POST /site/site-map (body) -> The site map
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/get-site-host-mappings/{siteName}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which site to map.

```json
{
  "siteName": "acme-shop"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The site map |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/app/dsl/{name}

**Get an app DSL**

`operationId: SiteController_getAppDSL`

Returns the DSL definition for a named app — the declarative description a client renders from.

**Public route:** it carries no authentication, so any DSL served here is readable by anyone who knows the name. Keep credentials and private endpoints out of DSL definitions.

#### Signature

```http
GET /site/app/dsl/{name} (name: string) -> The DSL definition
```

#### Access

Public — no credentials required.

#### Notes

- Unauthenticated — treat the content as public.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /site/page-data/{datatype}/{dataId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | App name. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The DSL definition |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/add

**Add a site**

`operationId: SiteController_addSite`

Registers a site and its host mappings. Overlaps with `create-site`; which one applies depends on whether the underlying site content already exists.

#### Signature

```http
POST /site/add (body) -> The added site
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `ContentAdmin`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_SITE_NAME | Invalid site name. Must start with a letter and contain only lowercase letters, numbers, and hyphens | The site name breaks the naming rule. | Must start with a letter and contain only lowercase letters, numbers and hyphens. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/remove`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The site to add.

```json
{
  "siteName": "acme-shop",
  "domain": "shop.example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Site successfully created |
| `201` | The added site |
| `400` | Invalid site name. Must start with a letter and contain only lowercase letters, numbers, and hyphens — The site name breaks the naming rule. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/remove

**Remove a site**

`operationId: SiteController_removeSite`

Removes a site and its host mappings. The site stops resolving on every domain routed to it — confirm which domains are affected with `GET /site/get-site-host-mappings/{siteName}` first.

#### Signature

```http
POST /site/remove (body) -> The removal result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- Takes the site offline on all of its domains.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `DELETE /site/delete-site/{siteOrgId}/{siteId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Which site to remove.

```json
{
  "siteName": "acme-shop"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Site successfully removed |
| `201` | The removal result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/create

**Create a site with plan validation (legacy)**

`operationId: SiteController_createSiteWithPlanValidation`

Legacy create path that checks the org's plan limits before provisioning. Kept for existing callers — new integrations should use `POST /site/create-site`.

#### Signature

```http
POST /site/create (body) -> The created site
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ContentAdmin`, `ConfigAdmin`.

#### Notes

- Legacy — prefer `POST /site/create-site`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_SITE_NAME | Invalid site name. Must start with a letter and contain only lowercase letters, numbers, and hyphens | The site name breaks the naming rule. | Must start with a letter and contain only lowercase letters, numbers and hyphens. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/create-site`
- `GET /site/plan-limits`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The site to create.

```json
{
  "siteName": "acme-shop",
  "title": "Acme Shop"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created site |
| `400` | Invalid site name. Must start with a letter and contain only lowercase letters, numbers, and hyphens — The site name breaks the naming rule. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/usage-stats

**Get organization usage statistics**

`operationId: SiteController_getUsageStats`

Resource usage for the org's sites — what counts against the plan limits.

#### Signature

```http
GET /site/usage-stats () -> Usage statistics
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/plan-limits`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Usage statistics |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /site/plan-limits

**Get organization plan limits**

`operationId: SiteController_getPlanLimits`

The org's plan ceilings — site count and resource allowances. Read alongside usage stats to know how much headroom is left before a create is refused.

#### Signature

```http
GET /site/plan-limits () -> Plan limits
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/usage-stats`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Plan limits |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/update-domain/{domain}

**Update a site domain**

`operationId: SiteController_updateDomain`

Updates a site's domain configuration from the body. The `domain` path segment is optional and is **not read by the handler** — the domain is taken from the body.

#### Signature

```http
POST /site/update-domain/{domain} (domain: string, body) -> The result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `ContentAdmin`.

#### Notes

- The path parameter is ignored; put the domain in the body.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/remove-domain/{domain}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `domain` | path | string | yes | Optional, and ignored — the handler reads the body. |
| `siteName` | path | any | yes | Site name |

### Request body

The domain configuration.

```json
{
  "siteName": "acme-shop",
  "domain": "shop.example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/remove-domain/{domain}

**Remove a site domain**

`operationId: SiteController_removeDomain`

Removes a domain from a site. As with `update-domain`, the `domain` path segment is optional and ignored — the domain comes from the body.

#### Signature

```http
POST /site/remove-domain/{domain} (domain: string, body) -> The result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- The path parameter is ignored; put the domain in the body.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | NO_DOMAIN_TO_REMOVE | No domain to remove | The body names no domain, or the site has no such mapping. | Check the mappings first. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/update-domain/{domain}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `domain` | path | string | yes | Optional, and ignored — the handler reads the body. |

### Request body

Which domain to remove.

```json
{
  "siteName": "acme-shop",
  "domain": "shop.example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | No domain to remove — The body names no domain, or the site has no such mapping. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/apply-template

**Apply a template to a site**

`operationId: SiteController_applySiteTemplate`

Applies a site template to an existing site. Template content overwrites the corresponding pages — this is not a merge, so a customised page matching a template page is replaced. `siteOrgId` defaults to the calling org.

#### Signature

```http
POST /site/apply-template (body) -> The result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`). Required role(s): `ConfigAdmin`, `ContentAdmin`.

#### Notes

- Overwrites existing pages that the template also defines.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /site/create-site`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

Site and template.

```json
{
  "siteName": "acme-shop",
  "templateName": "storefront"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Template successfully applied to site |
| `201` | The result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /site/fix-domains

**Rebuild a site's domain mappings**

`operationId: SiteController_fixSiteDomains`

Repair tool: **removes every existing host mapping** for a site and recreates the default and custom domain mappings from the site record, then syncs the result to Cloudflare when it is configured.

The removal happens first, so the site is briefly unreachable on all of its domains while this runs, and a mapping that exists only in the routing table and not on the site record does not come back. Capture `GET /site/get-site-host-mappings/{siteName}` before running it.

The response reports partial failure in its `errors` and `cloudflareResults` arrays while still returning 200 — check `success`, not just the status code.

#### Signature

```http
POST /site/fix-domains (body) -> What was removed, added and synced
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Destructive-then-restorative: all mappings are dropped before being rebuilt.
- Returns 200 even on partial failure — read `success` and `errors`.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `422` | SITE_NOT_FOUND | Site not found <siteName> | No site with that name exists in the org. Returned as 422, not 404. | List the org's sites with `GET /site/org-sites/{siteOrgId}`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /site/get-site-host-mappings/{siteName}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The site to repair.

```json
{
  "siteName": "acme-shop"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | What was removed, added and synced |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `422` | Site not found <siteName> — No site with that name exists in the org. Returned as 422, not 404. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

Example response:

```json
{
  "success": true,
  "message": "Domains rebuilt",
  "site": "acme-shop",
  "removedDomains": [
    "shop.example.com"
  ],
  "addedDomains": [
    "acme-shop.platform.io",
    "shop.example.com"
  ],
  "cloudflareResults": [
    {
      "domain": "shop.example.com",
      "target": "edge.platform.io",
      "success": true,
      "recordId": "cf_881"
    }
  ],
  "errors": []
}
```

