## POST /api/v2/cdn/zones/{id}

**Prepare or check external-DNS CDN setup**

Keep authoritative DNS with the current provider while routing selected hostnames through the CDN. Start with `GET /api/v2/cdn/zones` and use the returned public `cdn_...` ID. `prepare_partial` prepares the selected zone and returns its ownership-verification TXT record, preserved origin records and per-hostname routing instructions; it does not change external authoritative DNS or the domain's nameservers. Optional `hostnames` is accepted only for preparation: 1–50 absolute hostname entries equal to or below the zone, with apex and www as the default. Wildcards, URL schemes and paths are rejected; IDNs, case and a trailing dot are normalized, and duplicate names are collapsed. Preparation also verifies current authoritative delegation and cannot convert a currently delegated full setup. The server resolves origins and ownership; arbitrary origins, account identifiers and unknown body fields are rejected.

Publish only the exact `setup.verification` and `setup.routing[].record` values returned for this zone at the current DNS provider, respecting each row's status and reason. Apex routing requires that DNS provider's ALIAS/flattening support; an unsupported apex must not be marked complete. Then send `check_partial` with no other fields. This refreshes retained setup state and returns the same complete zone detail. Both actions require write permissions. HTTP 200 means the action completed, not that the setup is active: `pending_verification`, `pending_routing` and `pending_certificate` identify work still outstanding. Only `setup.status: active` confirms the setup is ready. An unavailable check does not prove an invalid DNS record.

The alternative is managed nameservers: use `setup.nameserverManagementUrl` when present, or set `setup.expectedNameservers` through the external registrar. Do not perform that nameserver change as part of partial setup. Regular proxy/security changes continue to use PATCH on this same resource; `setup` is response-only.

### Related Endpoints

- `GET /api/v2/cdn/zones/{id}`: Get CDN zone details
- `PATCH /api/v2/cdn/zones/{id}`: Update CDN zone settings
- `DELETE /api/v2/cdn/zones/{id}`: Disable CDN zone

### Headers

- `Accept`: application/json
- `Authorization`: Bearer YOUR_API_KEY
- Required API scopes: `write:cdn`, `write:domains`
- `Content-Type`: application/json

### Parameters

- `id` (path, string, required): Public CDN zone ID. Get it from `GET /api/v2/cdn/zones` `data[].id`. Do not invent this value; use the exact ID returned by the referenced API response. Example: `cdn_01hxa3b4c5d6e7f8g9h0j1k2m3`

### Request Body

- `action` (string, required): Prepare external-DNS setup for the selected zone.
  Allowed values: prepare_partial
- `hostnames` (array<string>, optional): Absolute hostnames equal to the selected zone or below it. Omit for apex and www. Supply 1–50 entries; duplicates are collapsed after normalization. Wildcards, URL schemes and paths are rejected. Case, IDNs and a trailing dot are normalized by the server. The current origins are read from owned DNS records. Example: `["example.com","www.example.com"]`

### Request Examples

#### Prepare apex and www with existing origins

```bash
curl -X POST "https://cloud.hostup.se/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "prepare_partial"
  }'
```

```json
{
  "action": "prepare_partial"
}
```

#### Prepare only www

```bash
curl -X POST "https://cloud.hostup.se/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "prepare_partial",
    "hostnames": [
      "www.example.com"
    ]
  }'
```

```json
{
  "action": "prepare_partial",
  "hostnames": [
    "www.example.com"
  ]
}
```

#### Check DNS and certificate readiness

```bash
curl -X POST "https://cloud.hostup.se/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "check_partial"
  }'
```

```json
{
  "action": "check_partial"
}
```

### Response Schema

- `setup` (object, optional): Canonical CDN setup state for both managed nameservers and external DNS. Read status and reason directly; a prepared partial setup is not active until verification, hostname routing and HTTPS certificate readiness are confirmed. DNS names and targets are returned values to copy, not patterns to reconstruct.
- `setup.mode` (string, required): full is nameserver-based configuration; partial keeps external authoritative DNS and routes selected hostnames through the CDN. Read currentNameservers for the currently observed delegation.
  Allowed values: full, partial, unknown
