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

# Create zone

> Create a new DNS zone. The SOA and NS records are generated
automatically against the platform nameserver list; the zone is
queryable immediately on success.

Defaults to a public zone. Pass `visibility: private` plus at least one
`vpc_ids` entry for a zone that resolves only inside those VPCs — a
private zone with no VPC, or a public zone carrying `vpc_ids`, is
rejected with 400 rather than silently coerced.




## OpenAPI

````yaml /api-reference/specs/dns.yaml post /v1/zones
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:
    post:
      tags:
        - DNS
      summary: Create zone
      description: |
        Create a new DNS zone. The SOA and NS records are generated
        automatically against the platform nameserver list; the zone is
        queryable immediately on success.

        Defaults to a public zone. Pass `visibility: private` plus at least one
        `vpc_ids` entry for a zone that resolves only inside those VPCs — a
        private zone with no VPC, or a public zone carrying `vpc_ids`, is
        rejected with 400 rather than silently coerced.
      operationId: createZone
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ZoneCreateRequest'
      responses:
        '201':
          description: Zone created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ZoneResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Zone name already in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '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
  schemas:
    ZoneCreateRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Zone FQDN.
          example: example.com
        description:
          type: string
          description: >-
            Free-form note stored with the zone. Not echoed back on the Zone
            object.
          example: Production apex
        visibility:
          type: string
          enum:
            - public
            - private
          default: public
          description: |
            `private` restricts the zone to the VPCs named in `vpc_ids` and
            requires at least one; `public` (the default) rejects `vpc_ids`
            outright rather than ignoring them. Cannot be changed afterwards.
          example: public
        vpc_ids:
          type: array
          items:
            type: string
            format: uuid
          description: >
            VPCs the zone resolves in. Required when visibility=private,
            rejected

            when visibility=public. More can be associated later via

            POST /v1/zones/{zone_id}/vpc-associations.
          example:
            - c3d4e5f6-a7b8-4901-c2d3-e4f5a6b7c8d9
        tags:
          $ref: '#/components/schemas/Tags'
    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
    Tags:
      type: object
      additionalProperties:
        type: string
      example:
        environment: production
        team: backend
    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'
    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:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INVALID_INPUT
              message: Invalid request parameters
              request_id: 550e8400-e29b-41d4-a716-446655440000
    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
    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.

````