## Attach a domain

`POST /v1/infra/servers/:id/domain`

Assigns your domain name to the application on the server and begins the first step of attachment: creating DNS records at the domain's DNS provider. The application starts accepting requests on the domain only after the records are verified successfully. A server can have only one domain.

Who can make the call — [Access](/docs/infra/domain#access). The purpose of the records and the difference between `host` and `name` — [DNS records](/docs/infra/domain#dns-records).

## Parameters

| Parameter | In | Type | Required | Description |
|----------|---|-----|:-----:|----------|
| `id` | path | string (UUID) | yes | Server ID. Source — the `id` field from the [server list](/docs/infra/servers/list) |

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `fqdn` | string | yes | Domain name without scheme, port or path, for example `app.example.com`. A subdomain or an apex domain is accepted. Case is ignored, leading and trailing spaces are trimmed. The name consists of at least two dot-separated labels; each label is 1 to 63 Latin letters, digits and hyphens, and a hyphen is not allowed at the start or the end of a label |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/domain" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "fqdn": "app.example.com"
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/domain" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fqdn": "app.example.com"
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/domain`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      fqdn: 'app.example.com',
    }),
  }
)
const { data } = await res.json()
for (const record of data.instructions) {
  console.log(`${record.type} ${record.name} → ${record.value}`)
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/domain`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      fqdn: 'app.example.com',
    }),
  }
)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.domain.id` | string (UUID) | Domain attachment ID |
| `data.domain.fqdn` | string | The attached name in lowercase |
| `data.domain.isApex` | boolean | `true` — the apex domain of the zone, `false` — a subdomain |
| `data.domain.status` | string | Domain status. After attachment — `"PENDING_VERIFICATION"`. All values — [Domain statuses](/docs/infra/domain#domain-statuses) |
| `data.domain.source` | string | Origin of the domain. `"BYO"` — the domain is yours and was connected by you |
| `data.domain.dnsMode` | string | Who maintains the DNS records. `"USER_DNS"` — you, at your DNS provider |
| `data.domain.verifiedAt` | string (ISO 8601) \| null | Time of the successful verification after which the domain became `ACTIVE`. After attachment — `null` |
| `data.domain.lastVerifiedAt` | string (ISO 8601) \| null | Time of the last record verification, including a failed one. After attachment — `null` |
| `data.domain.createdAt` | string (ISO 8601) | Time of attachment |
| `data.domain.certIssued` | boolean | Whether the certificate for `https` has been issued. After attachment — `false` |
| `data.instructions` | array | Two DNS records to create at the domain's DNS provider |
| `data.instructions[].type` | string | Record type: `"TXT"`, `"CNAME"` or `"A"` |
| `data.instructions[].host` | string | Full record name |
| `data.instructions[].zone` | string | Zone in which the record is created |
| `data.instructions[].name` | string | Record name relative to `zone` — the value entered in the "name" field of the DNS provider's panel. For the zone itself — `"@"` |
| `data.instructions[].value` | string | Record value |

## Response example

Response `201` for attaching a subdomain:

```json
{
  "success": true,
  "data": {
    "domain": {
      "id": "bea9db66-411b-432e-a365-283a74f5714f",
      "fqdn": "app.example.com",
      "isApex": false,
      "status": "PENDING_VERIFICATION",
      "source": "BYO",
      "dnsMode": "USER_DNS",
      "verifiedAt": null,
      "lastVerifiedAt": null,
      "createdAt": "2026-10-05T12:53:02.616Z",
      "certIssued": false
    },
    "instructions": [
      {
        "type": "TXT",
        "host": "_vibecode-challenge.app.example.com",
        "zone": "example.com",
        "name": "_vibecode-challenge.app",
        "value": "vibe-verify=f36ac31f213d1e3630c75f000b6631f5"
      },
      {
        "type": "CNAME",
        "host": "app.example.com",
        "zone": "example.com",
        "name": "app",
        "value": "lb.bitrix24.site"
      }
    ]
  }
}
```

The apex domain `example.com` would get `"isApex": true`, and the second record would be `"type": "A"` with `"name": "@"` and an IPv4 address in `value`. The rest of the response shape does not change.

## Error response example

409 — the name is already attached to another server on the platform:

```json
{
  "success": false,
  "error": {
    "code": "DOMAIN_TAKEN",
    "message": "This domain is already in use"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | The `fqdn` field is missing or is not a domain name |
| 400 | `FQDN_INVALID` | An IP address was passed in `fqdn` |
| 400 | `FQDN_RESERVED` | The name belongs to the platform: it is the platform's domain or its subdomain |
| 400 | `FQDN_FORBIDDEN` | The name is forbidden: it contains the substring `bitrix` |
| 400 | `INVALID_JSON_BODY` | The request body cannot be parsed as JSON |
| 409 | `CUSTOM_DOMAIN_GALAXY_MOVE_ACTIVE` | The server is moving to a Galaxy: it is the old server of the move or the place prepared for it. Attach the domain after the move is completed or cancelled |
| 409 | `CUSTOM_DOMAIN_LIMIT` | The server already has a domain, including one with the same name. [Detach it](./detach.md) first |
| 409 | `DOMAIN_TAKEN` | The name is attached to another server on the platform |
| 409 | `DOMAIN_TAKEN_EXTERNAL` | The name is connected to another Bitrix24 product. Disconnect it there and repeat the call |
| 503 | `CUSTOM_DOMAIN_REGISTRY_UNAVAILABLE` | The domain registry did not respond. The domain is not attached; repeat the call later |
| 403 | `CUSTOM_DOMAINS_DISABLED` | Custom domains are not enabled for the server owner |
| 403 | `CUSTOM_DOMAINS_ZONE_UNSUPPORTED` | The region of the Bitrix24 account does not support custom domains |
| 403 | `CUSTOM_DOMAINS_REQUIRE_COMMERCIAL` | The Bitrix24 account has no commercial plan |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `INVALID_API_KEY` | The key is not recognized — no such key exists on the platform |
| 403 | `INFRA_SCOPE_REQUIRED` | The key lacks the `vibe:infra` scope |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | The call was made with a Cowork/Code key — such a key works with data only and cannot perform write operations. What to do — [Project key for deploy](/docs/cowork/deploy-key) |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key is in read-only mode and cannot perform write operations |
| 404 | `SERVER_NOT_FOUND` | No server with this `id` exists, or it is managed by a key other than the one used for the call |
| 429 | `RATE_LIMITED` | The limit of 5 requests per minute for this endpoint was exceeded. The limit is counted per key owner, not per individual key. The exact value arrives in the `x-ratelimit-limit` header (the cap is divided across replicas) |
| 503 | `user_self_deletion_pending` | The key owner requested deletion of their account on the portal. Access is restored once the deletion is cancelled |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **Order of checks.** The key scope and server ownership are checked first, then the three access conditions for custom domains, and only after them the request body. So a request with an invalid `fqdn` from an account that has no access to custom domains gets `403`, not `400`. The exception is `INVALID_JSON_BODY`: the body is parsed before the key check, so a body with a JSON syntax error gets `400` before all other refusals. The `FQDN_FORBIDDEN` and `DOMAIN_TAKEN_EXTERNAL` refusals come last: they are issued by the domain registry, which the platform queries after its own checks.
- **The name is fully secured only at verification.** Attachment closes the name to other servers on the platform, but in the Bitrix24 domain registry it is taken only after a successful [verification](./verify.md). If the name is connected to another Bitrix24 product between attachment and verification, the verification returns the `DOMAIN_TAKEN_EXTERNAL` refusal instead.
- **The records can be fetched again.** The `instructions` array with the same `TXT` value is returned at any time by [`GET /v1/infra/servers/:id/domain`](./get.md); storing the attachment response is not required.

## See also

- [Get the domain](./get.md)
- [Verify the domain](./verify.md)
- [Detach a domain](./detach.md)
- [Custom domain](/docs/infra/domain)
- [List servers](/docs/infra/servers/list)