- `setup.status` (string, required): Overall readiness. Pending states identify the next setup step. unavailable means the state could not be established; it is not proof that DNS or the certificate is incorrect.
  Allowed values: not_configured, pending_verification, pending_routing, pending_certificate, active, unavailable
- `setup.reason` (string, required, nullable): Explanation of the current setup state, or null when no explanation is needed.
- `setup.verification` (object, required, nullable): Ownership-verification TXT record to publish at the authoritative DNS provider; null when no record is available or needed. Copy the returned name and value exactly.
- `setup.routing` (array<object>, required): Selected hostnames with the origin values preserved during preparation and the required external-DNS routing changes. Publish the returned routing record without mixing conflicting record types at that name. An unsupported row cannot be treated as configured.
- `setup.routing[].hostname` (string, required) Example: `www.example.com`
- `setup.routing[].currentOrigin` (array<object>, required)
- `setup.routing[].currentOrigin[].type` (string, required)
  Allowed values: A, AAAA, CNAME
- `setup.routing[].currentOrigin[].value` (string, required) Example: `203.0.113.10`
- `setup.routing[].record` (object, required, nullable): Exact routing record to publish at the external DNS provider, or null when unavailable. ALIAS is for an apex-capable flattened alias; only use it when that provider supports it. Do not replace an apex with an ordinary CNAME when that would conflict with its other records.
- `setup.routing[].status` (string, required)
  Allowed values: pending, verified, unsupported, unknown
- `setup.routing[].reason` (string, required, nullable): Nullable (may be null when not applicable).
- `setup.certificate` (object, required)
- `setup.certificate.status` (string, required)
  Allowed values: active, pending, error, unknown
- `setup.certificate.hosts` (array<string>, required): Hostnames reported for the CDN HTTPS certificate.
- `setup.checkedAt` (string, required, nullable): UTC time of the last setup check, or null when no check time is known.
- `setup.currentNameservers` (array<string>, required)
- `setup.expectedNameservers` (array<string>, required): Nameservers for the managed-nameserver alternative. Partial setup does not require switching to these nameservers. Example: `["primary.ns.hostup.se","secondary.ns.hostup.se"]`
- `setup.registrarRelation` (string, required): Where the domain's registrar management is available; independent of the selected CDN setup mode.
  Allowed values: hostup, external, unknown
- `setup.nameserverManagementUrl` (string, required, nullable): Customer-facing nameserver-management URL when the domain can be managed here. Otherwise null: publish expectedNameservers through the external registrar if choosing managed nameservers.
- `id` (string, optional, nullable): Public CDN zone ID, also returned before setup is provisioned. Null only when no manageable resource identity can be returned. Example: `cdn_01hxa3b4c5d6e7f8g9h0j1k2m3`
- `domain` (string, optional) Example: `example.com`
- `domainId` (string, optional, nullable): Public domain ID for registrar-owned domains; `null` for DNS-only zones. Example: `dom_01hxa3b4c5d6e7f8g9h0j1k2m3`
- `status` (string, optional) Example: `active`
  Allowed values: active, pending, misconfigured, missing, disabled
- `reason` (string, optional, nullable): Nullable (may be null when not applicable). Example: `null`
- `proxied` (boolean, optional) Example: `true`
- `securityLevel` (string, optional) Example: `medium`
  Allowed values: off, low, medium, high
- `ssl` (object, optional)
- `ssl.status` (string, required) Example: `active`
  Allowed values: active, pending, error
- `ssl.expiresAt` (string, required, nullable): Nullable (may be null when not applicable). Example: `2026-08-01T00:00:00.000Z`
- `ruleCount` (integer, optional) Example: `2`
- `security` (object, optional)
- `security.level` (string, required): Overall CDN security level. Example: `medium`
  Allowed values: off, low, medium, high
