{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/customdomain-ai/json-schema/customdomain-ai-nameserver-migration-schema.json",
  "title": "NameserverMigration",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/customdomain-ai-openapi.yml#/components/schemas/NameserverMigration",
  "type": "object",
  "required": [
    "id",
    "zone",
    "state",
    "path",
    "target",
    "registrar",
    "old_nameservers",
    "new_nameservers",
    "observed_nameservers",
    "records_copied",
    "email_records",
    "email_kinds",
    "copy_warnings",
    "ds_present",
    "can_rollback",
    "rollback_targets",
    "old_dns_serves",
    "next_action",
    "created_at",
    "updated_at"
  ],
  "properties": {
    "id": {
      "type": "string"
    },
    "connection_id": {
      "type": "string"
    },
    "zone": {
      "type": "string",
      "description": "The registrable domain whose nameservers move."
    },
    "state": {
      "type": "string",
      "enum": [
        "pending",
        "copying",
        "needs_review",
        "needs_dnssec_off",
        "waiting_ds",
        "setting",
        "propagating",
        "active",
        "failed",
        "rolled_back"
      ],
      "description": "`pending`: target `custom` waits for the platform's zone. `copying`: the zone is being created and the records copied; a copy whose write to the DNS servers failed is retried, never switched to. `needs_review`: the copy is not proven complete (it was read from public DNS, which can't list a zone, or some records could not be carried over); a person checks `copy_warnings` and confirms with `copy_reviewed`. `needs_dnssec_off`: DNSSEC must be turned off at the registrar first. `waiting_ds`: DNSSEC is off; waiting for the registry to stop publishing the DS record and its TTL to pass. `setting`: the nameservers are being changed (`waiting_for` says by whom). `propagating`: changed; waiting for the registry to list every new nameserver (checked every 5 minutes for 2 hours, then hourly up to 48 hours). `active`: the registry lists them (re-checked every 6 hours; a move away is `failed` with `delegation_drift`). `failed`: see `failure_code`. `rolled_back`: moved back."
    },
    "path": {
      "type": "string",
      "enum": [
        "api",
        "agent",
        "manual"
      ]
    },
    "target": {
      "type": "string",
      "enum": [
        "managed",
        "dns_host",
        "custom"
      ]
    },
    "dns_host": {
      "type": "string",
      "description": "The DNS host of target `dns_host` (an adapter id such as `cloudflare`)."
    },
    "provider": {
      "type": "string",
      "description": "Where the nameservers are changed: the registrar's provider id (`registrar.provider`), or, when `registrar` is null, the DNS host the domain used, as a guess (then `next_step` says \"where the domain is registered\" and names no company)."
    },
    "registrar": {
      "description": "Who the domain is registered with (RDAP, or the account or key the caller brought); null when unknown.",
      "oneOf": [
        {
          "$ref": "#/$defs/NameserverRegistrar"
        },
        {
          "type": "null"
        }
      ]
    },
    "old_dns_host": {
      "type": "string",
      "description": "The DNS host the domain used when the move started (a provider id such as `cloudflare`)."
    },
    "old_nameservers": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "What the registry listed when the move started; a rollback sets these."
    },
    "new_nameservers": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "observed_nameservers": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "What the registry listed at the last check."
    },
    "waiting_for": {
      "type": "string",
      "enum": [
        "zone_ready",
        "zone_served",
        "copy_review",
        "dnssec_off",
        "ds_clear",
        "credential",
        "registrar_confirm",
        "browser_session",
        "registrar",
        "delegation"
      ]
    },
    "next_step": {
      "type": "string",
      "description": "One sentence for a person."
    },
    "next_action": {
      "description": "The one request to press next, read from the move alone (the same for every caller), so a card can show the button `next_step` only describes. `null` when nothing is waiting for a press: the move is copying, waiting for the registry, waiting for a person at the registrar, DNSSEC or the copy review (`copy_reviewed`), or over. Because it is the same for every caller, it can name a request the caller who reads it is refused. Four cases: `sign_in` points at `POST /v1/connections/{id}/browser:start`, which runs only from a customer's browser or the console and refuses every agent token and every API key without the console's client label (`403 browser_customer_present_only`); `retry` is refused `403 ns_not_offered` to an agent token while the application's DNS hosting is off; `retry` is refused `403 forbidden` to a widget token when the setting is off; and `retry` of a move made with a connected account is refused `403 forbidden` to an API key whose member may not use that account (another member's). A caller in one of those cases tells the person (`next_step` says what to do) and does not send the request. `rollback` is open to all three callers whatever the setting says.",
      "oneOf": [
        {
          "$ref": "#/$defs/NameserverNextAction"
        },
        {
          "type": "null"
        }
      ]
    },
    "failure_code": {
      "type": "string",
      "enum": [
        "zone_copy_failed",
        "zone_not_served",
        "dnssec_timeout",
        "ds_removal_failed",
        "credential_refused",
        "registrar_refused",
        "registrar_locked",
        "changed_elsewhere",
        "agent_failed",
        "delegation_timeout",
        "delegation_drift",
        "internal"
      ],
      "description": "`changed_elsewhere`: the registrar listed other nameservers than the ones the move started from, so the change did not overwrite them."
    },
    "failure_detail": {
      "type": "string"
    },
    "records_copied": {
      "type": "integer"
    },
    "email_records": {
      "type": "integer",
      "description": "How many of the copied records carry mail (MX, SPF, DKIM, DMARC)."
    },
    "email_kinds": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "mx",
          "spf",
          "dkim",
          "dmarc"
        ]
      }
    },
    "copy_source": {
      "type": "string",
      "enum": [
        "api",
        "discovery",
        "existing_zone"
      ],
      "description": "How the records were copied. `api`: the whole zone, read through a key we hold at the DNS host the domain used (a kept sign in or a connected account there); proven complete. `discovery`: read from public DNS at the names domains commonly use; a person confirms it. `existing_zone`: the platform's zone already held records, which were kept."
    },
    "copy_warnings": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "description": "What a person should check before the switch: records left out (a Cloudflare proxied record answers with Cloudflare's addresses, not the origin), lookups that went unanswered, forwarding that stays at the old host."
    },
    "copy_reviewed_at": {
      "type": "string",
      "format": "date-time",
      "description": "When a person confirmed a copy that is not proven complete."
    },
    "ds_present": {
      "type": "boolean",
      "description": "The registry published a DS record (DNSSEC was on) during the move."
    },
    "ds_removed_at": {
      "type": "string",
      "format": "date-time"
    },
    "ds_cleared_at": {
      "type": "string",
      "format": "date-time"
    },
    "can_rollback": {
      "type": "boolean"
    },
    "rollback_path": {
      "type": "string",
      "enum": [
        "api",
        "agent",
        "manual",
        "none"
      ],
      "description": "How the move back was made: `api` (the registrar's API, done), `agent` (the person signs in to the registrar in the cloud browser with `purpose: nameservers` and approves it), `manual` (`next_step` says what to set or choose), `none` (nothing had changed yet)."
    },
    "rollback_targets": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": [
          "previous",
          "registrar_default"
        ]
      },
      "description": "The move backs possible now; `previous` is left out once the old nameservers no longer answer for the zone."
    },
    "rollback_default": {
      "type": "string",
      "enum": [
        "previous",
        "registrar_default"
      ],
      "description": "Where a move back without `rollback_target` goes."
    },
    "rollback_target": {
      "type": "string",
      "enum": [
        "previous",
        "registrar_default"
      ],
      "description": "Where the move back went."
    },
    "rollback_set_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the move back was made at the registrar; absent while a person or the cloud browser still has it to do."
    },
    "old_dns_serves": {
      "type": [
        "boolean",
        "null"
      ],
      "description": "Whether the old nameservers still answered for the zone when last asked (once a day while active, and at a move back); null when not asked yet."
    },
    "consent": {
      "type": "object",
      "required": [
        "id",
        "version",
        "at"
      ],
      "properties": {
        "id": {
          "type": "string"
        },
        "version": {
          "type": "string"
        },
        "at": {
          "type": "string",
          "format": "date-time"
        }
      }
    },
    "actor": {
      "type": "string",
      "enum": [
        "end_user",
        "api_key",
        "console",
        "agent"
      ]
    },
    "created_at": {
      "type": "string",
      "format": "date-time"
    },
    "set_at": {
      "type": "string",
      "format": "date-time",
      "description": "When the nameservers were changed at the registrar (or first seen changed)."
    },
    "active_at": {
      "type": "string",
      "format": "date-time"
    },
    "failed_at": {
      "type": "string",
      "format": "date-time"
    },
    "rolled_back_at": {
      "type": "string",
      "format": "date-time"
    },
    "drifted_at": {
      "type": "string",
      "format": "date-time"
    },
    "checked_at": {
      "type": "string",
      "format": "date-time"
    },
    "next_check_at": {
      "type": "string",
      "format": "date-time"
    },
    "updated_at": {
      "type": "string",
      "format": "date-time"
    }
  },
  "$defs": {
    "NameserverNextAction": {
      "type": "object",
      "required": [
        "kind",
        "method",
        "path"
      ],
      "description": "A request that moves a nameserver migration on, with the connection's id filled in. The caller adds what it always adds to that route.",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "sign_in",
            "retry",
            "rollback"
          ],
          "description": "`sign_in`: the move waits for the cloud browser (state `needs_dnssec_off` or `setting` on path `agent`, or a move back whose `rollback_path` is `agent` and not yet made), so the person signs in to their registrar and approves what will change. The request is the registrar sign in's start. `retry`: the move failed before anything was changed at the registrar (`can_rollback` is false); the start call with this `body` begins the same move again, on the same path and, for `api` with a connected account, with that account, where a start with no body picks them afresh (a move made with a key sent once keeps no account: its retry is `{path: api}` and the key is sent again; the end user's start also sends `consent`). `rollback`: the move failed after the registrar was changed or asked (`can_rollback` is true), and moving back is the safe way out, to where a move back without a choice goes (`rollback_default`)."
        },
        "method": {
          "type": "string",
          "enum": [
            "POST"
          ]
        },
        "path": {
          "type": "string",
          "description": "The route: `/v1/connections/{id}/browser:start` for `sign_in`, `/v1/connections/{id}/nameservers:migrate` for `retry`, `/v1/connections/{id}/nameservers:rollback` for `rollback`."
        },
        "body": {
          "type": "object",
          "additionalProperties": true,
          "description": "The fields the route needs beyond the ones the caller always sends. `sign_in`: `purpose` `nameservers` and the registrar's `provider` (the caller adds `return_origin` and `consent_version`). `rollback`: `rollback_target`. `retry`: the move's `path`, and for `api` with a connected account that account's `provider_account_id` (a widget token reads the path only: it may not name an account; a move made with a key sent once has none, so only `{path: api}`), absent when the move recorded no path. A caller who may not use that account is refused `403 forbidden`."
        }
      }
    },
    "NameserverRegistrar": {
      "type": "object",
      "description": "The company a domain is registered with, where its nameservers are changed. Looked up over RDAP at the TLD's registry (found through the IANA bootstrap file) and matched to a provider by its IANA Registrar ID, so a domain registered at GoDaddy with its DNS at Cloudflare is changed at GoDaddy.",
      "required": [
        "name",
        "source"
      ],
      "properties": {
        "name": {
          "type": "string",
          "description": "The brand (\"GoDaddy\"), the reseller the registry names, or RDAP's own name."
        },
        "provider": {
          "type": "string",
          "description": "Our provider id for it, when we route to it (`godaddy`)."
        },
        "iana_id": {
          "type": "integer",
          "description": "The IANA Registrar ID, when the registry gives one."
        },
        "url": {
          "type": "string",
          "description": "The registrar's website."
        },
        "source": {
          "type": "string",
          "enum": [
            "rdap",
            "request",
            "account",
            ""
          ],
          "description": "`rdap`: looked up. `request`: the caller named it with a key or an account. `account`: RDAP said nothing, and a connected account read the domain's registration. Empty: RDAP named a registrar we do not route to (`name` only)."
        }
      }
    }
  }
}