{
  "openapi": "3.1.0",
  "info": {
    "title": "DNSMint API",
    "version": "1.0.0",
    "description": "DNSMint turns an IP address into a stable hostname on a domain we operate, served by our authoritative DNS. Register an IP with one call, point your ACME client at the hostname, and update the IP whenever it changes while the name and certificate stay the same.",
    "contact": {
      "email": "hello@dnsmint.com",
      "url": "https://dnsmint.com/quickstart"
    }
  },
  "servers": [
    {
      "url": "https://dnsmint.com/api"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "hostnames",
      "description": "Register, read, update, and release hostnames."
    }
  ],
  "paths": {
    "/v1/hostnames": {
      "post": {
        "operationId": "createHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Register an IP and mint a hostname",
        "description": "Registers an IPv4 or IPv6 address and returns a new hostname. The subdomain is opaque unless you pass one, and lands on a domain assigned to your account. An account holding more than one may choose which. Public, private, and reserved addresses on every plan; the address a hostname is minted with fixes its address class for life. New registrations may briefly return \"pending\"; poll GET until \"live\". Request bodies are capped at 4KB. Plans that include custom subdomains may pass one. Any account may pass an optional dedicated domain, which must be one it already holds.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateHostnameRequest"
              },
              "example": {
                "ip": "34.120.50.10"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Hostname created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostView"
                },
                "example": {
                  "id": "host_01937d3e8f217c4ab8e24f9a1c3d5e6b",
                  "hostname": "q7k4m2.a3f9c1-d4e7b8.dev",
                  "status": "live",
                  "certificate": "self",
                  "created_at": "2026-08-26T08:30:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, missing \"ip\" field, syntactically invalid IP address, invalid subdomain or domain, or body over 4KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Not a valid IP address: 34.120.50",
                  "code": "BAD_REQUEST"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "The body is not valid JSON, is over 4KB, is missing \"ip\", or the address does not parse"
              }
            ]
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Signup is unfinished: no card on file, so no domain has been assigned yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This key does not carry the scope this call needs",
                  "code": "FORBIDDEN"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "PAYMENT_REQUIRED",
                "when": "Signup is unfinished: no card on file, so no domain has been assigned yet"
              }
            ]
          },
          "403": {
            "description": "The API key does not carry the scope this call needs, or its grant does not reach the hostname being minted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This key does not carry the scope this call needs",
                  "code": "FORBIDDEN"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The API key does not carry the scope this call needs, or its grant does not reach the hostname being minted"
              }
            ]
          },
          "409": {
            "description": "Told apart by the error code. CONFLICT: the requested subdomain is taken or retired on this domain. DOMAIN_PROVISIONING: a domain for this team is still being registered; carries Retry-After and a Location pointing at the provisioning status. NO_DOMAIN_AVAILABLE: no domain is assigned and none is being registered, so a timed retry will not help.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Subdomain is taken or retired on this domain",
                  "code": "CONFLICT"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "The requested subdomain is taken or retired on this domain"
              },
              {
                "code": "DOMAIN_PROVISIONING",
                "when": "A domain for this team is still being registered. Carries Retry-After and a Location pointing at the provisioning status"
              },
              {
                "code": "NO_DOMAIN_AVAILABLE",
                "when": "No domain is assigned to this team and none is being registered. Ours to fix; a timed retry will not help"
              }
            ]
          },
          "429": {
            "description": "The account is at its hostname cap, or has made more than 30 registrations in a minute.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Hostname cap reached (5). Release unused names or upgrade.",
                  "code": "RATE_LIMITED"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "The account is at its active-hostname cap, or has made more than 30 registrations in a minute"
              }
            ]
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "501": {
            "description": "A valid request this deployment cannot serve. Returned for certificate: \"managed\" where no certificate encryption key is configured. Retry later; do not change the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "certificate: \"managed\" is unavailable on this deployment: no certificate encryption key is configured. Use \"self\" and run an ACME client against the DNS-01 API; see https://dnsmint.com/quickstart",
                  "code": "NOT_IMPLEMENTED"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_IMPLEMENTED",
                "when": "certificate: \"managed\" was requested and this deployment has no certificate encryption key configured"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "create-hostname",
          "group": "hostnames",
          "displayPath": "POST /api/v1/hostnames",
          "summary": "Registers an IP address and mints a hostname. The subdomain is opaque unless you pass one, and lands on a domain assigned to your account.",
          "params": [
            {
              "name": "ip",
              "where": "body, required unless target is sent",
              "description": "An IPv4 or IPv6 address as a string. The record type follows the address: A for IPv4, AAAA for IPv6. Public, private, and reserved ranges are all accepted, on every plan. The address you mint with fixes the hostname's address class for its lifetime - see Address classes."
            },
            {
              "name": "target",
              "where": "body, required unless ip is sent",
              "description": "A platform endpoint to follow instead of an address, for a platform that gives you a URL and no IP. We resolve it and publish A or AAAA records, then follow it within the target's TTL. Send either ip or target, not both. What it answers fixes the address class, the same as an ip does."
            },
            {
              "name": "subdomain",
              "where": "body, optional",
              "description": "A custom subdomain, on every plan. 3-63 lowercase letters, digits, and hyphens; no periods; no leading or trailing hyphen."
            },
            {
              "name": "domain",
              "where": "body, optional",
              "description": "Which of your dedicated domains to mint on. Must already be assigned to the team. Omitted, we pick one."
            },
            {
              "name": "certificate",
              "where": "body, optional",
              "description": "Who holds the private key, and with it who runs the ACME client and who talks to the CA. `self` (default) means you do all three: run an ACME client against the DNS-01 API, choose your own CA, and we never see key material. `managed` means we do all three and renew before expiry on a fresh key each time, and you pull the current pair from `GET /v1/hostnames/{id}/certificate`. See Certificate modes."
            },
            {
              "name": "ca",
              "where": "body, optional",
              "description": "Which certificate authority issues, for `certificate: \"managed\"` and `certificate: \"csr\"`, the modes where we run the ACME client - `letsencrypt` (default) or `google`. Sending it with `self` is a 400: there you run the client, so the CA is a choice you make in it. Omitted, we pick Let's Encrypt unless it has recently rate-limited orders for your domain. Renewals always return to the CA that issued. See https://dnsmint.com/ca"
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/v1/hostnames \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"ip\": \"34.120.50.10\"}'",
          "responseIntro": "201 with the new hostname:",
          "response": "{\n  \"id\": \"host_01937d3e8f217c4ab8e24f9a1c3d5e6b\",\n  \"hostname\": \"q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"status\": \"pending\",\n  \"certificate\": \"self\",\n  \"created_at\": \"2026-08-26T08:30:00.000Z\"\n}",
          "notes": [
            "The record is served as soon as it is written. The status reads \"pending\" until every nameserver has been observed answering for it, then \"live\"; poll GET until then.",
            "Any account may pass subdomain, and may pass domain, which must be one already assigned to it; domain is only a choice once the team holds more than one.",
            "Every plan has its own cap on how many hostnames an account holds at once; releasing one frees its slot. Registration past the cap returns 429, as does more than 30 registrations a minute."
          ]
        }
      },
      "get": {
        "operationId": "listHostnames",
        "tags": [
          "hostnames"
        ],
        "summary": "List this team's hostnames",
        "description": "Lists the team's hostnames, newest first. Released and terminated hostnames are excluded; suspended ones are shown.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of hostnames to return. Values are clamped to the 1 to 500 range.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "skip",
            "in": "query",
            "required": false,
            "description": "Number of hostnames to skip, for pagination. Minimum 0.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The account's hostnames and list metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostnameList"
                },
                "example": {
                  "hostnames": [
                    {
                      "id": "host_01937d3e8f217c4ab8e24f9a1c3d5e6b",
                      "hostname": "q7k4m2.a3f9c1-d4e7b8.dev",
                      "status": "live",
                      "certificate": "self",
                      "created_at": "2026-08-26T08:30:00.000Z"
                    }
                  ],
                  "total": 1,
                  "active": 1,
                  "cap": 5,
                  "limit": 100,
                  "skip": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-docs": {
          "anchor": "list-hostnames",
          "group": "hostnames",
          "displayPath": "GET /api/v1/hostnames",
          "summary": "Lists the team's hostnames, newest first. Released and terminated hostnames are excluded; suspended ones are shown, so a team can see what has been actioned.",
          "params": [
            {
              "name": "limit",
              "where": "query, optional",
              "description": "Maximum results to return. Default 100; values are clamped to the 1 to 500 range."
            },
            {
              "name": "skip",
              "where": "query, optional",
              "description": "Results to skip, for pagination. Default 0."
            }
          ],
          "curl": "curl \"https://dnsmint.com/api/v1/hostnames?limit=100&skip=0\" \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "200 with a hostnames envelope:",
          "response": "{\n  \"hostnames\": [\n    {\n      \"id\": \"host_01937d3e8f217c4ab8e24f9a1c3d5e6b\",\n      \"hostname\": \"q7k4m2.a3f9c1-d4e7b8.dev\",\n      \"status\": \"live\",\n      \"certificate\": \"self\",\n      \"created_at\": \"2026-08-26T08:30:00.000Z\"\n    }\n  ],\n  \"total\": 1,\n  \"active\": 1,\n  \"cap\": 5,\n  \"limit\": 100,\n  \"skip\": 0\n}",
          "notes": [
            "Released and terminated hostnames never appear.",
            "total is the non-released count. active is live hostnames, which is what the create cap counts. cap is the plan's maxActiveHostnames. limit and skip are the values actually applied after clamping.",
            "Another page exists when skip + limit < total."
          ]
        }
      }
    },
    "/v1/hostnames/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HostnameId"
        }
      ],
      "get": {
        "operationId": "getHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Read one hostname",
        "description": "Returns one hostname. New registrations may briefly return \"pending\"; poll GET until \"live\".",
        "responses": {
          "200": {
            "description": "The hostname.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostView"
                },
                "example": {
                  "id": "host_01937d3e8f217c4ab8e24f9a1c3d5e6b",
                  "hostname": "q7k4m2.a3f9c1-d4e7b8.dev",
                  "status": "live",
                  "certificate": "self",
                  "created_at": "2026-08-26T08:30:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-docs": {
          "anchor": "get-hostname",
          "group": "hostnames",
          "displayPath": "GET /api/v1/hostnames/{id}",
          "summary": "Reads one hostname by id.",
          "params": [
            {
              "name": "id",
              "where": "path, required",
              "description": "The hostname id returned at creation."
            }
          ],
          "curl": "curl https://dnsmint.com/api/v1/hostnames/host_01937d3e8f217c4ab8e24f9a1c3d5e6b \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "200 with the hostname:",
          "response": "{\n  \"id\": \"host_01937d3e8f217c4ab8e24f9a1c3d5e6b\",\n  \"hostname\": \"q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"status\": \"live\",\n  \"certificate\": \"self\",\n  \"created_at\": \"2026-08-26T08:30:00.000Z\"\n}",
          "notes": [
            "New registrations may briefly return \"pending\"; poll GET until \"live\"."
          ]
        }
      },
      "put": {
        "operationId": "updateHostnameIp",
        "tags": [
          "hostnames"
        ],
        "summary": "Set the hostname IP",
        "description": "Puts an IP on the hostname. If the address is unchanged, DNS is not rewritten. If it changed, the record is replaced and the name comes back live. Request bodies are capped at 4KB. A hostname keeps the address class it was minted with. Repointing within a class is free, including between A and AAAA; crossing from public to private or private to public returns 409. This blocks DNS rebinding, where one origin is public first, to deliver a payload under a trusted certificate, and private second, to read a response from inside the visitor's network. Release the hostname and mint a new one for the other class.\n\nSend `target` instead of `ip` to follow a platform endpoint that publishes no address of its own. We resolve it and serve A or AAAA, so the hostname keeps its certificates, its CAA and every child record.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterIpRequest"
              },
              "example": {
                "ip": "34.120.51.22"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated hostname.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostView"
                },
                "example": {
                  "id": "host_01937d3e8f217c4ab8e24f9a1c3d5e6b",
                  "hostname": "q7k4m2.a3f9c1-d4e7b8.dev",
                  "status": "live",
                  "certificate": "self",
                  "created_at": "2026-08-26T08:30:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, missing \"ip\" field, syntactically invalid IP address, or body over 4KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Body must include an \"ip\" string",
                  "code": "BAD_REQUEST"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Invalid JSON, missing \"ip\" field, syntactically invalid IP address, or body over 4KB"
              }
            ]
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The hostname is suspended pending an abuse review, or the API key does not carry the scope this call needs, or its grant does not reach this hostname.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Hostname is suspended pending review",
                  "code": "FORBIDDEN"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The hostname is suspended pending an abuse review, or the API key does not carry the scope this call needs, or its grant does not reach this hostname"
              }
            ]
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Either the hostname was released or terminated, neither of which can be updated, or the new address is a different class from the one the hostname was minted with: a public hostname cannot become private, or the reverse.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This hostname is pinned to public addresses and cannot be repointed to a private one. Release it and mint a new hostname for the private address.",
                  "code": "CONFLICT"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "Either the hostname was released or terminated, neither of which can be updated, or the new address is a different class from the one the hostname was minted with: a public hostname cannot become private, or the reverse"
              }
            ]
          },
          "429": {
            "description": "More than 60 updates in a minute in this team.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 60 updates in a minute in this team"
              }
            ]
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-docs": {
          "anchor": "update-hostname-ip",
          "group": "hostnames",
          "displayPath": "PUT /api/v1/hostnames/{id}",
          "summary": "Sets the hostname's IP. The same address is a no-op; a new address updates the record. The name and its certificates carry over untouched.",
          "params": [
            {
              "name": "id",
              "where": "path, required",
              "description": "The hostname id returned at creation."
            },
            {
              "name": "ip",
              "where": "body, instead of target",
              "description": "The IPv4 or IPv6 address, public or private. Repeating the current address is a no-op; nothing needs to be sent to keep a hostname live. A different address updates the record; the record type follows the address, so a hostname can move between A and AAAA. It cannot move between address classes - a public hostname stays public and a private one stays private, and crossing that line returns 409. See Address classes."
            },
            {
              "name": "target",
              "where": "body, instead of ip",
              "description": "A platform endpoint to follow instead of an address. Send this or `ip`, never both. We resolve it and publish ordinary A and AAAA records, refreshed within the target's own TTL, so the name never carries a CNAME and every record you set on the hostname keeps working. The hostname keeps the address class it was minted with, and that is re-checked every time we refresh rather than only when you set it: a target that starts answering with a private address is refused and the hostname stays on its last public one. Setting `ip` takes the hostname back off its target."
            }
          ],
          "curl": "curl -X PUT https://dnsmint.com/api/v1/hostnames/host_01937d3e8f217c4ab8e24f9a1c3d5e6b \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"ip\": \"34.120.51.22\"}'",
          "responseIntro": "200 with the updated hostname:",
          "response": "{\n  \"id\": \"host_01937d3e8f217c4ab8e24f9a1c3d5e6b\",\n  \"hostname\": \"q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"status\": \"pending\",\n  \"certificate\": \"self\",\n  \"created_at\": \"2026-08-26T08:30:00.000Z\"\n}",
          "notes": [
            "If the address is unchanged, DNS is not rewritten and nothing changes. There is no keepalive to send: a hostname stays live without being touched.",
            "If the address changed, the record is replaced and the hostname comes back live.",
            "A released or terminated hostname cannot be updated; that returns 409. A suspended one returns 403 until the review is resolved.",
            "Repointing across address classes returns 409. A hostname minted on a public address can never be moved to a private one, or the reverse; release it and mint a new hostname instead. See Address classes for why.",
            "More than 60 updates a minute on one team returns 429."
          ]
        }
      },
      "delete": {
        "operationId": "releaseHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Release a hostname",
        "description": "Releases the hostname. The DNS record stops being served and any certificate we hold for it is revoked. The name is free immediately: POST the same subdomain again to take it back, which mints a new hostname with a new id. Releasing an already released hostname returns the same success response.",
        "responses": {
          "200": {
            "description": "The hostname was released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReleaseResult"
                },
                "example": {
                  "released": true
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The hostname is suspended pending an abuse review and cannot be released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The hostname is suspended pending an abuse review and cannot be released"
              }
            ]
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The hostname was terminated for policy violation and cannot be released.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "The hostname was terminated for policy violation and cannot be released"
              }
            ]
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        },
        "x-docs": {
          "anchor": "release-hostname",
          "group": "hostnames",
          "displayPath": "DELETE /api/v1/hostnames/{id}",
          "summary": "Releases the hostname. The record stops being served and any certificate we hold is revoked. The subdomain returns to your account and can be minted again.",
          "params": [
            {
              "name": "id",
              "where": "path, required",
              "description": "The hostname id returned at creation."
            }
          ],
          "curl": "curl -X DELETE https://dnsmint.com/api/v1/hostnames/host_01937d3e8f217c4ab8e24f9a1c3d5e6b \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "200 with a confirmation:",
          "response": "{\n  \"released\": true\n}",
          "notes": [
            "The DNS record stops being served at once. The released hostname stays released: writes to it return 409, and minting the subdomain again creates a new hostname. Releasing an already released hostname returns the same 200.",
            "A suspended or terminated hostname cannot be released. Releasing one would replace an enforcement record with a customer-initiated exit, so it returns 403 or 409 instead."
          ]
        }
      }
    },
    "/v1/hostnames/{id}/acme-credential": {
      "post": {
        "operationId": "createAcmeCredential",
        "summary": "Mint an acme-dns credential scoped to this hostname",
        "description": "Creates a DNS-01 publishing credential for exactly this hostname. At most 5 per hostname. The password is shown only in this response.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Credential created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AcmeRegistration"
                }
              }
            }
          },
          "400": {
            "description": "Credential limit reached (5)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Credential limit reached (5)"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks the dns01:write scope, or is narrowed to another hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks the dns01:write scope, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          },
          "409": {
            "description": "Hostname is released, suspended or terminated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "Hostname is released, suspended or terminated; a pending hostname is served and is not refused"
              }
            ]
          },
          "429": {
            "description": "More than 10 credential mints in a minute in this team.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 10 credential mints in a minute in this team"
              }
            ]
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "INTERNAL_ERROR",
                "when": "Unexpected server error"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "acme-credential",
          "group": "dns01",
          "displayPath": "POST /api/v1/hostnames/:id/acme-credential",
          "summary": "Mints a credential for the hostname (API-key auth, same as other v1 endpoints). The password appears in this response exactly once. At most 5 credentials per hostname, and at most 10 of these calls a minute per account.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname the credential may publish challenges for."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/v1/hostnames/HOST_ID/acme-credential \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "{\n  \"username\": \"2f1e6a9c-8b3d-4e5f-9a1b-6c7d8e9f0a1b\",\n  \"password\": \"f3a9...\",\n  \"fulldomain\": \"_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"subdomain\": \"2f1e6a9c-8b3d-4e5f-9a1b-6c7d8e9f0a1b\",\n  \"server_url\": \"https://dnsmint.com/api/acme\",\n  \"allowfrom\": []\n}",
          "notes": []
        }
      }
    },
    "/v1/hostnames/{id}/records": {
      "get": {
        "operationId": "listHostnameRecords",
        "summary": "List records under a hostname",
        "description": "The TXT, TLSA, CAA, MX, SRV, SSHFP, HTTPS or SVCB records on this hostname. Certificate-challenge records are not listed: they belong to issuance, not to you.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Records",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostRecordList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:read, or is narrowed to another hostname",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:read, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "list-records",
          "group": "records",
          "displayPath": "GET /api/v1/hostnames/:id/records",
          "summary": "The TXT, TLSA, CAA, MX, SRV, SSHFP, HTTPS or SVCB records under a hostname. Certificate-challenge records are not listed: they belong to issuance, not to you.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id returned when it was minted."
            }
          ],
          "curl": "curl https://dnsmint.com/api/v1/hostnames/HOST_ID/records \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "{\n  \"records\": [\n    {\n      \"id\": \"6a1f...\",\n      \"name\": \"_verify.q7k4m2.a3f9c1-d4e7b8.dev\",\n      \"type\": \"TXT\",\n      \"ttl\": 300,\n      \"data\": { \"text\": \"token-from-your-provider\" }\n    }\n  ]\n}",
          "notes": []
        }
      },
      "post": {
        "operationId": "createHostnameRecord",
        "summary": "Add a record under a hostname",
        "description": "Adds one TXT, TLSA, CAA, MX, SRV, SSHFP, HTTPS or SVCB record. Up to 10 records per hostname, on every plan - a ceiling against a hostname becoming a zone, since DNS egress follows hostnames. To change a value, PUT the record's id - one call, and the id survives. Deleting and re-adding costs two writes and leaves a window where the name resolves wrong. TTL is fixed at 300s. A TXT value longer than 255 bytes is split into several character-strings, which is what the wire format requires and what every client concatenates back.\n\nTLSA returns 409 until the domain is signed and its DS is live. A TLSA record says which certificate to trust, and in an unsigned zone anything that can spoof the answer can substitute its own binding - so publishing one would point clients at a certificate nobody vouched for.\n\nCAA replaces the domain's policy for that hostname rather than adding to it. A CA reads the nearest ancestor with a CAA record set and ignores the rest, so publishing one here takes over which CAs may issue for your name - including excluding ours. Delete it and the domain's policy applies again. That is how CAA works and we serve what you write; we do not merge it with ours.\n\nA SRV record's name must be `_<service>._<proto>`, the way a TLSA record's must be `_<port>._<proto>` - RFC 2782 puts them in the owner name and a client only looks there.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRecordRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostRecord"
                }
              }
            }
          },
          "400": {
            "description": "Unusable type, name or rdata",
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Unusable type, name or rdata"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:write, or is narrowed to another hostname",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          },
          "409": {
            "description": "At the 10-record limit, the hostname is frozen, or TLSA on an unsigned zone",
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "At the 10-record limit, the hostname is frozen, or TLSA on an unsigned zone"
              }
            ]
          },
          "429": {
            "description": "Rate limited",
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "Rate limited"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "create-record",
          "group": "records",
          "displayPath": "POST /api/v1/hostnames/:id/records",
          "summary": "TXT, TLSA, CAA, MX, SRV, SSHFP, HTTPS or SVCB records under a hostname you hold. Up to 10 per hostname, on every plan. TTL is 300s and not settable. To change a value, PUT the record's id rather than deleting and re-adding. Omit `name` to put the record on the hostname itself. `_acme-challenge` is reserved - certificate issuance publishes there.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id returned when it was minted."
            },
            {
              "name": "type",
              "where": "body, required",
              "description": "`TXT`, `TLSA`, `CAA`, `MX`, `SRV`, `SSHFP`, `HTTPS` or `SVCB`. A hostname's address is set by PUT on the hostname itself, so `A` and `AAAA` are not accepted here."
            },
            {
              "name": "name",
              "where": "body, optional",
              "description": "Label under the hostname. Omit for a record on the hostname itself. `_acme-challenge` is reserved: certificate issuance publishes there. A TLSA record must be named for its port and protocol, for example `_443._tcp` - RFC 6698 puts them in the owner name, so a client only looks there."
            },
            {
              "name": "text",
              "where": "body, TXT only",
              "description": "A string, or a list of strings to send as-is. Each character-string is at most 255 bytes - octets rather than characters, so a non-ASCII value reaches the limit sooner - and a longer value is split across several, which the client concatenates. 2048 bytes in total. No control characters."
            },
            {
              "name": "usage",
              "where": "body, TLSA only",
              "description": "TLSA certificate usage, 0 to 3. 3 is DANE-EE."
            },
            {
              "name": "selector",
              "where": "body, TLSA only",
              "description": "TLSA selector. 0 full certificate, 1 SubjectPublicKeyInfo."
            },
            {
              "name": "matching_type",
              "where": "body, TLSA only",
              "description": "TLSA matching type. 0 full, 1 SHA-256, 2 SHA-512. The association length must match: 64 hex digits for SHA-256, 128 for SHA-512."
            },
            {
              "name": "association",
              "where": "body, TLSA only",
              "description": "TLSA certificate-association data, lowercase hex, no separators."
            },
            {
              "name": "preference",
              "where": "body, MX only",
              "description": "Lower is preferred. `0` with an exchange of `.` is the RFC 7505 null MX, which says this name accepts no mail."
            },
            {
              "name": "exchange",
              "where": "body, MX only",
              "description": "The mail host. `.` is the root, for the null MX."
            },
            {
              "name": "priority",
              "where": "body, SRV only",
              "description": "Lower is tried first."
            },
            {
              "name": "weight",
              "where": "body, SRV only",
              "description": "Relative share among targets of equal priority."
            },
            {
              "name": "port",
              "where": "body, SRV only",
              "description": "The port the service listens on."
            },
            {
              "name": "target",
              "where": "body, SRV only",
              "description": "The host offering the service. `.` says the service is decidedly not offered at this name."
            },
            {
              "name": "algorithm",
              "where": "body, SSHFP only",
              "description": "`1` RSA, `2` DSA, `3` ECDSA, `4` Ed25519, `6` Ed448."
            },
            {
              "name": "fptype",
              "where": "body, SSHFP only",
              "description": "`1` for SHA-1, `2` for SHA-256. The fingerprint length follows from it."
            },
            {
              "name": "fingerprint",
              "where": "body, SSHFP only",
              "description": "Lowercase hex, no separators. 40 digits for SHA-1, 64 for SHA-256."
            },
            {
              "name": "params",
              "where": "body, HTTPS and SVCB only",
              "description": "RFC 9460 service parameters as fields: `alpn`, `noDefaultAlpn`, `port`, `ipv4hint`, `ipv6hint`, `ech`, `mandatory`. We emit them in the ascending key order the wire format requires."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/v1/hostnames/HOST_ID/records \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"type\":\"TXT\",\"name\":\"_verify\",\"text\":\"token-from-your-provider\"}'",
          "responseIntro": "",
          "response": "{\n  \"id\": \"6a1f...\",\n  \"name\": \"_verify.q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"type\": \"TXT\",\n  \"ttl\": 300,\n  \"data\": { \"text\": \"token-from-your-provider\" }\n}",
          "notes": [
            "A hostname\u2019s address is set by PUT on the hostname, so it is not accepted here. TLSA returns 409 until the domain is signed and its DS is published: an unsigned TLSA record is a certificate binding nobody can verify.",
            "CAA replaces the domain's policy for that hostname rather than adding to it. A CA reads the nearest ancestor with a CAA record set and ignores the rest, so publishing one here takes over which CAs may issue for your name - including excluding ours. Delete it and the domain's policy applies again. That is how CAA works and we serve what you write; we do not merge it with ours."
          ]
        }
      }
    },
    "/v1/hostnames/{id}/records/{record_id}": {
      "put": {
        "operationId": "updateHostnameRecord",
        "summary": "Change a record's value",
        "description": "Replaces the value of one TXT or TLSA record, addressed by its id. The body is the same shape as the POST on the collection.\n\n**Name and type stay as they are.** In DNS the owner name and the type are what a resolver looks a record up by - they are the record's identity, and the value is the part that varies. A body naming a different name or type returns 400: that is a different record, so POST it. Other records sharing this one's name are untouched.\n\nUse this rather than deleting and re-adding. That costs two writes against the rate limit and leaves a window where the answer is wrong - delete-then-add has the name unresolved in between, add-then-delete transiently exceeds the ten-record cap. This is one atomic swap with neither problem, and the id survives, so nothing has to track a new one.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "record_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRecordRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated record",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostRecord"
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or a name or type that differs from the stored record",
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Invalid rdata, or the body names a different name or type than the record holds - that is a different record, so POST it"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:write, is narrowed to another hostname, or the hostname is suspended",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:write, is narrowed to another hostname, or the hostname is suspended"
              }
            ]
          },
          "404": {
            "description": "Hostname or record not found",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No such hostname, or no such record under it - the same answer either way, so a record id from another team cannot be confirmed"
              }
            ]
          },
          "409": {
            "description": "The hostname is not live, or a TLSA record on an unsigned domain",
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "The hostname is not live, or the record is TLSA and the domain is not signed"
              }
            ]
          },
          "429": {
            "description": "Over the record-write limit",
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "Over recordWritePerMinute for this team"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "update-record",
          "group": "records",
          "displayPath": "PUT /api/v1/hostnames/:id/records/:record_id",
          "summary": "Change the value of one record. The id in the path says which; the body says what it should now be. Name and type stay as they are.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id."
            },
            {
              "name": "record_id",
              "where": "path",
              "description": "The record id, from the list or create response. This is what says which record changes; the name does not."
            },
            {
              "name": "type",
              "where": "body, required",
              "description": "`TXT`, `TLSA`, `CAA`, `MX`, `SRV`, `SSHFP`, `HTTPS` or `SVCB`, and it must match the record's current type. A different type is a different record, so POST it."
            },
            {
              "name": "name",
              "where": "body, optional",
              "description": "Label under the hostname, omitted for a record on the hostname itself. Must resolve to the stored record's name; changing it returns 400."
            },
            {
              "name": "text",
              "where": "body, TXT only",
              "description": "One string, at most 255 bytes, no control characters. Bytes rather than characters: a non-ASCII string encodes to more bytes than it has characters."
            },
            {
              "name": "usage",
              "where": "body, TLSA only",
              "description": "TLSA certificate usage, 0 to 3. 3 is DANE-EE."
            },
            {
              "name": "selector",
              "where": "body, TLSA only",
              "description": "TLSA selector. 0 full certificate, 1 SubjectPublicKeyInfo."
            },
            {
              "name": "matching_type",
              "where": "body, TLSA only",
              "description": "TLSA matching type. 0 full, 1 SHA-256, 2 SHA-512. The association length must match: 64 hex digits for SHA-256, 128 for SHA-512."
            },
            {
              "name": "association",
              "where": "body, TLSA only",
              "description": "TLSA certificate-association data, lowercase hex, no separators."
            },
            {
              "name": "preference",
              "where": "body, MX only",
              "description": "Lower is preferred. `0` with an exchange of `.` is the RFC 7505 null MX, which says this name accepts no mail."
            },
            {
              "name": "exchange",
              "where": "body, MX only",
              "description": "The mail host. `.` is the root, for the null MX."
            },
            {
              "name": "priority",
              "where": "body, SRV only",
              "description": "Lower is tried first."
            },
            {
              "name": "weight",
              "where": "body, SRV only",
              "description": "Relative share among targets of equal priority."
            },
            {
              "name": "port",
              "where": "body, SRV only",
              "description": "The port the service listens on."
            },
            {
              "name": "target",
              "where": "body, SRV only",
              "description": "The host offering the service. `.` says the service is decidedly not offered at this name."
            },
            {
              "name": "algorithm",
              "where": "body, SSHFP only",
              "description": "`1` RSA, `2` DSA, `3` ECDSA, `4` Ed25519, `6` Ed448."
            },
            {
              "name": "fptype",
              "where": "body, SSHFP only",
              "description": "`1` for SHA-1, `2` for SHA-256. The fingerprint length follows from it."
            },
            {
              "name": "fingerprint",
              "where": "body, SSHFP only",
              "description": "Lowercase hex, no separators. 40 digits for SHA-1, 64 for SHA-256."
            },
            {
              "name": "params",
              "where": "body, HTTPS and SVCB only",
              "description": "RFC 9460 service parameters as fields: `alpn`, `noDefaultAlpn`, `port`, `ipv4hint`, `ipv6hint`, `ech`, `mandatory`. We emit them in the ascending key order the wire format requires."
            }
          ],
          "curl": "curl -X PUT https://dnsmint.com/api/v1/hostnames/HOST_ID/records/RECORD_ID \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"type\": \"TXT\", \"text\": \"v=spf1 -all\"}'",
          "responseIntro": "200 with the updated record:",
          "response": "{\n  \"id\": \"host_01937d3e8f217c4ab8e24f9a1c3d5e6b\",\n  \"name\": \"q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"type\": \"TXT\",\n  \"ttl\": 300,\n  \"data\": { \"text\": \"v=spf1 -all\" }\n}",
          "notes": [
            "The id survives, so nothing has to track a new one.",
            "Prefer this to deleting and re-adding: that is two writes against the rate limit, and it leaves a window where the name resolves wrong or the ten-record cap is briefly exceeded.",
            "Name and type cannot change. They are what a resolver looks a record up by; a different one is a different record.",
            "Other records sharing this one's name are untouched - the id says which record, not the name.",
            "TTL is 300s and not settable, the same as on create."
          ]
        }
      },
      "delete": {
        "operationId": "deleteHostnameRecord",
        "summary": "Delete a record",
        "description": "404 covers both \"no such record\" and \"not yours\": the ownership filter is the authorisation, and telling the two apart would confirm another team's record id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "record_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:write, or is narrowed to another hostname",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname or record not found",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname or record not found"
              }
            ]
          },
          "409": {
            "description": "Hostname is frozen",
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "Hostname is frozen"
              }
            ]
          },
          "429": {
            "description": "Rate limited",
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "Rate limited"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "delete-record",
          "group": "records",
          "displayPath": "DELETE /api/v1/hostnames/:id/records/:record_id",
          "summary": "204 on success. 404 covers both \u201cno such record\u201d and \u201cnot yours\u201d: the ownership filter is the authorisation, and telling the two apart would confirm another team\u2019s record id.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id."
            },
            {
              "name": "record_id",
              "where": "path",
              "description": "The record id returned when it was created or listed."
            }
          ],
          "curl": "curl -X DELETE https://dnsmint.com/api/v1/hostnames/HOST_ID/records/RECORD_ID \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "",
          "notes": []
        }
      }
    },
    "/v1/hostnames/{id}/certificate": {
      "get": {
        "operationId": "getManagedCertificate",
        "summary": "Fetch the current certificate, with its key when we hold one",
        "description": "For hostnames registered with certificate: \"managed\" or certificate: \"csr\". Returns the current chain and when it expires. `private_key` is present only on managed - on csr the key is yours and we never held it, so the field is absent rather than null.\n\nDelivery is pull: we never connect to your infrastructure, so nothing is ever pushed to you. An expiring certificate is returned rather than withheld - a working agent keeps working through a renewal problem - and only an already-expired one is refused, because serving that would be worse than an error saying why.\n\nA managed response contains a private key and is never cached: `Cache-Control: no-store, private`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current certificate",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagedCertificate"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:read, or is narrowed to another hostname",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:read, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found, or nothing issued for it yet",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found, or nothing issued for it yet"
              }
            ]
          },
          "409": {
            "description": "The hostname is certificate: \"self\", or the certificate has expired and renewal has not completed",
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "The hostname is certificate: \"self\", or the certificate has expired and renewal has not completed"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "managed-certificate",
          "group": "certificates",
          "displayPath": "GET /api/v1/hostnames/:id/certificate",
          "summary": "The current certificate for a hostname we run ACME for. On `certificate: \"managed\"` that includes the private key, because we generated it and keep a copy. On `certificate: \"csr\"` it does not, because you hold it - the chain and its expiry are all we have.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id returned when it was minted."
            }
          ],
          "curl": "curl https://dnsmint.com/api/v1/hostnames/HOST_ID/certificate \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "{\n  \"certificate\": \"-----BEGIN CERTIFICATE-----\\n...\",\n  \"private_key\": \"-----BEGIN PRIVATE KEY-----\\n...\",\n  \"names\": [\"q7k4m2.a3f9c1-d4e7b8.dev\"],\n  \"expires_at\": \"2026-10-10T08:30:00.000Z\",\n  \"mode\": \"managed\",\n  \"ca\": \"letsencrypt\",\n  \"issuer\": \"Let's Encrypt\"\n}",
          "notes": [
            "private_key is present only for managed. Its absence on csr is the accurate statement: we never held that key.",
            "mode echoes the hostname's certificate mode, so a client can tell which shape it is looking at without a second call.",
            "On csr, watch expires_at and POST a fresh request before it passes. Nothing renews it for you."
          ]
        }
      },
      "post": {
        "operationId": "submitCertificateRequest",
        "summary": "Order a certificate from your own CSR",
        "description": "Only for hostnames registered with certificate: \"csr\". You generate the keypair and sign a PKCS#10 request over the hostname; we place the ACME order, answer the DNS-01 challenge in our zone, and store the issued chain for you to fetch. We never see the private key.\n\nFirst issuance and every renewal arrive the same way - there is no difference between them from here. **Renewal is yours to schedule.** We hold no key for this hostname, so we cannot build a replacement request and nothing renews it automatically. Watch `expires_at` on the GET.\n\nThe request must name exactly the hostname you registered, and nothing else. Returns 202: the order is queued, not issued. Poll the GET.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CertificateRequestSubmission"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "The order was accepted and queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CertificateOrderAccepted"
                }
              }
            }
          },
          "400": {
            "description": "The csr is missing, unreadable, or does not name exactly this hostname",
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "The csr is missing, unreadable, unsigned by its own key, or names anything other than this hostname"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:write, or is narrowed to another hostname",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          },
          "409": {
            "description": "The hostname is not certificate: \"csr\", or an order is already in flight",
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "The hostname is certificate: \"self\" or \"managed\", or an order for it is already in progress"
              }
            ]
          },
          "429": {
            "description": "Too many certificate requests for this team",
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More certificate requests than certificateOrderPerMinute allows for this team"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "submit-csr",
          "group": "certificates",
          "displayPath": "POST /api/v1/hostnames/:id/certificate",
          "summary": "For hostnames registered with `certificate: \"csr\"`: you keep the private key, we run the ACME protocol around the request you send. This is how first issuance and every renewal happen - they are the same call.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id returned when it was minted."
            },
            {
              "name": "csr",
              "where": "body, required",
              "description": "A PKCS#10 certificate request, PEM or bare base64. It must carry a subjectAltName naming exactly this hostname - plus its wildcard when you pass `wildcard` - and must be signed by the private half of its own public key. ECDSA, or RSA of at least 2048 bits, with SHA-256, SHA-384 or SHA-512."
            },
            {
              "name": "wildcard",
              "where": "body, optional",
              "description": "Cover `*.hostname` as well as the hostname. Default false. The csr must name both when this is true."
            },
            {
              "name": "ca",
              "where": "body, optional",
              "description": "Which authority issues - `letsencrypt` or `google`. Omitted, we pick. See https://dnsmint.com/ca"
            }
          ],
          "curl": "openssl req -new -newkey ec:<(openssl ecparam -name prime256v1) \\\n    -nodes -keyout key.pem -subj \"/\" \\\n    -addext \"subjectAltName=DNS:$HOSTNAME\" -out req.pem\n\ncurl -X POST https://dnsmint.com/api/v1/hostnames/HOST_ID/certificate \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \"$(jq -n --rawfile csr req.pem '{csr: $csr}')\"",
          "responseIntro": "202 with the queued order:",
          "response": "{\n  \"id\": \"host_01937d3e8f217c4ab8e24f9a1c3d5e6b\",\n  \"state\": \"pending\",\n  \"names\": [\"q7k4m2.a3f9c1-d4e7b8.dev\"],\n  \"ca\": \"letsencrypt\",\n  \"issuer\": \"Let's Encrypt\"\n}",
          "notes": [
            "202, not 201. The order is queued and advanced in the background; poll GET on this path for the chain.",
            "The names come from the hostname you registered, never from the csr. A request naming anything else is a 400 - that check is what stops a certificate being issued for a name you do not hold.",
            "One order at a time per hostname. Two in flight publish four challenge values where only the two newest survive, so the older one would fail validation.",
            "Rate limited per account. Each accepted request spends a certificate against the CA's per-domain cap, so the limit is lower than the other write endpoints.",
            "Nothing renews this for you. Watch expires_at on the GET and POST a fresh request before it passes.",
            "Leave the CSR subject empty. A CN is allowed but must be the hostname; the CAs read the SAN extension and reject a CN that is not in it."
          ]
        }
      }
    },
    "/v1/hostnames/{id}/acme-challenge": {
      "post": {
        "operationId": "publishChallengeDirect",
        "summary": "Publish a DNS-01 challenge value with the API key",
        "description": "Publishes the TXT value at _acme-challenge.HOSTNAME directly, authenticated by the API key rather than by a minted acme-dns credential. Same authorization as minting a credential (dns01:write, narrowed to the hostname if the key is), same two-newest rule and TTL on the record, and the same per-hostname rate budget as /acme/update. This is the path for a client that keeps no state: a credential's password is shown once and capped at five per hostname, so minting one per run would exhaust it.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "txt"
                ],
                "properties": {
                  "txt": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "txt": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, or txt is not a 43-character challenge value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Invalid JSON, or txt is not a 43-character challenge value"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks dns01:write, or is narrowed to another hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks dns01:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          },
          "409": {
            "description": "Hostname is released, suspended or terminated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "Hostname is released, suspended or terminated; a pending hostname is served and is not refused"
              }
            ]
          },
          "429": {
            "description": "More than 20 challenge updates in a minute on this hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 20 challenge updates in a minute on this hostname, across this route, DELETE and /acme/update"
              }
            ]
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "INTERNAL_ERROR",
                "when": "Unexpected server error"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "acme-challenge",
          "group": "dns01",
          "displayPath": "POST /api/v1/hostnames/:id/acme-challenge",
          "summary": "Publish the DNS-01 challenge value directly, with the API key. The acme-dns route above needs a minted credential, whose password is shown once and capped at five per hostname; a client that keeps no state would exhaust that in five renewals. This route takes the same `dns01:write` authorization, narrowed to the hostname if the key is, and writes the same record under the same two-newest rule.",
          "params": [
            {
              "name": "txt",
              "where": "body, required",
              "description": "The DNS-01 challenge value your ACME client computed. Exactly 43 base64url characters."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/v1/hostnames/HOST_ID/acme-challenge \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"txt\": \"CHALLENGE_VALUE_43_CHARS\"}'",
          "responseIntro": "",
          "response": "{\"txt\": \"CHALLENGE_VALUE_43_CHARS\"}",
          "notes": [
            "The hostname must be live. The two newest values for the name are served, which covers the apex plus wildcard double validation.",
            "This route, `DELETE` on it and `/acme/update` share one budget per hostname, so a caller holding both paths cannot double it."
          ]
        }
      },
      "delete": {
        "operationId": "withdrawChallengeDirect",
        "summary": "Withdraw a DNS-01 challenge value",
        "description": "Removes one published challenge value. Withdrawing a value that is already gone is not an error: a client's cleanup runs whether or not its publish completed, so it must be callable unconditionally. Works on a hostname in any status.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "txt"
                ],
                "properties": {
                  "txt": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge withdrawn, or was not present",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "txt": {
                      "type": "string"
                    },
                    "removed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, or txt is not a 43-character challenge value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Invalid JSON, or txt is not a 43-character challenge value"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks dns01:write, or is narrowed to another hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks dns01:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          },
          "429": {
            "description": "More than 20 challenge updates in a minute on this hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 20 challenge updates in a minute on this hostname"
              }
            ]
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "INTERNAL_ERROR",
                "when": "Unexpected server error"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "acme-challenge-delete",
          "group": "dns01",
          "displayPath": "DELETE /api/v1/hostnames/:id/acme-challenge",
          "summary": "Withdraw a challenge value after validation. Removing a value that is already gone returns 200 with `removed: false` rather than an error: a client's cleanup runs whether or not its publish completed, so it has to be callable unconditionally.",
          "params": [
            {
              "name": "txt",
              "where": "body, required",
              "description": "The value to withdraw, exactly as published."
            }
          ],
          "curl": "curl -X DELETE https://dnsmint.com/api/v1/hostnames/HOST_ID/acme-challenge \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"txt\": \"CHALLENGE_VALUE_43_CHARS\"}'",
          "responseIntro": "",
          "response": "{\"txt\": \"CHALLENGE_VALUE_43_CHARS\", \"removed\": true}",
          "notes": []
        }
      }
    },
    "/httpreq/present": {
      "post": {
        "operationId": "httpreqPresent",
        "summary": "lego httpreq provider: publish a challenge",
        "description": "The endpoint lego's built-in httpreq provider posts to. Body {fqdn, value} as lego sends it in its default mode; HTTP basic auth with the API key as the password. The hostname is read from the fqdn. RAW mode ({domain, token, keyAuth}) is refused.",
        "security": [
          {
            "basicAuth": []
          },
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fqdn",
                  "value"
                ],
                "properties": {
                  "fqdn": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "fqdn": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, fqdn is not a challenge name, value is not 43 characters, or RAW mode",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Invalid JSON, fqdn is not _acme-challenge.HOSTNAME, value is not 43 characters, or HTTPREQ_MODE=RAW"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key in the basic-auth password"
              }
            ]
          },
          "403": {
            "description": "Key lacks dns01:write, or is narrowed to another hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks dns01:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "No hostname by that name on this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No hostname by that name on this account"
              }
            ]
          },
          "409": {
            "description": "Hostname is released, suspended or terminated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "Hostname is released, suspended or terminated; a pending hostname is served and is not refused"
              }
            ]
          },
          "429": {
            "description": "More than 20 challenge updates in a minute on this hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 20 challenge updates in a minute on this hostname"
              }
            ]
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "INTERNAL_ERROR",
                "when": "Unexpected server error"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "httpreq-present",
          "group": "dns01",
          "displayPath": "POST /api/httpreq/present",
          "summary": "The endpoint lego's built-in `httpreq` provider posts to, so lego and Traefik issue for a DNSMint hostname with three environment variables and nothing to pre-seed. lego sends `{fqdn, value}` with HTTP basic auth; the password is your API key, and the username can be anything but has to be set, because lego sends basic auth only when both are. Authorization is the key's: `dns01:write`, narrowed to the hostname if the key is. The hostname is read from the fqdn, which for a wildcard order is the bare name, so one hostname covers `*.hostname` too.",
          "params": [
            {
              "name": "fqdn",
              "where": "body, required",
              "description": "The challenge name lego computed: `_acme-challenge.` followed by the hostname, trailing dot optional."
            },
            {
              "name": "value",
              "where": "body, required",
              "description": "The DNS-01 challenge value. Exactly 43 base64url characters."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/httpreq/present \\\n    -u \":$DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"fqdn\": \"_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev.\", \"value\": \"CHALLENGE_VALUE_43_CHARS\"}'",
          "responseIntro": "",
          "response": "{\"fqdn\": \"_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev.\", \"value\": \"CHALLENGE_VALUE_43_CHARS\"}",
          "notes": [
            "RAW mode (`HTTPREQ_MODE=RAW`, body `{domain, token, keyAuth}`) is refused with a 400 naming the fix. The default mode carries the finished value, which is all a DNS server needs.",
            "Shares the per-hostname budget with `/acme/update` and the direct route above."
          ],
          "setup": {
            "intro": "lego and Traefik: point the provider at this endpoint, set any username, and give it the key as the password. Nothing else - no credential, no storage file. lego sends basic auth only when both username and password are set, so the username is required even though we ignore its value.",
            "code": "HTTPREQ_ENDPOINT=https://dnsmint.com/api/httpreq\nHTTPREQ_USERNAME=dnsmint\nHTTPREQ_PASSWORD=<your DNSMint API key>\n\n# lego\nlego --email you@example.com --dns httpreq \\\n    -d q7k4m2.a3f9c1-d4e7b8.dev -d '*.q7k4m2.a3f9c1-d4e7b8.dev' run\n\n# Traefik (static configuration), same two variables in the environment\ncertificatesResolvers:\n  dnsmint:\n    acme:\n      email: you@example.com\n      storage: /letsencrypt/acme.json\n      dnsChallenge:\n        provider: httpreq"
          }
        }
      }
    },
    "/httpreq/cleanup": {
      "post": {
        "operationId": "httpreqCleanup",
        "summary": "lego httpreq provider: withdraw a challenge",
        "description": "The cleanup half of lego's httpreq provider. Same body and authentication as /httpreq/present. Withdrawing a value that is already gone is not an error, and a hostname in any status is accepted.",
        "security": [
          {
            "basicAuth": []
          },
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fqdn",
                  "value"
                ],
                "properties": {
                  "fqdn": {
                    "type": "string"
                  },
                  "value": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge withdrawn, or was not present",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "fqdn": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, fqdn is not a challenge name, value is not 43 characters, or RAW mode",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Invalid JSON, fqdn is not _acme-challenge.HOSTNAME, value is not 43 characters, or HTTPREQ_MODE=RAW"
              }
            ]
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key in the basic-auth password"
              }
            ]
          },
          "403": {
            "description": "Key lacks dns01:write, or is narrowed to another hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks dns01:write, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "No hostname by that name on this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No hostname by that name on this account"
              }
            ]
          },
          "429": {
            "description": "More than 20 challenge updates in a minute on this hostname",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 20 challenge updates in a minute on this hostname"
              }
            ]
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "INTERNAL_ERROR",
                "when": "Unexpected server error"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "httpreq-cleanup",
          "group": "dns01",
          "displayPath": "POST /api/httpreq/cleanup",
          "summary": "The cleanup half of lego's `httpreq` provider: same body and authentication as `/present`. Withdrawing a value that is already gone returns 200, because lego calls cleanup whether or not present completed.",
          "params": [
            {
              "name": "fqdn",
              "where": "body, required",
              "description": "The challenge name, as sent to `/present`."
            },
            {
              "name": "value",
              "where": "body, required",
              "description": "The value to withdraw, exactly as published."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/httpreq/cleanup \\\n    -u \":$DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"fqdn\": \"_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev.\", \"value\": \"CHALLENGE_VALUE_43_CHARS\"}'",
          "responseIntro": "",
          "response": "{\"fqdn\": \"_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev.\", \"value\": \"CHALLENGE_VALUE_43_CHARS\"}",
          "notes": []
        }
      }
    },
    "/acme/update": {
      "post": {
        "operationId": "publishAcmeChallenge",
        "summary": "Publish a DNS-01 challenge (acme-dns wire protocol)",
        "description": "Authenticated by X-Api-User/X-Api-Key headers from the registration blob. subdomain must equal the credential's username; txt is the 43-character challenge value. The two newest values per name are served. Credentials for released hostnames are rejected.",
        "security": [],
        "parameters": [
          {
            "name": "X-Api-User",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "X-Api-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "subdomain",
                  "txt"
                ],
                "properties": {
                  "subdomain": {
                    "type": "string"
                  },
                  "txt": {
                    "type": "string",
                    "pattern": "^[A-Za-z0-9_-]{43}$"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge published",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "txt": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed challenge value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Malformed challenge value"
              }
            ]
          },
          "401": {
            "description": "Invalid acme-dns credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Invalid acme-dns credentials"
              }
            ]
          },
          "403": {
            "description": "Subdomain does not belong to this credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Subdomain does not belong to this credential"
              }
            ]
          },
          "429": {
            "description": "More than 20 challenge updates in a minute on this credential.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "More than 20 challenge updates in a minute on this credential"
              }
            ]
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "INTERNAL_ERROR",
                "when": "Unexpected server error"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "acme-update",
          "group": "dns01",
          "displayPath": "POST /api/acme/update",
          "summary": "The acme-dns wire protocol. Authentication is the credential, sent as `X-Api-User` and `X-Api-Key` headers. `subdomain` must be the credential\u2019s username; `txt` is the 43-character challenge value. The two newest values per hostname are served, which covers Let\u2019s Encrypt\u2019s apex plus wildcard double validation. A credential only works while its hostname is live, so a released, suspended, or terminated hostname stops accepting challenges. Returns 429 past 20 updates a minute per credential.",
          "params": [
            {
              "name": "subdomain",
              "where": "body, required",
              "description": "The `subdomain` from the registration blob, which is also the `X-Api-User`. It must match the credential that authenticated: a credential publishes for one hostname and nothing else."
            },
            {
              "name": "txt",
              "where": "body, required",
              "description": "The DNS-01 challenge value your ACME client computed. Exactly 43 base64url characters, which is what a SHA-256 key authorization digest is."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/acme/update \\\n    -H \"X-Api-User: 2f1e6a9c-8b3d-4e5f-9a1b-6c7d8e9f0a1b\" \\\n    -H \"X-Api-Key: f3a9...\" \\\n    -d '{\"subdomain\": \"2f1e6a9c-8b3d-4e5f-9a1b-6c7d8e9f0a1b\", \"txt\": \"CHALLENGE_VALUE_43_CHARS\"}'",
          "responseIntro": "",
          "response": "{\"txt\": \"CHALLENGE_VALUE_43_CHARS\"}",
          "notes": [
            "This is the only acme-dns endpoint we implement. There is no `/register`: it is unauthenticated and carries no hostname, so there would be nothing to scope a credential to. Credentials come from the endpoint above instead. Clients that auto-register when their storage has no entry for a domain - lego, and Traefik, which embeds it - need that storage pre-seeded with the JSON from `acme-credential`, keyed by hostname. certbot and cert-manager read a pre-provisioned file already and need nothing extra."
          ],
          "setup": {
            "intro": "Pre-seeding lego or Traefik: point both at our base URL and at a storage file, and write the response from `POST /v1/hostnames/{id}/acme-credential` into that file under the hostname before the first run. lego then finds the entry and skips `/register`. The key is the hostname without any wildcard prefix; one entry covers the hostname and `*.hostname`.",
            "code": "ACME_DNS_API_BASE=https://dnsmint.com/api/acme\nACME_DNS_STORAGE_PATH=/etc/lego/acme-dns.json\n\n# /etc/lego/acme-dns.json\n{\n  \"q7k4m2.a3f9c1-d4e7b8.dev\": {\n    \"username\": \"2f1e6a9c-8b3d-4e5f-9a1b-6c7d8e9f0a1b\",\n    \"password\": \"f3a9...\",\n    \"fulldomain\": \"_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev\",\n    \"subdomain\": \"2f1e6a9c-8b3d-4e5f-9a1b-6c7d8e9f0a1b\",\n    \"server_url\": \"https://dnsmint.com/api/acme\"\n  }\n}"
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "summary": "List API keys",
        "description": "Every live key on the team, metadata only. Secrets are never returned here; a secret exists once, in the response that created it. Requires the keys:write scope.",
        "operationId": "listKeys",
        "tags": [
          "Keys"
        ],
        "responses": {
          "200": {
            "description": "The account's keys",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyList"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or expired key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "No Bearer header, or a key that does not match a live credential."
              },
              {
                "code": "KEY_EXPIRED",
                "when": "The key was real and its lifetime has passed. Retrying will not help; mint a new key."
              }
            ]
          },
          "403": {
            "description": "Key does not carry keys:write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `keys:write`. It is team-wide and must be named explicitly when the key is created."
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "list-keys",
          "group": "keys",
          "displayPath": "GET /api/v1/keys",
          "summary": "Lists every live key on the team, metadata only. Secrets are never returned here - a secret exists once, in the response that created it, and is not recoverable afterwards.",
          "params": [],
          "curl": "curl https://dnsmint.com/api/v1/keys \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "Each entry says what the key reaches, when it was last used, when it expires, and which key created it.",
          "response": "{\n  \"keys\": [\n    {\n      \"key_id\": \"4e378b700a9e\",\n      \"name\": \"ci\",\n      \"scopes\": [\n        {\n          \"scope\": \"dns01:write\",\n          \"on\": \"q7k4m2.example.dev\"\n        }\n      ],\n      \"created_at\": \"2026-09-05T10:00:00.000Z\",\n      \"last_used_at\": \"2026-09-05T11:02:00.000Z\",\n      \"expires_at\": \"2026-12-04T10:00:00.000Z\",\n      \"created_by_key_id\": null,\n      \"expired\": false\n    }\n  ]\n}",
          "notes": [
            "created_by_key_id is null when a person created the key in the dashboard, and names the minting key when a program did. That is what makes a leaked keys:write key containable: you can enumerate what it created rather than revoking everything."
          ]
        }
      },
      "post": {
        "summary": "Create an API key",
        "description": "Mint a key. The secret is returned once, in this response, and cannot be retrieved afterwards. Requires the keys:write scope. A key can only grant within its own reach: a key narrowed to one domain cannot create a team-wide key, or reach a domain it does not hold. keys:write cannot be combined with dns01:write on one key. A key carrying hostnames:write may grant dns01:write on a hostname it reaches without holding it, which is how a per-machine renewal credential is minted; that grant must name a hostname, so no domain-wide or team-wide DNS-01 key can be created through the API.",
        "operationId": "createKey",
        "tags": [
          "Keys"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateKeyRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Contains the only copy of the secret.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreatedApiKey"
                }
              }
            }
          },
          "400": {
            "description": "Invalid scopes, an escalation attempt, or the key cap reached",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "Unknown scope, a `keys:write` grant narrowed to a domain, `keys:write` paired with `dns01:write`, a grant exceeding the creating key's own reach, an `expires_in_days` outside 1-3650, a name the team does not own, or the key cap reached."
              }
            ]
          },
          "401": {
            "description": "Missing, invalid or expired key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "No Bearer header, or a key that does not match a live credential."
              },
              {
                "code": "KEY_EXPIRED",
                "when": "The key was real and its lifetime has passed. Retrying will not help; mint a new key."
              }
            ]
          },
          "403": {
            "description": "Key does not carry keys:write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `keys:write`. It is team-wide and must be named explicitly when the key is created."
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "create-key",
          "group": "keys",
          "displayPath": "POST /api/v1/keys",
          "summary": "Mints a key and returns the secret once. This is the only response that ever contains it. Requires the keys:write scope, which is team-wide and is never granted to a key created without naming scopes.",
          "params": [
            {
              "name": "name",
              "where": "body, optional",
              "description": "A label, up to 60 characters. Defaults to \"api\"."
            },
            {
              "name": "scopes",
              "where": "body, optional",
              "description": "Scope strings for the whole team, or objects narrowing one to a domain or hostname. Omitted, the key gets hostnames:read, hostnames:write and dns01:write on the whole team - never keys:write, which must be named."
            },
            {
              "name": "expires_in_days",
              "where": "body, optional",
              "description": "1 to 3650. Omitted or null for a key that does not expire."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/v1/keys \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"name\": \"worker-7\", \"scopes\": [{\"scope\": \"dns01:write\", \"hostname\": \"q7k4m2.example.dev\"}], \"expires_in_days\": 30}'",
          "responseIntro": "201, and the only copy of the secret.",
          "response": "{\n  \"key\": \"dnsm_9b21c5f04a7e_1f3d\\u2026\",\n  \"key_id\": \"9b21c5f04a7e\",\n  \"name\": \"worker-7\",\n  \"scopes\": [\n    {\n      \"scope\": \"dns01:write\",\n      \"on\": \"q7k4m2.example.dev\"\n    }\n  ],\n  \"created_at\": \"2026-09-05T12:00:00.000Z\",\n  \"last_used_at\": null,\n  \"expires_at\": \"2026-10-05T12:00:00.000Z\",\n  \"created_by_key_id\": \"4e378b700a9e\",\n  \"expired\": false\n}",
          "notes": [
            "A key can only grant within its own reach. A key narrowed to one domain cannot create a team-wide key, or reach a domain it does not hold; either is refused with 400. Without that, every narrowing on the team would be advisory. A key carrying hostnames:write may grant dns01:write on a hostname it reaches without holding it; that grant must name a hostname, never a domain or the team.",
            "keys:write cannot be combined with dns01:write on one key. DNS-01 belongs on an exposed web server, and a key that can also mint credentials would turn a compromise there into a permanent one. Use two keys.",
            "A key may mint a successor that also carries keys:write. That is rotation, not escalation - mint the replacement, then revoke the outgoing key."
          ]
        }
      }
    },
    "/v1/keys/{key_id}": {
      "put": {
        "summary": "Rename an API key",
        "description": "Changes the label only. A key's scopes are fixed for its life: the secret is bound to the grants it was minted with, so widening one in place would give new reach to every holder of a credential issued narrow. To change what a key may do, mint its replacement and revoke it. A key may rename itself.",
        "operationId": "renameKey",
        "tags": [
          "Keys"
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key id, as it appears in dnsm_<key_id>_<secret>."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 60,
                    "description": "The new label. Trimmed, and truncated to 60 characters. Must not be blank."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renamed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key_id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A missing or blank name, or a body that is not JSON",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "`name` absent, not a string, or blank after trimming. A rename with nothing in it is refused rather than falling back to a default, because the default would be a name nobody asked for."
              }
            ]
          },
          "401": {
            "description": "Missing, invalid or expired key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "No Bearer header, or a key that does not match a live credential."
              },
              {
                "code": "KEY_EXPIRED",
                "when": "The key was real and its lifetime has passed. Retrying will not help; mint a new key."
              }
            ]
          },
          "403": {
            "description": "Key does not carry keys:write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `keys:write`. It is team-wide and must be named explicitly when the key is created."
              }
            ]
          },
          "404": {
            "description": "No live key with that id in this team",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No live key with that id in this team. Already revoked, never existed, and belonging to another team all answer the same way."
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "rename-key",
          "group": "keys",
          "displayPath": "PUT /api/v1/keys/{key_id}",
          "summary": "Renames a key. The label changes and nothing else does.",
          "params": [
            {
              "name": "key_id",
              "where": "path, required",
              "description": "The key id: the middle segment of the key string, between the `dnsm_` prefix and the secret. Returned by every listing."
            },
            {
              "name": "name",
              "where": "body, required",
              "description": "The new label. Trimmed, and truncated to 60 characters, the same rule that applies when a key is created. Blank is refused."
            }
          ],
          "curl": "curl -X PUT https://dnsmint.com/api/v1/keys/9b21c5f04a7e \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"name\": \"ci-deploy\"}'",
          "responseIntro": "200 with the key id and its new name.",
          "response": "{\n  \"key_id\": \"9b21c5f04a7e\",\n  \"name\": \"ci-deploy\"\n}",
          "notes": [
            "Scopes cannot be changed, here or anywhere. A key's secret is bound to the grants it was minted with, so widening one in place would give new reach to everything already holding a credential that was issued narrow, with nothing re-issued and no event to point at. To change what a key may do, mint its replacement and revoke it.",
            "A key may rename itself. Renaming is weaker than revoking, which a caller holding `keys:write` can already do.",
            "A blank name is refused rather than replaced with a default, because the default would be a name nobody asked for.",
            "A key id that does not exist, is already revoked, or belongs to another team all return the same 404, so key ids on other accounts cannot be probed."
          ]
        }
      },
      "delete": {
        "summary": "Revoke an API key",
        "description": "Revocation takes effect on the key's next request. A key may revoke itself, which is what a program does when it detects its own compromise. Revoking a key does not revoke keys it created: rotation is mint-then-revoke, so a cascade would destroy the replacement.",
        "operationId": "revokeKey",
        "tags": [
          "Keys"
        ],
        "parameters": [
          {
            "name": "key_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key id, as it appears in dnsm_<key_id>_<secret>."
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key_id": {
                      "type": "string"
                    },
                    "revoked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid or expired key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "No Bearer header, or a key that does not match a live credential."
              },
              {
                "code": "KEY_EXPIRED",
                "when": "The key was real and its lifetime has passed. Retrying will not help; mint a new key."
              }
            ]
          },
          "403": {
            "description": "Key does not carry keys:write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `keys:write`. It is team-wide and must be named explicitly when the key is created."
              }
            ]
          },
          "404": {
            "description": "No live key with that id in this team",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "No live key with that id in this team. Already revoked, never existed, and belonging to another team all answer the same way."
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "revoke-key",
          "group": "keys",
          "displayPath": "DELETE /api/v1/keys/{key_id}",
          "summary": "Revokes a key. It stops authenticating on its next request, with no propagation delay and no cached window to wait out.",
          "params": [
            {
              "name": "key_id",
              "where": "path, required",
              "description": "The key id: the middle segment of the key string, between the `dnsm_` prefix and the secret. Returned by every listing."
            }
          ],
          "curl": "curl -X DELETE https://dnsmint.com/api/v1/keys/9b21c5f04a7e \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "200 once it is revoked.",
          "response": "{\n  \"key_id\": \"9b21c5f04a7e\",\n  \"revoked\": true\n}",
          "notes": [
            "A key may revoke itself. That is what a program does when it detects its own compromise, and refusing it would mean the only response available to an automated caller is to wake a person.",
            "Revoking a key does not revoke keys it created. Rotation is mint-then-revoke, so a cascade would destroy the replacement at the moment rotation completed.",
            "A key id that does not exist, is already revoked, or belongs to another team all return the same 404, so key ids on other accounts cannot be probed."
          ]
        }
      }
    },
    "/v1/hostnames/{id}/diagnose": {
      "get": {
        "operationId": "diagnoseHostname",
        "summary": "Why a hostname is not working",
        "description": "Runs the checks we can run from the authoritative side and returns a verdict for each, with the next action on anything that failed.\n\nRead-only. Nothing here writes a record, re-signs a zone or orders a certificate, so it is safe to call on a schedule and safe to retry.\n\nAlways 200 when the hostname exists, including when every check fails - that is the case this endpoint is for, so the verdict is in the body rather than the status code. Each check carries `verdict`: `pass`, `fail`, `warn`, `unknown` (we could not find out, which is never the same as fine) or `skipped` (an earlier answer made it meaningless). `summary` names the first failure, because it is usually the cause of the ones after it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The diagnosis",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Diagnosis"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "Missing or invalid API key"
              }
            ]
          },
          "403": {
            "description": "Key lacks hostnames:read, or is narrowed to another hostname",
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "Key lacks hostnames:read, or is narrowed to another hostname"
              }
            ]
          },
          "404": {
            "description": "Hostname not found",
            "x-error-codes": [
              {
                "code": "NOT_FOUND",
                "when": "Hostname not found"
              }
            ]
          },
          "429": {
            "description": "Too many diagnoses for this account",
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "Each call queries both nameservers, two public resolvers and a certificate transparency log"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "diagnose-hostname",
          "group": "hostnames",
          "displayPath": "GET /api/v1/hostnames/:id/diagnose",
          "summary": "Answers \"why is this not working\" from the authoritative side. We are the nameserver for this name, so this reports things an external checker can only infer: whether the record is live on every node of ours, whether the zone validates from the root, whether a DNS-01 challenge was ever written and when, and whether a CAA record on your hostname is quietly refusing the CA you are trying to use.",
          "params": [
            {
              "name": "id",
              "where": "path",
              "description": "The hostname id returned when it was minted."
            }
          ],
          "curl": "curl https://dnsmint.com/api/v1/hostnames/HOST_ID/diagnose \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "{\n  \"hostname\": \"q7k4m2.a3f9c1-d4e7b8.dev\",\n  \"summary\": \"q7k4m2.a3f9c1-d4e7b8.dev is not working: no unexpired certificate covering it in crt.sh\",\n  \"checks\": [\n    { \"id\": \"record\", \"label\": \"record\", \"verdict\": \"pass\", \"detail\": \"A 203.0.113.10, live, TTL 60\" },\n    { \"id\": \"nameservers\", \"label\": \"nameservers\", \"verdict\": \"pass\", \"detail\": \"ns1 12ms, ns2 31ms\" },\n    { \"id\": \"dnssec\", \"label\": \"DNSSEC\", \"verdict\": \"pass\", \"detail\": \"chain validates from the root (cloudflare set AD)\" },\n    {\n      \"id\": \"certificate\",\n      \"label\": \"certificate\",\n      \"verdict\": \"fail\",\n      \"detail\": \"no unexpired certificate covering q7k4m2.a3f9c1-d4e7b8.dev in crt.sh\",\n      \"action\": \"Your ACME client has not completed a challenge. The DNS-01 line above says whether one was ever written.\"\n    }\n  ],\n  \"checked_at\": \"2026-09-05T09:14:02.000Z\"\n}",
          "notes": [
            "unknown is not pass. A certificate transparency log that did not answer, or a resolver we could not reach, reports unknown - reading that as healthy is how a real fault gets closed as fine.",
            "action is present on everything except pass, and absent when we cannot honestly name a next step.",
            "A hostname that is released, suspended or terminated returns that as the first check and skips the rest, because every one of them would fail for the same reason.",
            "The checks run cheapest first and share a time budget. One that ran out of time comes back unknown rather than delaying the response."
          ]
        }
      }
    },
    "/v1/billing/checkout": {
      "post": {
        "summary": "Start a checkout",
        "description": "Create a hosted checkout the account owner completes, so a caller that has hit its cap has somewhere to send them. Returns a URL and charges nothing. Requires the billing:write scope. Only for an account with no plan yet: changing a plan that already exists charges the card on file, so it answers 409 with the page where a person does that.",
        "operationId": "startCheckout",
        "tags": [
          "Billing"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A checkout to open",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Checkout"
                }
              }
            }
          },
          "400": {
            "description": "The plan asked for is out of bounds",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "BAD_REQUEST",
                "when": "A count that is not a whole number, or above what a plan can hold."
              }
            ]
          },
          "401": {
            "description": "Missing, invalid or expired key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "UNAUTHORIZED",
                "when": "No Bearer header, or a key that does not match a live credential."
              },
              {
                "code": "KEY_EXPIRED",
                "when": "The key was real and its lifetime has passed. Retrying will not help; mint a new key."
              }
            ]
          },
          "403": {
            "description": "Key does not carry billing:write",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `billing:write`. It is team-wide and must be named explicitly when the key is created."
              }
            ]
          },
          "409": {
            "description": "The account already has a plan, or the plan asked for is smaller than what it holds",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutConflict"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "CONFLICT",
                "when": "A plan is already running - the response carries `manage_url` for a person to finish at - or the selection is below what the account currently uses."
              }
            ]
          },
          "429": {
            "description": "Too many checkouts started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "RATE_LIMITED",
                "when": "Nothing changes between attempts until somebody pays, so retrying on a timer achieves nothing."
              }
            ]
          },
          "503": {
            "description": "Billing is unavailable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "SERVICE_UNAVAILABLE",
                "when": "Billing is not configured on this deployment, or the payment provider did not answer."
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "start-checkout",
          "group": "billing",
          "displayPath": "POST /api/v1/billing/checkout",
          "summary": "Returns a link the account owner opens to raise the account's limits. Call it when a mint is refused because the account is at its hostname cap or has no plan - those responses tell a caller to do something about it, and this is where it can be done.",
          "params": [
            {
              "name": "hostnames",
              "type": "integer",
              "required": false,
              "description": "Hostnames the plan should include. The cap you want, not the extra. Defaults to what the account already holds."
            },
            {
              "name": "domains",
              "type": "integer",
              "required": false,
              "description": "Dedicated domains the plan should include. Defaults to what the account already holds, so asking for hostnames alone keeps the domains."
            }
          ],
          "curl": "curl -X POST https://dnsmint.com/api/v1/billing/checkout \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\" \\\n    -H \"Content-Type: application/json\" \\\n    -d '{\"hostnames\": 250}'",
          "responseIntro": "Give the URL to the account owner. Nothing changes until they complete it.",
          "response": "{\n  \"checkout_url\": \"https://polar.sh/checkout/co_9f2a\",\n  \"amount_cents\": 4900,\n  \"selection\": {\n    \"domains\": {\n      \"starter\": 2\n    },\n    \"hostnames\": 250\n  }\n}",
          "notes": [
            "It charges nothing. The purchase happens only if a person opens the URL and enters card details, which is the whole reason a machine may ask for one.",
            "billing:write is team-wide, is never granted to a key created without naming scopes, and cannot be narrowed to a domain or a hostname - a subscription belongs to the account.",
            "An account that already has a plan gets 409 with manage_url instead. Changing a running plan charges the card on file, so it is a person's decision, and the link carries the numbers asked for so the plan panel opens on them.",
            "A plan smaller than what the account currently uses is refused: nothing takes the excess away when a smaller plan lands."
          ]
        }
      },
      "get": {
        "operationId": "getBillingStatus",
        "summary": "What the plan allows, and whether a checkout is outstanding",
        "description": "The read half of the checkout. Reports and never acts, so a caller can wait on it instead of retrying a mint to find out whether a payment link was completed.",
        "tags": [
          "Billing"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The account's plan, its usage, and any outstanding checkout.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BillingStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not carry `hostnames:read`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This API key is missing the \"hostnames:read\" scope",
                  "code": "FORBIDDEN"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `hostnames:read`"
              }
            ]
          }
        },
        "x-docs": {
          "anchor": "billing-status",
          "group": "billing",
          "displayPath": "GET /api/v1/billing/checkout",
          "summary": "The read half of the checkout. `POST` hands back a URL and says a person completes it, and tells the caller not to ask again hoping the limit lifts - which left nothing to ask instead. This says what the plan allows, how much is used, and whether a checkout is still outstanding.",
          "params": [],
          "curl": "curl https://dnsmint.com/api/v1/billing/checkout \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "{\n  \"plan\": {\n    \"active\": false,\n    \"name\": \"Starter\"\n  },\n  \"hostnames\": {\n    \"active\": 5,\n    \"cap\": 5\n  },\n  \"domains\": {\n    \"held\": 1,\n    \"cap\": 1\n  },\n  \"pending_checkout\": {\n    \"started_at\": \"2026-09-08T04:12:00.000Z\"\n  }\n}",
          "notes": [
            "It reports and never acts. Reading it starts no purchase, so a caller waiting on a payment link can poll it instead of retrying a mint.",
            "`pending_checkout` appears only while no plan is active. Once one exists the question is answered by `plan`, and a row from a checkout somebody abandoned is not news.",
            "Scoped `hostnames:read`, not `billing:write`. What the plan allows is the same authority as reading the hostnames counted against it, and a read-only key should not see the cap it is about to hit without being told why.",
            "The MCP tool is `get_plan`."
          ]
        }
      }
    },
    "/v1/provisioning": {
      "get": {
        "summary": "Is a domain being registered for this account?",
        "description": "The other half of the 409 `DOMAIN_PROVISIONING` that `POST /v1/hostnames` returns while a domain is still being bought. That response says to retry shortly; this says when retrying will work, so a caller does not have to poll the write endpoint to find out. Reports only: reading it buys, claims and promotes nothing, so a polling loop cannot become a spending loop.",
        "operationId": "getProvisioning",
        "tags": [
          "Provisioning"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "x-docs": {
          "anchor": "provisioning-status",
          "group": "hostnames",
          "displayPath": "GET /api/v1/provisioning",
          "summary": "The other half of the 409 `DOMAIN_PROVISIONING` that `POST /api/v1/hostnames` returns while a domain is still being bought for this account. That response says to retry shortly; this says when retrying will work, so nothing has to poll the write endpoint to find out.",
          "params": [],
          "curl": "curl https://dnsmint.com/api/v1/provisioning \\\n    -H \"Authorization: Bearer $DNSMINT_KEY\"",
          "responseIntro": "",
          "response": "{\n  \"state\": \"provisioning\",\n  \"domain\": \"a3f9c1-d4e7b8.dev\",\n  \"since\": \"2026-09-08T04:12:00.000Z\",\n  \"delegated\": false,\n  \"retry_after\": 30\n}",
          "errors": [
            {
              "status": 401,
              "code": "UNAUTHORIZED",
              "when": "No API key, or one that is unknown, revoked or expired"
            },
            {
              "status": 403,
              "code": "FORBIDDEN",
              "when": "The key does not carry `hostnames:read`"
            }
          ],
          "notes": [
            "It reports and never acts. Reading it buys, claims and promotes nothing, so a polling loop cannot turn into a spending loop.",
            "`delegated` is the gate that decides readiness. `state` is our own bookkeeping, and only delegation proves the parent zone points at our nameservers.",
            "`retry_after` also arrives as a `Retry-After` header. It is absent rather than zero when there is nothing left to wait for, because `Retry-After: 0` reads as retry immediately.",
            "Scoped `hostnames:read` rather than a scope of its own: whether you can mint yet is the same authority as reading the hostnames a key can see."
          ]
        },
        "responses": {
          "200": {
            "description": "The state of the newest domain bought for this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProvisioningStatus"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key does not carry `hostnames:read`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "This API key is missing the \"hostnames:read\" scope",
                  "code": "FORBIDDEN"
                }
              }
            },
            "x-error-codes": [
              {
                "code": "FORBIDDEN",
                "when": "The key does not carry `hostnames:read`"
              }
            ]
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dnsm_<key_id>_<secret>",
        "description": "API key sent as Authorization: Bearer dnsm_<key_id>_<secret>. Keys are created in the DNSMint dashboard; the secret is shown once at creation. A key carries scopes: hostnames:read to list and read, hostnames:write to mint, repoint and release, and dns01:write to mint acme-dns credentials. Each scope also carries what it reaches, chosen when the key is created: the whole team, one domain, or one hostname. A key missing the scope an endpoint requires gets 403 FORBIDDEN, and so does one that carries the scope but is narrowed away from the hostname in the request. A key created without naming any scope gets all three on the whole team. Scope to a domain when the caller works across a namespace: a key limited to example.dev reaches every hostname under it, including ones minted later, and nothing else on the team. Scope to a hostname when the caller is one machine: give the box that renews a certificate dns01:write on its own hostname and nothing else, and it cannot mint a credential for anything else you run, repoint an IP, or release a name. A key may also carry an expiry, chosen when it is created. An expired key stops authenticating and returns 401 with code KEY_EXPIRED, naming the date it lapsed, so it is distinguishable from a malformed or revoked credential: retrying will not help and the fix is a new key. Keys created without an expiry do not expire. A fourth scope, keys:write, lets a key create and revoke keys through /v1/keys so credentials can be rotated without a person. It is team-wide only, is never granted to a key created without naming scopes, and cannot be combined with dns01:write on the same key. A key can only grant within its own reach: one narrowed to a domain cannot create a team-wide key. A key carrying hostnames:write may grant dns01:write on a hostname it reaches without holding it, and that grant must name a hostname. A fifth scope, billing:write, lets a key ask for a checkout link through /v1/billing/checkout when the account is at its cap. It is team-wide only for the same reason keys:write is - a subscription belongs to the account - and is never granted to a key created without naming scopes. It buys nothing: the link is completed by a person."
      },
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "Accepted only by the /httpreq endpoints, because lego's httpreq provider sends HTTP basic auth. The password is the API key; the username is ignored. Authorization is the key's: dns01:write, narrowed to the hostname if the key is."
      }
    },
    "parameters": {
      "HostnameId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The hostname's id, returned when it was created.",
        "schema": {
          "type": "string",
          "example": "host_01937d3e8f217c4ab8e24f9a1c3d5e6b"
        }
      }
    },
    "schemas": {
      "HostView": {
        "type": "object",
        "description": "A hostname. Where it points is deliberately not here: `ip`, `record_type` and the target are either something you just sent or something DNS answers authoritatively, and a second copy in this body can only disagree with it. Ask /v1/hostnames/{id}/diagnose for where it points and what we observed, with the time we observed it.",
        "required": [
          "id",
          "hostname",
          "status",
          "certificate",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Opaque identifier used in the /v1/hostnames/{id} paths."
          },
          "hostname": {
            "type": "string",
            "description": "The fully qualified hostname: an opaque subdomain on a domain assigned to your account."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "live",
              "released",
              "suspended",
              "terminated"
            ],
            "x-status-docs": [
              {
                "value": "pending",
                "means": "Registered, not yet confirmed on the nameservers. Poll GET until it goes live.",
                "writes": "Accepted"
              },
              {
                "value": "live",
                "means": "The record is served, and it stays so until you release it.",
                "writes": "Accepted"
              },
              {
                "value": "released",
                "means": "You released it. It stops appearing in listings, and the subdomain is yours to mint again as a new hostname.",
                "writes": "409"
              },
              {
                "value": "suspended",
                "means": "Held while an abuse report is reviewed. It stops resolving, but nothing is lost: if the report is not upheld the hostname, its IP and its DNS-01 credentials all come back.",
                "writes": "403"
              },
              {
                "value": "terminated",
                "means": "An abuse report was upheld. Not reversible, and it stops appearing in listings.",
                "writes": "409"
              }
            ],
            "description": "pending: registered, not yet confirmed on the nameservers. live: the record is served, and it stays so until you release it. released: you released it; writes return 409 and the subdomain can be minted again as a new hostname. suspended: held while an abuse report is reviewed; it stops resolving and writes return 403, but nothing is lost if the report is not upheld. terminated: an abuse report was upheld, not reversible. New registrations may briefly return pending; poll GET until live."
          },
          "certificate": {
            "type": "string",
            "enum": [
              "self",
              "csr",
              "managed"
            ],
            "description": "Who holds the private key. self: you, and you run the ACME client. csr: you hold the key and we run the protocol around your signed request. managed: we generate the key, obtain and renew the certificate, and keep a copy for you to fetch."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the hostname was registered."
          }
        }
      },
      "HostnameList": {
        "type": "object",
        "required": [
          "hostnames",
          "total",
          "active",
          "cap",
          "limit",
          "skip"
        ],
        "properties": {
          "hostnames": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostView"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0,
            "description": "Non-released hostnames on the team. Used for pagination."
          },
          "active": {
            "type": "integer",
            "minimum": 0,
            "description": "Live hostnames. This is what the create cap counts."
          },
          "cap": {
            "type": "integer",
            "minimum": 0,
            "description": "Plan maximum for hostnames held at once. Registration returns 429 at the cap."
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "description": "Page size actually applied after clamping."
          },
          "skip": {
            "type": "integer",
            "minimum": 0,
            "description": "Offset actually applied."
          }
        }
      },
      "CreateHostnameRequest": {
        "type": "object",
        "required": [],
        "properties": {
          "ip": {
            "type": "string",
            "description": "An IPv4 or IPv6 address, public or private. Private and reserved ranges are accepted on every plan. Send either ip or target, not both. The address a hostname is minted with fixes its address class for life. A hostname keeps the address class it was minted with. Repointing within a class is free, including between A and AAAA; crossing from public to private or private to public returns 409. This blocks DNS rebinding, where one origin is public first, to deliver a payload under a trusted certificate, and private second, to read a response from inside the visitor's network. Release the hostname and mint a new one for the other class."
          },
          "target": {
            "type": "string",
            "description": "A platform endpoint to follow instead of an address, for a platform that gives you a URL and no IP. We resolve it and publish ordinary A or AAAA records, then re-resolve within the target's TTL and follow it as it moves. Send either ip or target, not both. What the target answers fixes the hostname's address class exactly as an ip would, and a target that later answers outside that class is refused and the last address stays served. A target answering both public and private addresses is refused as ambiguous."
          },
          "subdomain": {
            "type": "string",
            "description": "Optional custom subdomain, on every plan. 3-63 lowercase letters, digits, and hyphens; no periods; no leading or trailing hyphen. Duplicate or retired subdomains return 409."
          },
          "domain": {
            "type": "string",
            "description": "Optional dedicated domain. Must already be assigned to the team. Omitted, one of the team's dedicated domains is chosen."
          },
          "certificate": {
            "type": "string",
            "enum": [
              "self",
              "csr",
              "managed"
            ],
            "default": "self",
            "description": "Who holds the private key, and with it who runs the ACME client. \"self\" means you do all of it: run an ACME client against the DNS-01 API, pick your own CA, and we never see key material. \"csr\" means you keep the key and we run the protocol: register, then POST a certificate request to /v1/hostnames/{id}/certificate. Renewal is yours there, because we hold no key to build a replacement from. \"managed\" means we generate the key, run ACME, and renew before expiry on a fresh key each time; you pull the current pair from GET /v1/hostnames/{id}/certificate. See Certificate modes."
          },
          "ca": {
            "type": "string",
            "enum": [
              "letsencrypt",
              "google"
            ],
            "description": "Which certificate authority issues, for certificate: \"managed\" and certificate: \"csr\" - the modes where we run the ACME client. Sending it with \"self\" returns 400: you run the client there, so the CA is a choice you make in it. Omitted, we pick - Let's Encrypt unless it has recently rate-limited orders for your domain and another CA is configured. Renewals always return to the CA that issued, because the ACME Renewal Information identifier is only resolvable by that CA. See https://dnsmint.com/ca"
          }
        }
      },
      "RegisterIpRequest": {
        "type": "object",
        "required": [],
        "properties": {
          "ip": {
            "type": "string",
            "description": "An IPv4 or IPv6 address, public or private. Private and reserved ranges are accepted on every plan. A hostname keeps the address class it was minted with. Repointing within a class is free, including between A and AAAA; crossing from public to private or private to public returns 409. This blocks DNS rebinding, where one origin is public first, to deliver a payload under a trusted certificate, and private second, to read a response from inside the visitor's network. Release the hostname and mint a new one for the other class."
          },
          "target": {
            "type": "string",
            "description": "A platform endpoint to follow instead of an address. Send this or `ip`, never both. We resolve it and publish ordinary A and AAAA records, refreshed within the target's own TTL, so the name never carries a CNAME and every record you set on the hostname keeps working. The hostname keeps the address class it was minted with, and that is re-checked every time we refresh rather than only when you set it: a target that starts answering with a private address is refused and the hostname stays on its last public one. Setting `ip` takes the hostname back off its target."
          }
        },
        "description": "Exactly one of `ip` or `target`. Setting `ip` on a hostname that follows a target takes it off that target."
      },
      "ReleaseResult": {
        "type": "object",
        "required": [
          "released"
        ],
        "properties": {
          "released": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Standard error shape for every non-2xx response.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "enum": [
              "UNAUTHORIZED",
              "KEY_EXPIRED",
              "FORBIDDEN",
              "NOT_FOUND",
              "BAD_REQUEST",
              "CONFLICT",
              "PAYMENT_REQUIRED",
              "RATE_LIMITED",
              "INTERNAL_ERROR",
              "NOT_IMPLEMENTED",
              "SERVICE_UNAVAILABLE",
              "DOMAIN_PROVISIONING",
              "NO_DOMAIN_AVAILABLE"
            ],
            "description": "Stable machine-readable code."
          },
          "field": {
            "type": "string",
            "description": "The request body property the message is about, when it is about one. Present on validation failures so a client can show the message beside the input that caused it rather than above the whole form. Optional: a caller that ignores it reads `error` as before.",
            "example": "subdomain"
          }
        }
      },
      "AcmeRegistration": {
        "type": "object",
        "description": "acme-dns registration blob. The password appears exactly once, at creation.",
        "required": [
          "username",
          "password",
          "fulldomain",
          "subdomain",
          "server_url",
          "allowfrom"
        ],
        "properties": {
          "username": {
            "type": "string",
            "format": "uuid"
          },
          "password": {
            "type": "string"
          },
          "fulldomain": {
            "type": "string",
            "example": "_acme-challenge.q7k4m2.a3f9c1-d4e7b8.dev"
          },
          "subdomain": {
            "type": "string",
            "description": "Equals username; send it in acme-dns update calls."
          },
          "server_url": {
            "type": "string",
            "example": "https://dnsmint.com/api/acme"
          },
          "allowfrom": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "HostRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this to DELETE."
          },
          "name": {
            "type": "string",
            "example": "_443._tcp.q7k4m2ab.pool-1.dnsmint.com"
          },
          "type": {
            "type": "string",
            "enum": [
              "TXT",
              "TLSA",
              "CAA",
              "MX",
              "SRV",
              "SSHFP",
              "HTTPS",
              "SVCB"
            ]
          },
          "ttl": {
            "type": "integer",
            "example": 300
          },
          "data": {
            "type": "object",
            "description": "The record's fields: `text` for TXT; `usage`, `selector`, `matching_type` and `association` for TLSA."
          }
        }
      },
      "HostRecordList": {
        "type": "object",
        "properties": {
          "records": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostRecord"
            }
          }
        }
      },
      "CreateRecordRequest": {
        "type": "object",
        "required": [
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "TXT",
              "TLSA",
              "CAA",
              "MX",
              "SRV",
              "SSHFP",
              "HTTPS",
              "SVCB"
            ],
            "description": "A hostname's address is set by PUT on the hostname itself, and CAA is operator policy, so neither is accepted here."
          },
          "name": {
            "type": "string",
            "description": "Label under the hostname. Omit for a record on the hostname itself. `_acme-challenge` is reserved: certificate issuance publishes there. TLSA requires `_<port>._<proto>`, e.g. `_443._tcp` - RFC 6698 puts the port and protocol in the owner name and a client looks nowhere else.",
            "example": "_443._tcp"
          },
          "text": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 255
                },
                "minItems": 1
              }
            ],
            "description": "TXT only. A string, or a list of strings to send as-is. No control characters. RFC 1035 caps each character-string at 255 bytes and a TXT record holds a sequence of them which the client concatenates, so a longer value is split for you at 255 and a list you supply is sent unchanged, each string checked against that limit. 2048 bytes in total. This is how a 2048-bit DKIM key or a full MCP discovery record is carried."
          },
          "usage": {
            "type": "integer",
            "enum": [
              0,
              1,
              2,
              3
            ],
            "description": "TLSA certificate usage. 3 is DANE-EE."
          },
          "selector": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "TLSA selector. 0 full certificate, 1 SubjectPublicKeyInfo."
          },
          "matching_type": {
            "type": "integer",
            "enum": [
              0,
              1,
              2
            ],
            "description": "TLSA matching type. 0 full, 1 SHA-256, 2 SHA-512. The association length must match: 64 hex digits for SHA-256, 128 for SHA-512."
          },
          "association": {
            "type": "string",
            "description": "TLSA certificate-association data, lowercase hex, no separators."
          },
          "preference": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "MX only. Lower is preferred. 0 with an exchange of `.` is the RFC 7505 null MX, which says this name accepts no mail."
          },
          "exchange": {
            "type": "string",
            "description": "MX only. The mail host. `.` is the root, for the null MX."
          },
          "priority": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "SRV: lower is tried first. HTTPS and SVCB: 1 or above. 0 is AliasMode, which we do not serve, because it aliases a zone apex and a hostname is never an apex."
          },
          "weight": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "SRV only. Relative share among targets of equal priority."
          },
          "port": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "SRV only. The port the service listens on."
          },
          "target": {
            "type": "string",
            "description": "SRV: the host offering the service, or `.` to say it is decidedly not offered here. HTTPS and SVCB: the endpoint, or `.` for this name, which is the ordinary case."
          },
          "algorithm": {
            "type": "integer",
            "description": "SSHFP only. 1 RSA, 2 DSA, 3 ECDSA, 4 Ed25519, 6 Ed448."
          },
          "fptype": {
            "type": "integer",
            "description": "SSHFP only. 1 for SHA-1, 2 for SHA-256. The fingerprint length follows from it."
          },
          "fingerprint": {
            "type": "string",
            "description": "SSHFP only. Lowercase hex, no separators. 40 digits for SHA-1, 64 for SHA-256."
          },
          "params": {
            "type": "object",
            "description": "HTTPS and SVCB only. RFC 9460 service parameters: `alpn` (list, e.g. h2 or h3), `noDefaultAlpn` (boolean, only meaningful with `alpn`), `port`, `ipv4hint` and `ipv6hint` (lists of literal addresses), `ech` (base64), and `mandatory` (the parameter names a client must understand, which may not name itself or a parameter this record does not set). Send them as fields; we emit them in the ascending key order the wire format requires."
          }
        }
      },
      "ManagedCertificate": {
        "type": "object",
        "properties": {
          "certificate": {
            "type": "string",
            "description": "PEM chain, leaf first."
          },
          "private_key": {
            "type": "string",
            "description": "PKCS#8 PEM. Present only on managed, where we generated it - our copy is encrypted at rest under a key separate from every other secret we store, and is replaced at every renewal. Absent on csr, because you hold that key and we never had it."
          },
          "privateKey": {
            "type": "string",
            "description": "Deprecated spelling, still accepted. Use `private_key`.",
            "deprecated": true
          },
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "deprecated": true,
            "description": "Deprecated spelling, still accepted. Use `expires_at`."
          },
          "ca": {
            "type": "string",
            "enum": [
              "letsencrypt",
              "google"
            ],
            "description": "Which CA signed this certificate. The issuer, not the preference on the hostname - a renewal can land on a different CA than the original request asked for."
          },
          "issuer": {
            "type": "string",
            "description": "The CA's display name, for a log line or a status page."
          },
          "mode": {
            "type": "string",
            "enum": [
              "csr",
              "managed"
            ],
            "description": "The hostname's certificate mode, so a client can tell whether private_key should be there."
          }
        }
      },
      "CertificateRequestSubmission": {
        "type": "object",
        "required": [
          "csr"
        ],
        "properties": {
          "csr": {
            "type": "string",
            "description": "A PKCS#10 certificate request, PEM or bare base64. Must name exactly the hostname (and its wildcard when wildcard is true) in subjectAltName, and be signed by its own key. ECDSA, or RSA of at least 2048 bits, with SHA-256, SHA-384 or SHA-512."
          },
          "wildcard": {
            "type": "boolean",
            "default": false,
            "description": "Cover *.hostname as well. The csr must name both."
          },
          "ca": {
            "type": "string",
            "enum": [
              "letsencrypt",
              "google"
            ],
            "description": "Which authority issues. Omitted, we pick. See https://dnsmint.com/ca"
          }
        }
      },
      "CertificateOrderAccepted": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The order id."
          },
          "state": {
            "type": "string",
            "description": "Where the order is. `pending` on acceptance; it advances in the background."
          },
          "names": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every name the certificate will cover."
          },
          "ca": {
            "type": "string",
            "description": "Which authority is issuing."
          },
          "issuer": {
            "type": "string",
            "description": "That authority's name."
          }
        }
      },
      "ApiKeySummary": {
        "type": "object",
        "description": "A key as seen from outside. Never contains the secret.",
        "properties": {
          "key_id": {
            "type": "string",
            "description": "The id half of dnsm_<key_id>_<secret>."
          },
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "scope": {
                  "type": "string",
                  "enum": [
                    "hostnames:read",
                    "hostnames:write",
                    "dns01:write",
                    "keys:write"
                  ]
                },
                "on": {
                  "type": "string",
                  "description": "The domain or hostname this scope is narrowed to. Absent means the whole team."
                }
              }
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Null for a key that does not expire."
          },
          "created_by_key_id": {
            "type": "string",
            "nullable": true,
            "description": "The key that minted this one, or null when a person created it in the dashboard."
          },
          "expired": {
            "type": "boolean"
          }
        }
      },
      "ApiKeyList": {
        "type": "object",
        "properties": {
          "keys": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKeySummary"
            }
          }
        }
      },
      "CreateKeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 60
          },
          "scopes": {
            "type": "array",
            "description": "Scope strings for the whole team, or objects narrowing one to a domain or hostname. Omit for the three hostname and DNS-01 scopes on the whole team; keys:write is never included by default and must be named.",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "object",
                  "properties": {
                    "scope": {
                      "type": "string"
                    },
                    "domain": {
                      "type": "string"
                    },
                    "hostname": {
                      "type": "string"
                    }
                  }
                }
              ]
            }
          },
          "expires_in_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 3650,
            "nullable": true,
            "description": "Days until the key stops working. Null or omitted for a key that does not expire."
          }
        }
      },
      "CreatedApiKey": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKeySummary"
          },
          {
            "type": "object",
            "required": [
              "key"
            ],
            "properties": {
              "key": {
                "type": "string",
                "description": "The full secret, dnsm_<key_id>_<secret>. Returned only here and never retrievable again."
              }
            }
          }
        ]
      },
      "Diagnosis": {
        "type": "object",
        "properties": {
          "hostname": {
            "type": "string"
          },
          "summary": {
            "type": "string",
            "description": "The first failure, or a statement that the hostname is working."
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DiagnosticCheck"
            }
          },
          "checked_at": {
            "type": "string",
            "format": "date-time"
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time",
            "deprecated": true,
            "description": "Deprecated spelling, still accepted. Use `checked_at`."
          }
        }
      },
      "DiagnosticCheck": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "status",
              "record",
              "address",
              "delegation",
              "nameservers",
              "public",
              "dnssec",
              "caa",
              "challenge",
              "certificate"
            ]
          },
          "label": {
            "type": "string"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "warn",
              "unknown",
              "skipped"
            ],
            "description": "unknown means we could not find out, which is never the same as pass."
          },
          "detail": {
            "type": "string",
            "description": "What we saw."
          },
          "action": {
            "type": "string",
            "description": "What to do about it. Absent on pass."
          }
        }
      },
      "CheckoutRequest": {
        "type": "object",
        "properties": {
          "hostnames": {
            "type": "integer",
            "description": "Hostnames the plan should include."
          },
          "domains": {
            "type": "integer",
            "description": "Dedicated domains the plan should include."
          }
        }
      },
      "Checkout": {
        "type": "object",
        "required": [
          "checkout_url",
          "amount_cents",
          "selection"
        ],
        "properties": {
          "checkout_url": {
            "type": "string",
            "description": "Where the account owner completes the purchase."
          },
          "amount_cents": {
            "type": "integer",
            "description": "What the plan costs each month, in cents."
          },
          "selection": {
            "type": "object",
            "description": "The plan being bought: domains per pool, and hostnames."
          }
        }
      },
      "CheckoutConflict": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "manage_url": {
            "type": "string",
            "description": "Present when a plan already exists: the page where a person changes it."
          }
        }
      },
      "ProvisioningStatus": {
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "none",
              "provisioning",
              "ready",
              "claimed"
            ],
            "description": "`none`: nothing is being bought, so retrying will not help. `provisioning`: a domain is being registered or is waiting on delegation. `ready`: bought and delegated, waiting to be claimed. `claimed`: assigned to this account, so minting works."
          },
          "domain": {
            "type": "string",
            "description": "The domain bought for this account, when there is one."
          },
          "since": {
            "type": "string",
            "format": "date-time",
            "description": "When it was registered, so a caller can show how long it has been."
          },
          "delegated": {
            "type": "boolean",
            "description": "Whether public DNS shows our nameservers yet."
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before asking again. Zero when there is nothing to wait for."
          }
        },
        "required": [
          "state",
          "retry_after"
        ]
      },
      "BillingStatus": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "object",
            "properties": {
              "active": {
                "type": "boolean",
                "description": "Whether a subscription is active."
              },
              "name": {
                "type": "string",
                "description": "The plan's display name."
              }
            }
          },
          "hostnames": {
            "type": "object",
            "properties": {
              "active": {
                "type": "integer"
              },
              "cap": {
                "type": "integer"
              }
            }
          },
          "domains": {
            "type": "object",
            "properties": {
              "held": {
                "type": "integer"
              },
              "cap": {
                "type": "integer"
              }
            }
          },
          "pending_checkout": {
            "type": "object",
            "nullable": true,
            "description": "Present only while a checkout has been started and no plan is active.",
            "properties": {
              "started_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          }
        },
        "required": [
          "plan",
          "hostnames",
          "domains"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "The Authorization header is missing, malformed, or the key is unknown or revoked.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Missing or invalid API key",
              "code": "UNAUTHORIZED"
            }
          }
        },
        "x-error-codes": [
          {
            "code": "UNAUTHORIZED",
            "when": "The Authorization header is missing, malformed, or the key is unknown or revoked"
          }
        ]
      },
      "NotFound": {
        "description": "No hostname with this id belongs to the authenticated account.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Hostname not found",
              "code": "NOT_FOUND"
            }
          }
        },
        "x-error-codes": [
          {
            "code": "NOT_FOUND",
            "when": "No hostname with this id belongs to the authenticated account"
          }
        ]
      },
      "Released": {
        "description": "The hostname was released. Released names cannot be revived; register a new one.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Hostname was released; register a new one",
              "code": "CONFLICT"
            }
          }
        },
        "x-error-codes": [
          {
            "code": "CONFLICT",
            "when": "The hostname was released. Released names cannot be revived; register a new one"
          }
        ]
      },
      "Internal": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Internal server error",
              "code": "INTERNAL_ERROR"
            }
          }
        },
        "x-error-codes": [
          {
            "code": "INTERNAL_ERROR",
            "when": "Unexpected server error"
          }
        ]
      }
    }
  }
}