- `security.sslMode` (string, required): TLS mode used between visitors, CDN, and origin. Example: `full`
  Allowed values: off, flexible, full, strict
- `security.alwaysUseHttps` (boolean, required) Example: `true`
- `security.minTlsVersion` (string, required) Example: `1.2`
  Allowed values: 1.0, 1.1, 1.2, 1.3
- `security.botProtection` (boolean, required) Example: `true`
- `security.blockBadCrawlers` (boolean, required) Example: `true`
- `security.blockBadBots` (boolean, required) Example: `true`
- `security.wpLoginProtection` (boolean, required): Protect WordPress login endpoints. Example: `true`
- `security.wpAdminChallenge` (boolean, required): Challenge requests to WordPress administration paths. Example: `true`
- `performance` (object, optional)
- `performance.earlyHints` (boolean, required) Example: `true`
- `performance.alwaysOnline` (boolean, required) Example: `true`
- `cache` (object, optional)
- `cache.purgeCache` (boolean, required): Current persisted cache-purge toggle returned by the CDN settings surface. Example: `false`
- `waf` (object, optional)
- `waf.skipEnabled` (boolean, required): Whether custom WAF skip rules are enabled. Example: `false`
- `waf.challengeGeoEnabled` (boolean, required): Whether visitor-profile country challenge logic is enabled. Example: `true`
- `waf.visitorProfileMode` (string, required): `challenge` asks visitors outside the configured country profile to complete a challenge; `block` blocks them; `off` disables this profile action. Example: `challenge`
  Allowed values: off, challenge, block
- `waf.ipAllowlist` (array<string>, required): IP addresses or CIDR ranges allowed through custom WAF checks. Example: `["203.0.113.10"]`
- `waf.uaAllowlist` (array<string>, required): User-agent substrings allowed through custom WAF checks. Example: `["HostUp-Monitor"]`
- `waf.pathAllowlist` (array<string>, required): Path prefixes allowed through custom WAF checks. Example: `["/health"]`
- `geoRestriction` (object, optional)
- `geoRestriction.enabled` (boolean, required) Example: `true`
- `geoRestriction.whitelistCountries` (array<string>, required): Effective uppercase country-code allowlist kept for compatibility. Prefer `combinedCountries` for new integrations. Example: `["SE"]`
- `geoRestriction.mode` (string, required): `off` disables the allowlist; `whitelist` allows only `combinedCountries`. Example: `whitelist`
  Allowed values: off, whitelist
- `geoRestriction.standardCountries` (array<string>, required): System-required countries that callers cannot remove. Example: `[]`
- `geoRestriction.additionalCountries` (array<string>, required): Caller-managed country codes layered on top of `standardCountries`. Example: `["SE"]`
- `geoRestriction.combinedCountries` (array<string>, required): Effective allowlist: `standardCountries` plus `additionalCountries`. Example: `["SE"]`
- `activity` (object, optional)
- `activity.lastChangeDetectedAt` (string, required, nullable): Nullable (may be null when not applicable). Example: `2026-04-27T12:00:00.000Z`
- `activity.lastCheckedAt` (string, required, nullable): Nullable (may be null when not applicable). Example: `2026-04-27T12:00:00.000Z`
- `activity.settingsUpdatedAt` (string, required, nullable): Nullable (may be null when not applicable). Example: `2026-04-27T12:00:00.000Z`
- `actions` (object, optional)
- `actions.canEnableProxy` (object, required)
- `actions.canEnableProxy.allowed` (boolean, required) Example: `true`
- `actions.canEnableProxy.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canEnableProxy.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `actions.canIssueCertificate` (object, required)
- `actions.canIssueCertificate.allowed` (boolean, required) Example: `true`
- `actions.canIssueCertificate.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canIssueCertificate.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `actions.canChangeMode` (object, required)
- `actions.canChangeMode.allowed` (boolean, required) Example: `true`
- `actions.canChangeMode.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canChangeMode.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `actions.canActivate` (object, required)
- `actions.canActivate.allowed` (boolean, required) Example: `true`
- `actions.canActivate.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canActivate.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `actions.canDeactivate` (object, required)
- `actions.canDeactivate.allowed` (boolean, required) Example: `true`
- `actions.canDeactivate.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canDeactivate.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `actions.canPreparePartial` (object, required): Whether external-DNS preparation may be requested. The request also verifies current authoritative delegation; a currently delegated full setup cannot be converted through this action.
- `actions.canPreparePartial.allowed` (boolean, required) Example: `true`
- `actions.canPreparePartial.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canPreparePartial.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `actions.canCheckPartial` (object, required): Whether an existing partial setup can be checked now.
- `actions.canCheckPartial.allowed` (boolean, required) Example: `true`
- `actions.canCheckPartial.reason` (string, required, nullable): Nullable (may be null when not applicable). Example: `null`
- `actions.canCheckPartial.code` (string, optional, nullable): Machine-readable reason code when an action is blocked. Example: `pending_order`
- `records` (array<any>, optional, nullable): `null` unless `includeRecords=true`.

