> ## Documentation Index
> Fetch the complete documentation index at: https://docs.basaltic.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify zone ownership

> Check the domain-ownership challenge and, if it is published, start
serving the zone.

A zone created with no existing zone above it on the platform does not
resolve until ownership is proved. Publish the TXT record from the
zone's `ownership` block at your CURRENT DNS provider — before
delegating to us — then call this. Proof requires control of the
domain's existing delegation, which is what stops someone claiming a
subdomain of a domain they do not own and sitting in your subtree.

A zone created underneath a zone you already own inherits its proof and
is served immediately; calling this on one returns success with nothing
to do. Idempotent, so a client may poll it.

This is also the call that clears a failing re-proof. Ownership is
re-confirmed periodically, and a zone whose re-confirmation is failing
carries an `ownership.recheck_deadline`; on such a zone this runs the
check for real and clears the deadline when it passes. So unlike the
already-verified case above, a verified zone can still answer 400 here.




## OpenAPI

````yaml /api-reference/specs/dns.yaml post /v1/zones/{zone_id}/verify-ownership
openapi: 3.0.3
info:
  title: Basaltic DNS API
  version: 1.0.0
  description: |
    Authoritative hosted zones and their records, signed with DNSSEC. A zone
    is only served once its ownership is verified; associating a zone with a
    VPC makes it resolvable privately inside that network.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://dns.basaltic.sh
    description: Global API endpoint
security:
  - SignatureAuth: []
