---
title: "Attach a custom domain to a Cube"
method: POST
path: "/spaces/{spaceId}/cubes/{cubeId}/domains"
tags: ["Domains"]
---

# Attach a custom domain to a Cube

`POST /spaces/{spaceId}/cubes/{cubeId}/domains`

## Request

- Base URL: `https://krova.cloud/api/v1`
- URL: `https://krova.cloud/api/v1/spaces/{spaceId}/cubes/{cubeId}/domains`
- Auth: API key in header `X-API-KEY`

Attaches a domain and optionally configures any per-domain proxy settings at the same time. Settings can also be changed later via PATCH.

## Path parameters

- `spaceId` string, required
- `cubeId` string, required

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `domain` string, required: The hostname to attach, e.g. app.example.com
  - `port` integer, required: Cube port the domain proxies to.
  - `securityHeadersMode` 'enforced' | 'app_managed': enforced = proxy sets platform security headers (HSTS, X-Frame-Options, …); app_managed = the app's own headers pass through. Default enforced.
  - `responseHeaderTimeoutSecs` integer: Seconds the app may take to send response headers (streaming unaffected). Default 60.
  - `responseHeaderOverrides` object, nullable: Header rewrites: `set` adds/overwrites, `remove` deletes. Null clears all.
    - `set` object
    - `remove` string[]
  - `requestHeaderOverrides` object, nullable: Header rewrites: `set` adds/overwrites, `remove` deletes. Null clears all.
    - `set` object
    - `remove` string[]
  - `maxRequestBodyMb` integer, nullable: Max request body size (MB). Null = unlimited (default).
  - `corsConfig` object, nullable: Per-domain CORS policy, applied at the edge. Null (default) = CORS disabled and no Access-Control-* header is emitted anywhere. When set, the proxy answers OPTIONS preflight itself (204, the cube is never involved) and adds the headers to normal AND error responses, so a cross-origin request to a failing or stopped cube is never CORS-masked.
    - `allowedOrigins` string[], required: Exact scheme://host[:port] origins (no path, query or trailing slash), or the single-element wildcard ["*"]. "*" and allowCredentials are mutually exclusive.
    - `allowedMethods` string[]: Allowed methods. Omitted = GET, POST, PUT, PATCH, DELETE, OPTIONS.
    - `allowedHeaders` string[]: Allowed request headers. Omitted/empty = echo the preflight's Access-Control-Request-Headers.
    - `exposedHeaders` string[]: Response headers exposed to JS. Omitted/empty = none.
    - `allowCredentials` boolean: Send Access-Control-Allow-Credentials: true. Rejected together with the "*" origin.
    - `maxAgeSeconds` integer: Preflight cache TTL. Default 600; 86400 is Chromium's hard cap.
  - `originScheme` 'http' | 'https': Scheme the edge speaks to the CUBE on the backend hop. http (default) = cleartext. https = the cube terminates TLS itself (a control panel holding its own certificate, or an app listening on HTTPS) and answers plain HTTP with a redirect, so it cannot be reached over cleartext at all. Visitors are on HTTPS either way. The dial port is derived: https on the default port 80 connects on 443; a deliberate custom port is honored exactly. Applied as saved: if the cube does not serve this hostname over HTTPS, requests to the domain fail until you set it back to http.
  - `proxyProtocol` 'v1' | 'v2', nullable: Send a PROXY protocol header (v1 text or v2 binary) at the start of every connection to the cube, so the app reads the visitor's address from the connection itself, as on a plain VPS. Null (default) = off. The app must accept PROXY protocol on this port, or every request to the domain fails; it is applied as saved. Every domain on the same cube port must use the same setting: a save that would mix them is refused with 409 unless confirmMixedProxyProtocol is true.
  - `confirmMixedProxyProtocol` boolean: Accept a save that leaves domains on the same cube port with different proxyProtocol settings (for an app that handles both on one port, or mid-migration). Not stored.
  - `responseCompression` boolean: Edge gzip/zstd compression. Default false (domains behind a CDN are already compressed there).
  - `ipAllowList` string[], nullable: Visitor IP/CIDR allow-list (v4+v6). Non-empty ⇒ only these reach the app. Null/empty = open.
  - `ipDenyList` string[], nullable: Visitor IP/CIDR deny-list (v4+v6) → 403. Null/empty = none.
  - `basicAuthUser` string, nullable: HTTP Basic Auth username. Null/empty clears Basic Auth. Setting it requires basicAuthPassword.
  - `basicAuthPassword` string: Plaintext Basic Auth password — bcrypt-hashed server-side, never stored or returned. Required when setting basicAuthUser.

## Response `201`

Domain attached

- object
  - `domain` Domain, required
    - `id` string, required
    - `cubeId` string, required
    - `domain` string, required
    - `port` integer, nullable, required
    - `status` string, required
    - `ingressStatus` string, nullable, required
    - `securityHeadersMode` 'enforced' | 'app_managed', required
    - `responseHeaderTimeoutSecs` integer, required
    - `responseHeaderOverrides` object, nullable, required: Header rewrites: `set` adds/overwrites, `remove` deletes. Null clears all.
      - `set` object
      - `remove` string[]
    - `requestHeaderOverrides` object, nullable, required: Header rewrites: `set` adds/overwrites, `remove` deletes. Null clears all.
      - `set` object
      - `remove` string[]
    - `maxRequestBodyMb` integer, nullable, required
    - `responseCompression` boolean, required
    - `ipAllowList` string[], nullable, required
    - `ipDenyList` string[], nullable, required
    - `basicAuthUser` string, nullable, required
    - `basicAuthEnabled` boolean, required
    - `corsConfig` object, nullable, required
    - `originScheme` 'http' | 'https', required
    - `proxyProtocol` 'v1' | 'v2', nullable, required
    - `isWildcard` boolean, required
    - `wildcardCertState` 'pending_delegation' | 'issuing' | 'ready' | 'failed', nullable, required
    - `acmeDelegationTarget` string, nullable, required
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
  - `records` DnsRecord[], required
    - `id` 'ownership' | 'routing' | 'certificate', required
    - `type` 'CNAME' | 'TXT' | 'A', required
    - `host` string, required
    - `value` string, required
    - `purpose` 'ownership' | 'routing' | 'certificate', required
    - `note` string, nullable
    - `mustBeGrey` boolean, required
    - `proxyOk` boolean, required

## Other responses

- `400`: Bad request
- `409`: Refused. Among other reasons (the domain is in use or is being removed; or, temporarily, the Cube is in the last stretch of a move to another server, refused with the error "This cube is being moved to another server. Try again in a minute." and accepted once the move ends): the save would leave domains on the same cube port with different proxyProtocol settings. Then `errorMeta.code` is `proxy_protocol_port_mismatch`, with `errorMeta.port` and `errorMeta.domains` naming the other domains. Change them together, or resend with confirmMixedProxyProtocol: true.
- `429`: Rate limit exceeded (10 mutations / 60s)

---

[API](https://skmtc.dev/krova/apis/krova-cloud-api.md) · [All operations](https://skmtc.dev/krova/apis/krova-cloud-api/llms.txt) · [OpenAPI document](https://skmtc.dev/krova/apis/krova-cloud-api/revisions/46bb68281ce1?raw)