### Responses

#### 200 - Full CDN zone detail after preparation or verification. A pending setup state is a successful response that still requires the indicated DNS or certificate step.
```json
{
  "id": "cdn_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "domain": "example.com",
  "domainId": "dom_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "status": "pending",
  "reason": "Add the verification TXT record at your DNS provider.",
  "proxied": false,
  "securityLevel": "medium",
  "ssl": {
    "status": "pending",
    "expiresAt": null
  },
  "ruleCount": 2,
  "security": {
    "level": "medium",
    "sslMode": "full",
    "alwaysUseHttps": true,
    "minTlsVersion": "1.2",
    "botProtection": true,
    "blockBadCrawlers": true,
    "blockBadBots": true,
    "wpLoginProtection": true,
    "wpAdminChallenge": true
  },
  "performance": {
    "earlyHints": true,
    "alwaysOnline": true
  },
  "cache": {
    "purgeCache": false
  },
  "waf": {
    "skipEnabled": false,
    "challengeGeoEnabled": true,
    "visitorProfileMode": "challenge",
    "ipAllowlist": [
      "203.0.113.10"
    ],
    "uaAllowlist": [
      "HostUp-Monitor"
    ],
    "pathAllowlist": [
      "/health"
    ]
  },
  "geoRestriction": {
    "enabled": true,
    "whitelistCountries": [
      "SE"
    ],
    "mode": "whitelist",
    "standardCountries": [],
    "additionalCountries": [
      "SE"
    ],
    "combinedCountries": [
      "SE"
    ]
  },
  "activity": {
    "lastChangeDetectedAt": "2026-04-27T12:00:00.000Z",
    "lastCheckedAt": "2026-04-27T12:00:00.000Z",
    "settingsUpdatedAt": "2026-04-27T12:00:00.000Z"
  },
  "actions": {
    "canEnableProxy": {
      "allowed": false,
      "reason": "Add the verification TXT record at your DNS provider."
    },
    "canIssueCertificate": {
      "allowed": true,
      "reason": null
    },
    "canChangeMode": {
      "allowed": true,
      "reason": null
    },
    "canActivate": {
      "allowed": false,
      "reason": "Add the verification TXT record at your DNS provider."
    },
    "canDeactivate": {
      "allowed": false,
      "reason": "CDN is not active for this domain."
    },
    "canPreparePartial": {
      "allowed": false,
      "reason": "External DNS setup is already prepared. Check its current status."
    },
    "canCheckPartial": {
      "allowed": true,
      "reason": null
    }
  },
  "records": null,
  "setup": {
    "mode": "partial",
    "status": "pending_verification",
    "reason": "Add the verification TXT record at your DNS provider.",
    "verification": {
      "type": "TXT",
      "name": "cdn-verification.example.com",
      "value": "example-verification-value",
      "status": "pending"
    },
    "routing": [
      {
        "hostname": "www.example.com",
        "currentOrigin": [
          {
            "type": "A",
            "value": "203.0.113.10"
          }
        ],
        "record": {
          "type": "CNAME",
          "name": "www.example.com",
          "value": "www.example.com.cdn.example.net"
        },
        "status": "pending",
        "reason": null
      }
    ],
    "certificate": {
      "status": "pending",
      "hosts": []
    },
    "checkedAt": "2026-04-27T12:00:00.000Z",
    "currentNameservers": [
      "ns1.example.net",
      "ns2.example.net"
    ],
    "expectedNameservers": [
      "primary.ns.hostup.se",
      "secondary.ns.hostup.se"
    ],
    "registrarRelation": "hostup",
    "nameserverManagementUrl": "/domains/example.com?tab=nameservers"
  }
}
```