paths:
  /v1/zones/{zone_id}/verify-ownership:
    post:
      tags:
        - DNS
      summary: Verify zone ownership
      description: |
        Check the domain-ownership challenge and, if it is published, start
        serving the zone.

        A zone created with no existing zone above it on the platform does not
        resolve until ownership is proved. Publish the TXT record from the
        zone's `ownership` block at your CURRENT DNS provider — before
        delegating to us — then call this. Proof requires control of the
        domain's existing delegation, which is what stops someone claiming a
        subdomain of a domain they do not own and sitting in your subtree.

        A zone created underneath a zone you already own inherits its proof and
        is served immediately; calling this on one returns success with nothing
        to do. Idempotent, so a client may poll it.

        This is also the call that clears a failing re-proof. Ownership is
        re-confirmed periodically, and a zone whose re-confirmation is failing
        carries an `ownership.recheck_deadline`; on such a zone this runs the
        check for real and clears the deadline when it passes. So unlike the
        already-verified case above, a verified zone can still answer 400 here.
      operationId: verifyZoneOwnership
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/ZoneId'
      responses:
        '200':
          description: Ownership verified; the zone now resolves
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZoneResponse'
        '400':
          description: |
            Ownership is not proved: the challenge TXT is absent or does not
            match, and the zone is not delegated here either. The message names
            both remedies — publish the TXT, or delegate the zone to our
            nameservers — and the same text comes back whichever check failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
      security:
        - SignatureAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: >-
        Optional client-generated key that makes a create replay-safe. Retrying
        a request with the same key returns the original outcome verbatim
        instead of creating a duplicate resource. Reusing a key with a different
        request body is rejected (422); a request whose key is still being
        processed returns 409. Records are honored for 24 hours. Use a UUID or
        similarly unique token.
      required: false
      schema:
        type: string
        maxLength: 255
      example: 550e8400-e29b-41d4-a716-446655440000
    ZoneId:
      name: zone_id
      in: path
      description: DNS Zone ID
      required: true
      schema:
        type: string
        format: uuid
      example: 550e8400-e29b-41d4-a716-446655440000
  schemas:
    ZoneResponse:
      type: object
      properties:
        zone:
          $ref: '#/components/schemas/Zone'
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Error code identifying the type of error
              example: INVALID_INPUT
            message:
              type: string
              description: Human-readable error message
              example: Invalid request parameters
            request_id:
              type: string
              format: uuid
              description: Request ID for debugging
              example: 550e8400-e29b-41d4-a716-446655440000
    Zone:
      type: object
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
          example: 550e8400-e29b-41d4-a716-446655440000
        crn:
          type: string
          readOnly: true
          description: Cloud Resource Name.
          example: crn:dns::my-account:zone/example.com
        name:
          type: string
          description: Zone FQDN.
          example: example.com
        nameservers:
          type: array
          items:
            type: string
          description: |
            Authoritative nameservers for the zone — the apex NS set.
            Copy this list verbatim into your registrar's nameserver
            configuration to delegate the zone to the platform.
          example:
            - ns1.cloud.bycoded.dev
            - ns2.cloud.bycoded.dev
        soa:
          $ref: '#/components/schemas/SOA'
        visibility:
          type: string
          enum:
            - public
            - private
          readOnly: true
          description: |
            `public` zones answer on the internet-facing nameservers; `private`
            zones answer only inside the VPCs associated with them (see the
            vpc-associations endpoints). Fixed at creation.
          example: public
        dnssec:
          $ref: '#/components/schemas/ZoneDNSSEC'
        tags:
          $ref: '#/components/schemas/Tags'
        ownership:
          $ref: '#/components/schemas/ZoneOwnership'
    SOA:
      type: object
      properties:
        primary_ns:
          type: string
          description: SOA `mname` — first authoritative nameserver for the zone.
          example: ns1.cloud.bycoded.dev
        admin_email:
          type: string
          format: email
          readOnly: true
          description: |
            SOA `rname` — the platform's DNS-operations contact.
            Stamped server-side; not customer-configurable.
          example: dns@bycoded.dev
        refresh:
          type: integer
          example: 10800
        retry:
          type: integer
          example: 3600
        expire:
          type: integer
          example: 604800
        minimum:
          type: integer
          description: NXDOMAIN cache TTL.
          example: 3600
    ZoneDNSSEC:
      type: object
      description: |
        DNSSEC signing state. Present once the signer has bootstrapped the zone,
        which is always-on for zones created through this API; absent on a zone
        that is not signed.

        `ds_records` is what the customer pastes at the parent registrar to
        complete the chain of trust. `rdata` is the zone-file form of the same
        record, for registrars that take one string.
      required:
        - enabled
      properties:
        enabled:
          type: boolean
          readOnly: true
          example: true
        ksk_key_tag:
          type: integer
          readOnly: true
          description: Key tag of the key-signing key — matches the DS records below.
          example: 12345
        zsk_key_tag:
          type: integer
          readOnly: true
          description: Key tag of the zone-signing key.
          example: 54321
        algorithm:
          type: integer
          readOnly: true
          description: DNSSEC algorithm number. 13 = ECDSA P-256 SHA-256.
          example: 13
        ds_records:
          type: array
          readOnly: true
          items:
            $ref: '#/components/schemas/ZoneDSRecord'
    Tags:
      type: object
      additionalProperties:
        type: string
      example:
        environment: production
        team: backend
    ZoneOwnership:
      type: object
      description: >
        Domain-ownership proof. A zone with no existing zone above it on the

        platform does not resolve until `verified` is true — publish the TXT

        record below at your current DNS provider, then POST

        /v1/zones/{zone_id}/verify-ownership.


        If the domain is already delegated to us there is nowhere to publish
        that

        TXT, because we are its current DNS and an unverified zone does not
        answer.

        Use `claim_nameservers` instead.


        A zone created underneath a zone you already own inherits its parent's

        proof and is verified on creation.


        The proof is a lease, not a deed: it is re-confirmed periodically for as

        long as the zone is served, so a domain that lapses or changes hands
        stops

        being served from here. `checked_at` is the last confirmation;

        `recheck_deadline` appears only while re-confirmation is failing and is

        the field to render a warning from.
      properties:
        verified:
          type: boolean
          readOnly: true
          description: >-
            Whether the zone has proved ownership. Unverified zones do not
            resolve.
          example: false
        verified_at:
          type: string
          format: date-time
          readOnly: true
          nullable: true
          description: When ownership was first proved. Absent while unverified.
          example: '2026-01-15T09:30:00Z'
        checked_at:
          type: string
          format: date-time
          readOnly: true
          description: >
            When the proof was last re-confirmed. `verified_at` keeps meaning

            "first demonstrated" and does not move; this does, on every pass
            that

            passes. Absent until the zone has been re-confirmed at least once.
          example: '2026-04-14T09:30:00Z'
        recheck_deadline:
          type: string
          format: date-time
          readOnly: true
          description: >
            Present ONLY while the periodic re-proof is failing: the instant the

            zone stops answering unless it passes again. Absent means healthy.


            Each pass re-checks a failing zone, so reaching this date takes

            sustained failure, not one bad afternoon — a transient resolver

            problem clears itself on the next pass. To clear it deliberately,

            republish the challenge TXT (or point the domain's delegation back
            at

            our nameservers) and POST /v1/zones/{zone_id}/verify-ownership.


            The organization's owner is emailed when this date is set, and again

            if the zone does stop resolving. A zone is never taken off the air

            before that message has gone out, so the date here is the earliest

            the zone can stop answering and never the only warning.
          example: '2026-04-28T09:30:00Z'
        record_type:
          type: string
          readOnly: true
          example: TXT
        record_name:
          type: string
          readOnly: true
          description: Owner name the challenge TXT must be published at.
          example: _basaltic-challenge.example.com
        record_value:
          type: string
          readOnly: true
          description: Value the challenge TXT must contain.
          example: 9f2c1e7a4b8d6035e1a4c9f27b0d8e63a5f1c4b90d2e7a836f5c1b904e7d2a68
        claim_nameservers:
          type: array
          readOnly: true
          description: >
            The other way to prove ownership: set these as the domain's

            nameservers at your registrar, then POST

            /v1/zones/{zone_id}/verify-ownership. This is the route for a domain

            already delegated to us, where the challenge TXT can never resolve.


            These names reach the same servers the zone's `nameservers` do, but

            they are unique to this zone, and that is what makes them proof.

            Anyone can point a domain at our published nameservers, so a
            delegation

            naming those shows only that somebody did so at some point —
            possibly

            a previous owner who never updated their registrar. A delegation
            naming

            these can only have been set for this zone, by whoever holds the
            domain

            now.


            The delegation may stay on these names permanently.
          items:
            type: string
          example:
            - ns1.9f2c1e7a4b8d6035.claim.dns.basaltic.cloud
            - ns2.9f2c1e7a4b8d6035.claim.dns.basaltic.cloud
    ZoneDSRecord:
      type: object
      description: One DS record to publish at the parent zone.
      required:
        - key_tag
        - algorithm
        - digest_type
        - digest
        - rdata
      properties:
        key_tag:
          type: integer
          example: 12345
        algorithm:
          type: integer
          example: 13
        digest_type:
          type: integer
          description: 2 = SHA-256.
          example: 2
        digest:
          type: string
          example: B8FA03AA1BB49CC702C32C5C3FE6061BBDD0808371F43A28A8E8BE2738CC9861
        rdata:
          type: string
          description: >-
            Full zone-file form — `<key_tag> <algorithm> <digest_type>
            <digest>`.
          example: >-
            12345 13 2
            B8FA03AA1BB49CC702C32C5C3FE6061BBDD0808371F43A28A8E8BE2738CC9861
  responses:
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Authentication required
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: ACCESS_DENIED
              message: You don't have permission to perform this action
              request_id: 550e8400-e29b-41d4-a716-446655440000
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: Resource not found
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Conflict:
      description: Resource conflict (e.g., already exists, invalid state)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: CONFLICT
              message: Resource with this name already exists
              request_id: 550e8400-e29b-41d4-a716-446655440000
    UnprocessableEntity:
      description: |
        The request is well-formed but cannot be processed as sent. On the
        operations that accept `Idempotency-Key` this is the key-reuse case: the
        key was first seen with a different request payload, so replaying the
        stored outcome would answer a question the caller did not ask.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: IDEMPOTENCY_KEY_REUSED
              message: >-
                This Idempotency-Key was already used with a different request
                payload
              request_id: 550e8400-e29b-41d4-a716-446655440000
    InternalServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INTERNAL_ERROR
              message: An internal error occurred
              request_id: 550e8400-e29b-41d4-a716-446655440000
  securitySchemes:
    SignatureAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Request signing with an access key issued to a service account. An

        HMAC-SHA256 over a canonical form of the request, close to AWS SigV4.

        The `basaltic` CLI signs for you.


        Send `Authorization`, `X-Date` (UTC, `YYYYMMDDTHHMMSSZ`) and `X-Nonce`

        (random per request); add `X-Content-Sha256` to bind a body, and

        `X-Amz-Security-Token` when using temporary credentials.


        ```

        Authorization: BASALTIC-HMAC-SHA256
        Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request,
        SignedHeaders=host;x-date;x-nonce, Signature=<hex>

        ```


        `<region>` is the region code you are calling, or `global` for the
        global

        services. A signature is valid for 5 minutes from `X-Date`, and mutating

        requests are replay-guarded on the nonce.


        **Full signing procedure, including a working implementation:**

        https://docs.basaltic.sh/authentication


        ## Rate limits

        There is no global request budget. A limit applies only where an

        operation documents a `429`, and that operation says what it counts.

        Those responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`,

        `X-RateLimit-Reset` and, on a `429`, `Retry-After` — read them rather

        than hard-coding a number. Retrying before `Retry-After` is refused and

        extends the window. Everything else is bounded by quota, not by request

        rate.

````