## Verify the domain

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

Checks both DNS records of the attached domain and, if they are correct, enables the domain: the application starts responding on it. The same call re-enables a domain that was disabled after a failed check.

Who can make the call — [Access](/docs/infra/domain#access).

## 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) |

The request body is empty.

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/domain/verify" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/domain/verify" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — personal key

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/domain/verify`,
  { method: 'POST', headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
)
const body = await res.json()
if (body.success) {
  console.log(`Domain enabled: ${body.data.domain.fqdn}`)
} else if (body.error.code === 'DOMAIN_VERIFICATION_FAILED') {
  // Show which record failed the check
  for (const record of body.error.details.records) {
    if (record.status !== 'ok') console.log(`${record.type} ${record.host}: ${record.status}`)
  }
} else {
  throw new Error(body.error.code)
}
```

### JavaScript — OAuth application

```javascript
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/domain/verify`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
    },
  }
)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | `true` when both records are correct and the domain is enabled |
| `data.domain.id` | string (UUID) | Domain attachment ID |
| `data.domain.fqdn` | string | Domain name in lowercase |
| `data.domain.isApex` | boolean | `true` — apex domain, `false` — subdomain |
| `data.domain.status` | string | Domain status, `ACTIVE` in this response. All values — [Domain statuses](/docs/infra/domain#domain-statuses) |
| `data.domain.source` | string | Origin of the domain. `BYO` — a domain you own |
| `data.domain.dnsMode` | string | Who maintains the DNS records. `USER_DNS` — you, at your DNS provider |
| `data.domain.verifiedAt` | string \| null | Time of the last successful check, ISO 8601 |
| `data.domain.lastVerifiedAt` | string \| null | Time of the last check, successful or not, ISO 8601 |
| `data.domain.createdAt` | string | Time the domain was attached, ISO 8601 |
| `data.domain.certIssued` | boolean | Whether the certificate for `https` has been issued |
| `data.records` | array | Result for each record: `TXT` comes first, `CNAME` or `A` second |
| `data.records[].type` | string | Record type: `TXT`, `CNAME` or `A` |
| `data.records[].host` | string | Full record name |
| `data.records[].status` | string | `ok` — the record is correct, `missing` — not found, `wrong` — found with a different value. In this response both records are `ok` |
| `data.records[].found` | string[] | Values the check read from DNS. A `CNAME` record may carry IP addresses here: that is how a DNS provider that flattens `CNAME` into addresses responds |

## Response example

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

## Error response example

422 — the `TXT` record is correct, but `CNAME` points to a different address:

```json
{
  "success": false,
  "error": {
    "code": "DOMAIN_VERIFICATION_FAILED",
    "message": "Domain verification failed",
    "details": {
      "reason": "NOT_POINTING_TO_US",
      "records": [
        {
          "type": "TXT",
          "host": "_vibecode-challenge.app.example.com",
          "status": "ok",
          "found": [
            "vibe-verify=f36ac31f213d1e3630c75f000b6631f5"
          ]
        },
        {
          "type": "CNAME",
          "host": "app.example.com",
          "status": "wrong",
          "found": [
            "other.example.net"
          ]
        }
      ],
      "domain": {
        "id": "bea9db66-411b-432e-a365-283a74f5714f",
        "fqdn": "app.example.com",
        "isApex": false,
        "status": "FAILED",
        "source": "BYO",
        "dnsMode": "USER_DNS",
        "verifiedAt": null,
        "lastVerifiedAt": "2026-10-05T12:53:02.648Z",
        "createdAt": "2026-10-05T12:53:02.616Z",
        "certIssued": false
      },
      "hint": "Fix the records whose status is \"missing\" or \"wrong\" (see records), then repeat the check. A record that is already published is read from the authoritative name servers of the zone, so it is seen at once. If the check fails right after you edited DNS, the DNS provider has not published the change yet, or a resolver still returns the previous answer until the record TTL expires — for a name that did not exist before, that is the negative-cache TTL of the zone (SOA minimum, up to an hour). Repeating the check immediately returns the same result: wait a few minutes between attempts. The check is limited to 5 calls per minute."
    }
  }
}
```

The `error.details` fields of this refusal:

| Field | Type | Description |
|------|-----|----------|
| `error.details.reason` | string | The first record that failed. `OWNERSHIP_TXT_MISSING` — the `TXT` record is not found or incorrect, `NOT_POINTING_TO_US` — `TXT` is correct, but the domain does not point to the platform |
| `error.details.records` | array | Result for both records, with the same fields as `data.records`. Until the `TXT` record is confirmed, `found` is empty for both records |
| `error.details.records[].hint` | string | Not present on every record. `DOUBLED_ZONE` — the record is missing, but it was found under a name with the zone appended twice: `host` was entered in the DNS panel instead of `name`, see [DNS records](/docs/infra/domain#dns-records). `HAS_AAAA` — the domain name has an `AAAA` record, delete it |
| `error.details.domain` | object | The domain after the check, with the same fields as `data.domain`. Status — `FAILED` |
| `error.details.hint` | string | Text in English: what to fix and when to repeat the check |

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `FQDN_FORBIDDEN` | Both records are correct, but the domain name is not allowed: it must not contain `bitrix`. The domain moves to `FAILED` |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is not passed |
| 401 | `INVALID_API_KEY` | The key is not recognized: no such string exists on the platform |
| 403 | `INFRA_SCOPE_REQUIRED` | The key lacks the `vibe:infra` scope. Checked before the server lookup |
| 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 — write operations are blocked for it |
| 403 | `CUSTOM_DOMAINS_DISABLED` | Custom domains are not enabled for the server owner. Conditions — [Access](/docs/infra/domain#access) |
| 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 |
| 404 | `SERVER_NOT_FOUND` | The server is not found, is deleted, or is managed by another key |
| 404 | `NO_DOMAIN` | No domain is attached to the server. Checked before the access conditions that return `403` |
| 409 | `SERVER_HAS_NO_APP_HOST` | Both records are correct, but the server has no public application address yet. The domain status does not change. Deploy the application and repeat the check |
| 409 | `DOMAIN_TAKEN_EXTERNAL` | Both records are correct, but after the domain was attached the name was connected to another Bitrix24 product. The domain moves to `FAILED`. Disconnect the name there and repeat the check |
| 422 | `DOMAIN_VERIFICATION_FAILED` | A record is not found or incorrect. The domain moves to `FAILED`. Which record failed — in `error.details.records` |
| 429 | `RATE_LIMITED` | The limit of 5 requests per minute for this endpoint is exceeded. The limit is counted per key owner, not per individual key. The exact value is in the `x-ratelimit-limit` header (the cap is divided across replicas) |
| 503 | `CUSTOM_DOMAIN_REGISTRY_UNAVAILABLE` | The domain registry did not respond, or the platform could not read the address it compares the records against. The domain status stays as it was. Repeat the call later |
| 503 | `user_self_deletion_pending` | The key owner requested deletion of their account on the portal. Access is restored once the deletion is cancelled |

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

## Known specifics

- **Repeating the check right after `422` returns the same result.** A record the DNS provider has already published is read from the authoritative servers of the zone and is seen at once. If the refusal came right after a DNS edit, the provider has not published the change yet, or a caching DNS server still returns the previous answer until the record's time to live (TTL) expires. For a name that did not exist before, that is up to an hour. Wait a few minutes between attempts.
- **Checking a working domain can disable it.** The call is also allowed for a domain in the `ACTIVE` status. If the records are incorrect by then, the domain moves to `FAILED`, and the application stops responding on it until the next successful check.
- **The certificate is issued after the response.** When the domain reaches `ACTIVE` from another status, `certIssued` is `false`. `https` readiness is shown by [`GET /v1/infra/servers/:id/domain`](./get.md).

## See also

- [Attach a domain](./attach.md)
- [Get the domain](./get.md)
- [Detach a domain](./detach.md)
- [Custom domain](/docs/infra/domain)