#### 400 - The action, hostname selection or request body is invalid. Only prepare_partial accepts hostnames.
```json
{
  "type": "https://developer.hostup.se/errors/invalid_request",
  "title": "Invalid request",
  "status": 400,
  "detail": "The action, hostname selection or request body is invalid. Only prepare_partial accepts hostnames.",
  "code": "invalid_request",
  "instance": "/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3"
}
```

#### 401 - Unauthorized. Authentication is required.
```json
{
  "type": "https://developer.hostup.se/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Authentication is required.",
  "code": "unauthorized",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 403 - Forbidden. The caller lacks a required scope or does not own the resource.
```json
{
  "type": "https://developer.hostup.se/errors/forbidden",
  "title": "Forbidden",
  "status": 403,
  "detail": "The caller lacks a required scope or does not own the resource.",
  "code": "forbidden",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 404 - The selected CDN zone could not be found.
```json
{
  "type": "https://developer.hostup.se/errors/cdn_zone_not_found",
  "title": "CDN zone not found",
  "status": 404,
  "detail": "The selected CDN zone could not be found.",
  "code": "cdn_zone_not_found",
  "instance": "/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3"
}
```

#### 429 - Rate limited. Retry after the limit resets. 429 responses include `Retry-After` seconds plus `X-RateLimit-*` headers.
```json
{
  "type": "https://developer.hostup.se/errors/rate_limit_exceeded",
  "title": "Too many requests",
  "status": 429,
  "detail": "Too many requests. Retry after the limit resets.",
  "code": "rate_limit_exceeded",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 500 - Internal error. Retry later or contact support if the issue persists.
```json
{
  "type": "https://developer.hostup.se/errors/internal_error",
  "title": "Internal server error",
  "status": 500,
  "detail": "An unexpected error occurred. Retry later or contact support if the issue persists.",
  "code": "internal_error",
  "instance": "/api/v2/resource",
  "requestId": "req_01hxa3b4c5d6e7f8g9h0j1k2m3",
  "timestamp": "2026-04-27T12:34:56.000Z"
}
```

#### 502 - The CDN setup service did not return a usable response.
```json
{
  "type": "https://developer.hostup.se/errors/upstream_failed",
  "title": "CDN setup failed",
  "status": 502,
  "detail": "The CDN setup service did not return a usable response.",
  "code": "upstream_failed",
  "instance": "/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3"
}
```

#### 503 - CDN setup could not be completed or checked right now. Retry later.
```json
{
  "type": "https://developer.hostup.se/errors/service_unavailable",
  "title": "CDN setup unavailable",
  "status": 503,
  "detail": "CDN setup could not be completed or checked right now. Retry later.",
  "code": "service_unavailable",
  "instance": "/api/v2/cdn/zones/cdn_01hxa3b4c5d6e7f8g9h0j1k2m3"
}
```
