{
  "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/docs"
    }
  },
  "servers": [
    {
      "url": "https://dnsmint.com/api"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "hostnames",
      "description": "Register, read, update, renew, and release hostnames."
    }
  ],
  "paths": {
    "/v1/hostnames": {
      "post": {
        "operationId": "createHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Register an IP and mint a hostname",
        "description": "Registers a public IPv4 or IPv6 address and returns a new hostname with an opaque label on a pool domain. The name starts live with a 7-day idle expiry. New registrations may briefly return \"pending\"; poll GET until \"live\". Request bodies are capped at 4KB. Pro and Business accounts may pass an optional custom label.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterIpRequest"
              },
              "example": {
                "ip": "34.120.50.10"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Hostname created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostView"
                },
                "example": {
                  "id": "68ad3a1e9c4b2f0d5e6a7b8c",
                  "hostname": "q7k4m2.pool-3.mintzone.net",
                  "ip": "34.120.50.10",
                  "record_type": "A",
                  "status": "live",
                  "expires_at": "2026-09-02T08:30:00.000Z",
                  "created_at": "2026-08-26T08:30:00.000Z"
                }
              }
            }
          },
          "400": {
            "description": "Invalid JSON, missing \"ip\" field, syntactically invalid IP address, invalid or blocked custom label, 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"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The IP is private or reserved (Business plan), or a custom label was requested without a Pro plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Private and reserved addresses require a Business plan",
                  "code": "FORBIDDEN"
                }
              }
            }
          },
          "409": {
            "description": "The requested custom label is taken or retired on this domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Label is taken or retired on this domain",
                  "code": "CONFLICT"
                }
              }
            }
          },
          "429": {
            "description": "The account already has 25 active hostnames, the free-tier cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Active hostname cap reached (25). Release unused names or upgrade.",
                  "code": "RATE_LIMITED"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "get": {
        "operationId": "listHostnames",
        "tags": [
          "hostnames"
        ],
        "summary": "List this account's hostnames",
        "description": "Returns the account's hostnames, newest first, excluding released ones. Expired names appear with status \"expired\". Listing does not renew anything.",
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostnameList"
                },
                "example": {
                  "hostnames": [
                    {
                      "id": "68ad3a1e9c4b2f0d5e6a7b8c",
                      "hostname": "q7k4m2.pool-3.mintzone.net",
                      "ip": "34.120.50.10",
                      "record_type": "A",
                      "status": "live",
                      "expires_at": "2026-09-02T08:30:00.000Z",
                      "created_at": "2026-08-26T08:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/hostnames/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HostnameId"
        }
      ],
      "get": {
        "operationId": "getHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Read one hostname",
        "description": "Returns one hostname. Reading a live hostname counts as use and renews it: the idle expiry slides to 7 days from now. New registrations may briefly return \"pending\"; poll GET until \"live\".",
        "responses": {
          "200": {
            "description": "The hostname, renewed if it was live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostView"
                },
                "example": {
                  "id": "68ad3a1e9c4b2f0d5e6a7b8c",
                  "hostname": "q7k4m2.pool-3.mintzone.net",
                  "ip": "34.120.50.10",
                  "record_type": "A",
                  "status": "live",
                  "expires_at": "2026-09-02T09:15:00.000Z",
                  "created_at": "2026-08-26T08:30:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "put": {
        "operationId": "updateHostnameIp",
        "tags": [
          "hostnames"
        ],
        "summary": "Point the hostname at a new IP",
        "description": "Replaces the registered IP. The hostname and any certificates issued for it are unchanged. The record type follows the address (A for IPv4, AAAA for IPv6), the name comes back live, and the idle expiry slides to 7 days from now. Request bodies are capped at 4KB.",
        "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": "68ad3a1e9c4b2f0d5e6a7b8c",
                  "hostname": "q7k4m2.pool-3.mintzone.net",
                  "ip": "34.120.51.22",
                  "record_type": "A",
                  "status": "live",
                  "expires_at": "2026-09-02T10:00:00.000Z",
                  "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"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The IP is private or reserved. Private addresses require a Business plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Private and reserved addresses require a Business plan",
                  "code": "FORBIDDEN"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Released"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      },
      "delete": {
        "operationId": "releaseHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Release a hostname",
        "description": "Releases the hostname permanently. The DNS record stops being served and the label is never reused, by this account or any other. 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"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/hostnames/{id}/renew": {
      "parameters": [
        {
          "$ref": "#/components/parameters/HostnameId"
        }
      ],
      "post": {
        "operationId": "renewHostname",
        "tags": [
          "hostnames"
        ],
        "summary": "Renew a hostname",
        "description": "Heartbeat with no request body. Slides the idle expiry to 7 days from now and brings an expired (but not released) hostname back to live.",
        "responses": {
          "200": {
            "description": "The renewed hostname.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostView"
                },
                "example": {
                  "id": "68ad3a1e9c4b2f0d5e6a7b8c",
                  "hostname": "q7k4m2.pool-3.mintzone.net",
                  "ip": "34.120.50.10",
                  "record_type": "A",
                  "status": "live",
                  "expires_at": "2026-09-02T11:45:00.000Z",
                  "created_at": "2026-08-26T08:30:00.000Z"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Released"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/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"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Hostname not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Hostname is not live",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Credential limit reached (5)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/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 or expired 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"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid acme-dns credentials",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Subdomain does not belong to this credential",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "400": {
            "description": "Malformed challenge value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "dnsm_<keyId>_<secret>",
        "description": "API key sent as Authorization: Bearer dnsm_<keyId>_<secret>. Keys are created in the DNSMint dashboard; the secret is shown once at creation. Each account can hold up to 20 keys."
      }
    },
    "parameters": {
      "HostnameId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The hostname's id, returned when it was created.",
        "schema": {
          "type": "string",
          "example": "68ad3a1e9c4b2f0d5e6a7b8c"
        }
      }
    },
    "schemas": {
      "HostView": {
        "type": "object",
        "description": "A registered hostname.",
        "required": [
          "id",
          "hostname",
          "ip",
          "record_type",
          "status",
          "expires_at",
          "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 label on a pool domain."
          },
          "ip": {
            "type": "string",
            "description": "The registered IPv4 or IPv6 address."
          },
          "record_type": {
            "type": "string",
            "enum": [
              "A",
              "AAAA"
            ],
            "description": "A for IPv4, AAAA for IPv6. Follows the registered address."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "live",
              "expired",
              "released"
            ],
            "description": "pending: registered, not yet confirmed on the nameservers. live: the record is served. expired: idle for over 7 days; renewable. released: permanently retired. New registrations may briefly return pending; poll GET until live."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the name expires if it sees no further authenticated use. Every create, read of the hostname, IP update, or renew moves it 7 days out."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the hostname was registered."
          }
        }
      },
      "HostnameList": {
        "type": "object",
        "required": [
          "hostnames"
        ],
        "properties": {
          "hostnames": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HostView"
            }
          }
        }
      },
      "RegisterIpRequest": {
        "type": "object",
        "required": [
          "ip"
        ],
        "properties": {
          "ip": {
            "type": "string",
            "description": "A public IPv4 or IPv6 address. Private and reserved ranges are rejected with 403 until Business-plan support ships."
          },
          "label": {
            "type": "string",
            "description": "Optional custom hostname label (Pro/Business). 3-63 lowercase letters, digits, and hyphens; no leading/trailing hyphen; not a reserved or impersonating term. Duplicate or retired labels return 409."
          }
        }
      },
      "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",
              "FORBIDDEN",
              "NOT_FOUND",
              "BAD_REQUEST",
              "CONFLICT",
              "RATE_LIMITED",
              "INTERNAL_ERROR",
              "SERVICE_UNAVAILABLE"
            ],
            "description": "Stable machine-readable code."
          }
        }
      },
      "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.pool-1.example"
          },
          "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"
            }
          }
        }
      }
    },
    "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"
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "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"
            }
          }
        }
      },
      "Internal": {
        "description": "Unexpected server error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Internal server error",
              "code": "INTERNAL_ERROR"
            }
          }
        }
      }
    }
  }
}