{
  "openapi": "3.1.0",
  "info": {
    "title": "rentadomain.sh API",
    "version": "1",
    "summary": "The names for rent and their prices, and DNS control for a rented name, for agents.",
    "description": "Names are rented, never sold. GET /api/v1/names is open: it lists every name for rent with the rent, total, floor and ceiling for each term. Everything under /api/v1/leases needs a token. Renting needs a person: they sign in, pass an identity check, sign the lease and pay by card at https://rentadomain.sh/names. That person then creates a token from the dashboard at https://rentadomain.sh/app. A token cannot rent, sign or pay. One token is scoped to one lease. It can read and change DNS records on that lease's name. It cannot apply a plan that touches an MX, CAA, SPF, DKIM or DMARC record, a wildcard record or a TXT at the apex, or a plan with more than 10 changes: a person applies those in the dashboard. A lease keeps its id across renewals, and the token stays on it. GET /api/v1/leases returns the lease the token works on; an id from an older lease that this one replaced still resolves. Limits: 60 record changes per hour on a lease. A plan lives 15 minutes. The first plan applied on a new lease is held for a person on our side to review: apply answers 202, and GET /api/v1/leases/{id}/plans/{planId} says what became of it. A plan can be applied while the lease is active or renewal_offered. HTTPS only: over plain HTTP every route that takes a token answers 403 with code https_required. Every error is JSON, {\"error\":{\"code\",\"message\"}}. To search, price and read terms from an MCP client, use the MCP server at https://rentadomain.sh/mcp (install lines per client at https://rentadomain.sh/for-agents).",
    "contact": {
      "name": "rentadomain.sh",
      "email": "lease@rentadomain.sh",
      "url": "https://rentadomain.sh/api"
    }
  },
  "servers": [
    {
      "url": "https://rentadomain.sh"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "externalDocs": {
    "description": "Plain text API reference",
    "url": "https://rentadomain.sh/api"
  },
  "x-mcp": {
    "url": "https://rentadomain.sh/mcp",
    "transport": "streamable-http",
    "docs": "https://rentadomain.sh/for-agents"
  },
  "paths": {
    "/api/v1/names": {
      "get": {
        "operationId": "listNames",
        "summary": "Every name for rent now, with the price of each term. No token.",
        "security": [],
        "responses": {
          "200": {
            "description": "The listed names.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "names"
                  ],
                  "properties": {
                    "names": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Name"
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/names/{host}": {
      "get": {
        "operationId": "getName",
        "summary": "One name, listed or rented, with the price of each term. No token.",
        "security": [],
        "parameters": [
          {
            "name": "host",
            "in": "path",
            "required": true,
            "description": "The name, like grownbyai.com. Case, a scheme, a path and a leading www. are ignored.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The name.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "name"
                  ],
                  "properties": {
                    "name": {
                      "$ref": "#/components/schemas/Name"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not a name listed or rented here. Code not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leases": {
      "get": {
        "operationId": "listLeases",
        "summary": "List the leases the token can see.",
        "description": "Always zero or one lease: the one the token works on. A lease keeps its id across renewals, so the id returned here stays good for as long as the lease runs.",
        "responses": {
          "200": {
            "description": "The token's lease.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "leases"
                  ],
                  "properties": {
                    "leases": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Lease"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token. Code unauthorized. Message: \"Send Authorization: Bearer rd_...\" error.docs is the URL of the API reference.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "The Bearer challenge (RFC 6750).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leases/{id}/records": {
      "get": {
        "operationId": "listRecords",
        "summary": "List the DNS records on one lease. Needs dns:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The lease id from GET /api/v1/leases, like l_abc123. A token only ever sees its own lease. A lease keeps its id across renewals. An id from an older lease that this one replaced still reaches it; GET /api/v1/leases always returns the current id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The live records.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "records"
                  ],
                  "properties": {
                    "records": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Record"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token. Code unauthorized. Message: \"Send Authorization: Bearer rd_...\" error.docs is the URL of the API reference.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "The Bearer challenge (RFC 6750).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope. Code forbidden. Message: \"This token cannot do that.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not the token's lease, or the lease is closed. Code not_found. Message: \"No such lease.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "DNS for the name is paused by a problem on our side. Code records_paused. Message: \"DNS for this name is paused by a problem on our side. We have been alerted. Nothing you set is lost.\" Try again later; nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leases/{id}/records/plan": {
      "post": {
        "operationId": "planRecords",
        "summary": "Propose a change. Nothing is written yet. Needs dns:write.",
        "description": "The verdict says whether policy allows the plan (ok) and whether it needs a person (protected). A plan lives 15 minutes.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The lease id from GET /api/v1/leases, like l_abc123. A token only ever sees its own lease. A lease keeps its id across renewals. An id from an older lease that this one replaced still reaches it; GET /api/v1/leases always returns the current id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ops"
                ],
                "properties": {
                  "ops": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50,
                    "items": {
                      "$ref": "#/components/schemas/Op"
                    },
                    "description": "Up to 50 changes. A plan with more than 10 changes is protected, so a token cannot apply it: keep a plan a token will apply to 10 or fewer."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The plan and the policy verdict.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan",
                    "verdict"
                  ],
                  "properties": {
                    "plan": {
                      "$ref": "#/components/schemas/Plan"
                    },
                    "verdict": {
                      "$ref": "#/components/schemas/Verdict"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The ops list is malformed, empty or longer than 50. Code bad_ops. Message: \"ops must be a non-empty list of create, update or delete operations.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token. Code unauthorized. Message: \"Send Authorization: Bearer rd_...\" error.docs is the URL of the API reference.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "The Bearer challenge (RFC 6750).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope. Code forbidden. Message: \"This token cannot do that.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not the token's lease, or the lease is closed. Code not_found. Message: \"No such lease.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "DNS for the name is paused by a problem on our side. Code records_paused. Message: \"DNS for this name is paused by a problem on our side. We have been alerted. Nothing you set is lost.\" Try again later; nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leases/{id}/records/apply": {
      "post": {
        "operationId": "applyPlan",
        "summary": "Apply a proposed plan by id. Needs dns:write.",
        "description": "A protected plan is refused with 403 and code protected: a person applies those from the dashboard. The first plan on a new lease is held for a person on our side to review and answers 202 with pending_review; nothing is written until it is approved, and the plan read says what became of it. Do not apply a held plan again. A renter whose identity is not verified gets 403 and code identity_required. A plan can be applied while the lease is active or renewal_offered.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The lease id from GET /api/v1/leases, like l_abc123. A token only ever sees its own lease. A lease keeps its id across renewals. An id from an older lease that this one replaced still reaches it; GET /api/v1/leases always returns the current id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "planId"
                ],
                "properties": {
                  "planId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "changes"
                  ],
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "changes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Change"
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Held: the first plan on a lease waits for a person to review it. Nothing was written. Poll status_url.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "pending_review",
                    "message"
                  ],
                  "properties": {
                    "ok": {
                      "const": false
                    },
                    "pending_review": {
                      "const": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "plan": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "const": "pending_review"
                        }
                      }
                    },
                    "status_url": {
                      "type": "string",
                      "description": "The plan read for this plan: GET it to learn whether the plan was applied or rejected."
                    },
                    "dashboard_url": {
                      "type": "string",
                      "description": "The lease in the dashboard, where the renter sees the held plan."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No planId. Code bad_plan. Message: \"planId must name a plan for this lease.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token. Code unauthorized. Message: \"Send Authorization: Bearer rd_...\" error.docs is the URL of the API reference.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "The Bearer challenge (RFC 6750).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "Missing scope (code forbidden), a protected plan (code protected: the message says why and names the plan, and the error also carries plan_id, dashboard_url, where a person applies it, and protected_reasons), or a renter whose identity is not yet verified (code identity_required).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not the token's lease, or no such plan on it. Code not_found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The plan was not applied and nothing changed. The code says why: expired (the plan outlived its time, plan again), not_proposed (already applied, held, rejected or expired), lease_inactive (the lease is not in a status that allows changes), policy (the zone changed and policy now refuses the plan; the message carries the reasons), failed (the write failed and was rolled back), not_yours, or apply_failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leases/{id}/plans/{planId}": {
      "get": {
        "operationId": "getPlan",
        "summary": "Where a plan stands. Needs dns:read.",
        "description": "The way to learn what became of a plan that was held for review: poll it until status leaves pending_review. A proposed plan past its time reads as expired.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The lease id from GET /api/v1/leases, like l_abc123. A token only ever sees its own lease. A lease keeps its id across renewals. An id from an older lease that this one replaced still reaches it; GET /api/v1/leases always returns the current id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "planId",
            "in": "path",
            "required": true,
            "description": "The plan id from the plan response, like p_xyz789.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The plan.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan"
                  ],
                  "properties": {
                    "plan": {
                      "$ref": "#/components/schemas/Plan"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed or revoked token. Code unauthorized. Message: \"Send Authorization: Bearer rd_...\" error.docs is the URL of the API reference.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "WWW-Authenticate": {
                "description": "The Bearer challenge (RFC 6750).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "The token lacks the scope. Code forbidden. Message: \"This token cannot do that.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No plan with that id on this lease (code not_found, message \"No such plan on this lease.\"), or not the token's lease, or the lease is closed (code not_found, message \"No such lease.\"). The message says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Our side failed. Code internal. Message: \"Something failed on our side. It is logged.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "An rd_ token: rd_ followed by 40 letters and digits, shown once when the person who rented creates it in the dashboard. Scopes: dns:read, dns:write. One token works on one lease."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "What to branch on."
              },
              "message": {
                "type": "string",
                "description": "A sentence for whoever reads the answer."
              },
              "docs": {
                "type": "string",
                "description": "On a 401 and on an unknown route: the URL of the API reference."
              },
              "plan_id": {
                "type": "string",
                "description": "On a 403 with code protected: the plan that was refused."
              },
              "dashboard_url": {
                "type": "string",
                "description": "On a 403 with code protected: where a person applies the plan."
              },
              "protected_reasons": {
                "type": "array",
                "description": "Why the plan is protected, when it is.",
                "items": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "protected_record",
                        "plan_too_large"
                      ]
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "NameTerm": {
        "type": "object",
        "description": "One term on a name's menu, with that term's own rent, total and renewal bounds.",
        "required": [
          "months",
          "monthly_cents",
          "total_cents",
          "floor_cents",
          "ceiling_cents"
        ],
        "properties": {
          "months": {
            "type": "integer",
            "description": "Term length in months."
          },
          "monthly_cents": {
            "type": "integer",
            "description": "Monthly rent on this term, in US cents."
          },
          "total_cents": {
            "type": "integer",
            "description": "monthly_cents times months: the rent over one term."
          },
          "floor_cents": {
            "type": "integer",
            "description": "The lowest monthly rent a renewal on this term can be set to."
          },
          "ceiling_cents": {
            "type": "integer",
            "description": "The highest monthly rent a renewal on this term can be set to."
          }
        }
      },
      "Name": {
        "type": "object",
        "description": "A name as the catalog shows it. Names are rented, never sold.",
        "required": [
          "host",
          "status",
          "monthly_cents",
          "floor_cents",
          "ceiling_cents",
          "terms",
          "url",
          "rent_url",
          "available_on",
          "terms_version"
        ],
        "properties": {
          "host": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "listed",
              "rented"
            ],
            "description": "listed: for rent now. rented: someone holds it; available_on is when its current term ends."
          },
          "monthly_cents": {
            "type": "integer",
            "description": "The listed rate: monthly rent on the 12 month term, in US cents."
          },
          "floor_cents": {
            "type": "integer",
            "description": "Renewal floor on the 12 month term."
          },
          "ceiling_cents": {
            "type": "integer",
            "description": "Renewal ceiling on the 12 month term."
          },
          "terms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NameTerm"
            },
            "description": "Every term on the menu, longest first."
          },
          "url": {
            "type": "string",
            "description": "The name's page."
          },
          "rent_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where a person starts renting it. Null when the name is rented. Renting needs a person: sign in, an identity check, a signature and a card."
          },
          "available_on": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "For a rented name, when its current term ends. The lease renews on its own unless it is ended, so the name is free then only if it does not renew. Null for a listed name."
          },
          "terms_version": {
            "type": "string",
            "description": "The version of the lease text a renter signs today."
          }
        }
      },
      "Lease": {
        "type": "object",
        "required": [
          "id",
          "host",
          "status",
          "monthly_cents",
          "term_months"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "host": {
            "type": "string",
            "description": "The rented name."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "signed",
              "active",
              "past_due",
              "suspended",
              "renewal_offered",
              "ended",
              "terminated"
            ]
          },
          "monthly_cents": {
            "type": "integer",
            "description": "Monthly rent in US cents."
          },
          "term_months": {
            "type": "integer"
          },
          "starts_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ends_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Record": {
        "type": "object",
        "required": [
          "id",
          "type",
          "name",
          "content",
          "ttl",
          "protected"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The record id; use it as recordId in an update or delete op."
          },
          "lease_id": {
            "type": "string"
          },
          "name_id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Fully qualified name."
          },
          "content": {
            "type": "string"
          },
          "ttl": {
            "type": "integer"
          },
          "priority": {
            "type": [
              "integer",
              "null"
            ]
          },
          "protected": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "1 when changing it needs a person with a passkey."
          },
          "deleted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DesiredRecord": {
        "type": "object",
        "required": [
          "type",
          "name",
          "content"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "A",
              "AAAA",
              "CNAME",
              "TXT",
              "MX",
              "SRV",
              "CAA"
            ]
          },
          "name": {
            "type": "string",
            "description": "A short label like www, @ for the apex, or a full name inside the zone."
          },
          "content": {
            "type": "string",
            "description": "The record value. For SRV write \"priority weight port target\", for example \"10 5 5060 sip.example.com\"."
          },
          "ttl": {
            "type": "integer",
            "description": "1 for automatic, or 60 to 86400 seconds."
          },
          "priority": {
            "type": "integer",
            "description": "MX only. For SRV the priority is the first number of content; if it is also sent here, the two must match."
          }
        }
      },
      "NormalizedOp": {
        "description": "An op as policy read it: the op that was sent, with the full name it lands on and whether it is protected.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Op"
          },
          {
            "type": "object",
            "required": [
              "fqdn",
              "protected"
            ],
            "properties": {
              "fqdn": {
                "type": "string",
                "description": "The fully qualified name the op changes."
              },
              "protected": {
                "type": "boolean",
                "description": "True when this op alone makes the plan need a person."
              }
            }
          }
        ]
      },
      "Op": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "op",
              "record"
            ],
            "properties": {
              "op": {
                "const": "create"
              },
              "record": {
                "$ref": "#/components/schemas/DesiredRecord"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "op",
              "recordId",
              "record"
            ],
            "properties": {
              "op": {
                "const": "update"
              },
              "recordId": {
                "type": "string"
              },
              "record": {
                "$ref": "#/components/schemas/DesiredRecord"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "op",
              "recordId"
            ],
            "properties": {
              "op": {
                "const": "delete"
              },
              "recordId": {
                "type": "string"
              }
            }
          }
        ]
      },
      "Verdict": {
        "type": "object",
        "required": [
          "ok",
          "protected",
          "reasons",
          "ops"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "False when policy refuses the plan; reasons says why."
          },
          "protected": {
            "type": "boolean",
            "description": "True when applying needs a person with a passkey or an authenticator code: the plan touches an MX, CAA, SPF, DKIM or DMARC record, a wildcard record or a TXT at the apex, or it has more than 10 changes. A token cannot apply it."
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Why policy refused the plan, one sentence each. Empty when ok is true."
          },
          "ops": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NormalizedOp"
            }
          },
          "protected_reasons": {
            "type": "array",
            "description": "Why the plan is protected, when it is.",
            "items": {
              "type": "object",
              "required": [
                "code",
                "message"
              ],
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "protected_record",
                    "plan_too_large"
                  ]
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Plan": {
        "type": "object",
        "required": [
          "id",
          "status",
          "protected",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "proposed",
              "pending_review",
              "applied",
              "expired",
              "rejected",
              "failed"
            ],
            "description": "proposed: waiting to be applied. pending_review: held for a person on our side, nothing written yet. applied: live. expired: not applied in time. rejected: turned down at review. failed: the write failed and was rolled back."
          },
          "protected": {
            "type": "boolean"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "A proposed plan lives 15 minutes."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "On a plan read: why the plan stands where it does, when there is something to say."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "applied_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Change": {
        "type": "object",
        "description": "One applied DNS change, with the before and after state as JSON strings.",
        "properties": {
          "id": {
            "type": "string"
          },
          "op": {
            "type": "string"
          },
          "record_type": {
            "type": "string"
          },
          "record_name": {
            "type": "string"
          },
          "before_json": {
            "type": [
              "string",
              "null"
            ]
          },
          "after_json": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}