{
  "openapi": "3.0.3",
  "info": {
    "title": "noobtunnel automation API",
    "version": "1.0.0",
    "description": "Common automation endpoints for this noobtunnel installation. This is not a reference for internal cluster, agent transport, billing, or global administration APIs.\n\n### Authentication\nCreate a token in Settings → Account access. Click Authorize and enter the token itself (ntapi_…), without the Bearer prefix. Requests use Authorization: Bearer TOKEN. The interactive client does not use dashboard cookies. Mutations affect live data.\n\n### Token permissions\n- read: general mesh status and configuration, without enrollment credentials.\n- dns: domains, authoritative records and DNS providers. DNS-only tokens can be shared with another account for noobtunnel DNS automation; they cover every zone their owner may manage.\n- resources: published services.\n- agents: agent management, including enrollment credentials.\n- full: all API operations allowed by the owner's current role, including API-token management. All four general permissions together are NOT full access.\n\nFull access satisfies every permission requirement below. Tokens never exceed the owner's current role or account limits. Limited tokens cannot create or revoke API tokens. Existing tokens retain their previous full access. Unknown operations are denied to limited tokens.\n\n### Errors and updates\nFailures normally return {\"error\":\"message\"}. 401 means invalid, revoked, disabled-account or unverified-account credentials; 403 means insufficient role or token permission. 400 indicates invalid input and may include stale revisions. 404 includes objects not visible to the account. 409 indicates a conflict; 429 is rate limiting; 503 indicates temporary unavailability. DNS record updates require the revision from GET records and replace the entire record list. Resource PATCH is a replacement-style update: send the full desired resource configuration, including protocol and targets, not just changed fields."
  },
  "servers": [{"url": "/", "description": "This noobtunnel instance"}],
  "security": [{"bearerAuth": []}],
  "tags": [
    {"name": "Status", "description": "General status and publishing locations."},
    {"name": "Agents", "description": "Manage enrolled machines and enrollment credentials."},
    {"name": "Resources", "description": "Published services and backends."},
    {"name": "DNS", "description": "Domains, authoritative records and DNS automation."},
    {"name": "Tokens", "description": "Manage your own tokens; requires full access."}
  ],
  "paths": {
    "/api/state": {
      "get": {"tags": ["Status"], "operationId": "getState", "summary": "Read mesh status", "description": "Permission: any general permission. Snapshot fields and credentials are filtered by token permissions and account ownership.", "responses": {"200": {"description": "Mesh snapshot: agents, resources, domains, exitNodes, dnsProviders, pops, summary, and permission-filtered settings.", "content": {"application/json": {"schema": {"type": "object"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}}}
    },
    "/api/pops": {
      "get": {"tags": ["Status"], "operationId": "listPOPs", "summary": "List publishing POPs", "description": "Permission: any general permission.", "responses": {"200": {"description": "Object containing a pops array with IDs, names, addresses, countries and online status.", "content": {"application/json": {"schema": {"type": "object"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}}}
    },
    "/api/exitnodes": {
      "get": {"tags": ["Status"], "operationId": "listExitNodes", "summary": "List publishing exit nodes", "description": "Permission: any general permission. Use an exit node ID when publishing a resource. Limited tokens do not receive setup credentials.", "responses": {"200": {"description": "Object containing exitNodes and localAddresses arrays.", "content": {"application/json": {"schema": {"type": "object"}}}}, "401": {"$ref": "#/components/responses/Unauthorized"}}}
    },
    "/api/agents": {
      "get": {"tags": ["Agents"], "operationId": "listAgents", "summary": "List your agents", "description": "Permission: read or agents. Returns a mesh snapshot, not a bare array. Enrollment credentials require agents permission.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}}},
      "post": {"tags": ["Agents"], "operationId": "createAgent", "summary": "Create an agent enrollment", "description": "Permission: agents. Returns an enrollment token and installation command. Treat these as secrets. Enrollments normally expire after 15 minutes.", "parameters": [{"name": "method", "in": "query", "schema": {"type": "string", "enum": ["service", "docker", "windows"], "default": "service"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AgentCreate"}, "example": {"name": "automation-agent"}}}}, "responses": {"200": {"description": "Object with agent and installCommand. The agent includes its id and token.", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "503": {"$ref": "#/components/responses/Unavailable"}}}
    },
    "/api/agents/{id}": {
      "parameters": [{"$ref": "#/components/parameters/AgentID"}],
      "get": {"tags": ["Agents"], "operationId": "getAgent", "summary": "Read an agent and enrollment details", "description": "Permission: agents. Returns agent and installCommand, including enrollment credentials.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "patch": {"tags": ["Agents"], "operationId": "updateAgent", "summary": "Update agent settings", "description": "Permission: agents. Only supplied fields change. Disabling an agent disconnects it.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AgentUpdate"}, "example": {"name": "renamed-agent"}}}}, "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "delete": {"tags": ["Agents"], "operationId": "deleteAgent", "summary": "Remove an agent", "description": "Permission: agents. Removes the agent and disconnects its session.", "responses": {"200": {"$ref": "#/components/responses/OK"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/agents/{id}/rotate": {
      "parameters": [{"$ref": "#/components/parameters/AgentID"}],
      "post": {"tags": ["Agents"], "operationId": "rotateAgentEnrollment", "summary": "Rotate agent enrollment credentials", "description": "Permission: agents. Invalidates previous enrollment credentials and requires re-enrollment.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/agents/{id}/ping": {
      "parameters": [{"$ref": "#/components/parameters/AgentID"}],
      "post": {"tags": ["Agents"], "operationId": "pingAgent", "summary": "Check agent connectivity", "description": "Permission: agents. A connectivity failure still returns HTTP 200 with online:false and an error.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/resources": {
      "get": {"tags": ["Resources"], "operationId": "listResources", "summary": "List your published services", "description": "Permission: read or resources. Returns resources and visible domains arrays.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "401": {"$ref": "#/components/responses/Unauthorized"}, "403": {"$ref": "#/components/responses/Forbidden"}}},
      "post": {"tags": ["Resources"], "operationId": "createResource", "summary": "Publish a service", "description": "Permission: resources. HTTP/HTTPS require an available domain. Regular accounts target their own enrolled agent; agentId:0 is reserved for eligible administrators. Account quotas and publication validation still apply.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ResourceInput"}, "example": {"name": "my-web-service", "protocol": "https", "domain": "app.example.com", "enabled": true, "targets": [{"agentId": 1, "host": "127.0.0.1", "port": 8080}]}}}}, "responses": {"200": {"description": "Object containing resource, including its id, revision, listenPort and public endpoint.", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "409": {"$ref": "#/components/responses/Conflict"}, "503": {"$ref": "#/components/responses/Unavailable"}}}
    },
    "/api/resources/{id}": {
      "parameters": [{"$ref": "#/components/parameters/ResourceID"}],
      "get": {"tags": ["Resources"], "operationId": "getResource", "summary": "Read a published service", "description": "Permission: read or resources. Returns a resource object. Response target.address is not the write payload: writes use host and port separately.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "patch": {"tags": ["Resources"], "operationId": "updateResource", "summary": "Replace service configuration", "description": "Permission: resources. Send the complete desired configuration, including name, protocol and targets. This is not a partial-field PATCH. Use the current resource revision in If-Match to protect against concurrent edits.", "parameters": [{"name": "If-Match", "in": "header", "description": "Current resource revision returned by GET.", "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ResourceInput"}}}}, "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}, "409": {"$ref": "#/components/responses/Conflict"}}},
      "delete": {"tags": ["Resources"], "operationId": "deleteResource", "summary": "Remove a published service", "description": "Permission: resources. Stops publishing the service and queues applicable automatic DNS cleanup.", "responses": {"200": {"$ref": "#/components/responses/OK"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/domains": {
      "get": {"tags": ["DNS"], "operationId": "listDomains", "summary": "List visible domains", "description": "Permission: read or dns. Returns a domains array. Verification credentials require dns permission.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}}},
      "post": {"tags": ["DNS"], "operationId": "createDomain", "summary": "Register a domain or hosted zone", "description": "Permission: dns. For hosted:true the response contains hostname and nameservers; complete nameserver delegation and verify before publishing. Otherwise the response contains hostname, verificationName and verificationToken. With a noobtunnel DNS provider, its owner's token can authorize an exact hostname in a verified hosted zone without a separate TXT challenge.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DomainCreate"}, "example": {"hostname": "example.com", "hosted": true}}}}, "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}}}
    },
    "/api/domains/{hostname}": {
      "parameters": [{"$ref": "#/components/parameters/Hostname"}],
      "patch": {"tags": ["DNS"], "operationId": "updateDomain", "summary": "Configure domain automation", "description": "Permission: dns. Only supplied fields change. An empty providerId removes automation; attaching a noobtunnel provider validates its stored token and zone access.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DomainUpdate"}}}}, "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "delete": {"tags": ["DNS"], "operationId": "deleteDomain", "summary": "Remove a domain", "description": "Permission: dns. Domains used by resources may be protected from deletion.", "responses": {"200": {"$ref": "#/components/responses/OK"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/domains/{hostname}/records": {
      "parameters": [{"$ref": "#/components/parameters/Hostname"}],
      "get": {"tags": ["DNS"], "operationId": "getDNSRecords", "summary": "Read hosted DNS records and revision", "description": "Permission: dns. Only authoritative hosted zones support this endpoint. Returns records, revision, canGeoDNS and operator.", "responses": {"200": {"description": "Records and concurrency revision.", "content": {"application/json": {"schema": {"type": "object", "properties": {"records": {"type": "array", "items": {"$ref": "#/components/schemas/DNSRecord"}}, "revision": {"type": "string"}, "canGeoDNS": {"type": "boolean"}, "operator": {"type": "boolean"}}}}}}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "put": {"tags": ["DNS"], "operationId": "replaceDNSRecords", "summary": "Replace all hosted DNS records", "description": "Permission: dns. First GET the current records and revision, merge your changes while retaining unrelated records, then submit the complete list with that revision. Managed-service records remain protected. A stale revision is rejected; fetch again and retry your merge.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "required": ["records", "revision"], "properties": {"records": {"type": "array", "items": {"$ref": "#/components/schemas/DNSRecord"}}, "revision": {"type": "string"}}}, "example": {"revision": "COPY_FROM_GET_RECORDS", "records": [{"name": "app", "type": "A", "value": "192.0.2.10", "ttl": 300}]}}}}, "responses": {"200": {"$ref": "#/components/responses/OK"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/domains/{hostname}/verify": {
      "parameters": [{"$ref": "#/components/parameters/Hostname"}],
      "post": {"tags": ["DNS"], "operationId": "verifyDomain", "summary": "Verify delegation or domain ownership", "description": "Permission: dns. Hosted zones check nameserver delegation. External domains check the requested DNS TXT challenge.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/domains/{hostname}/sync": {
      "parameters": [{"$ref": "#/components/parameters/Hostname"}],
      "post": {"tags": ["DNS"], "operationId": "syncDomain", "summary": "Request domain DNS synchronization", "description": "Permission: dns. Returns domains with sync status. Check synced and lastError: HTTP 200 alone does not mean provider publication succeeded.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/dns/providers": {
      "get": {"tags": ["DNS"], "operationId": "listDNSProviders", "summary": "List DNS automation providers", "description": "Permission: read or dns. Returns a providers array. Provider secrets are never returned.", "responses": {"200": {"$ref": "#/components/responses/Data"}, "403": {"$ref": "#/components/responses/Forbidden"}}},
      "post": {"tags": ["DNS"], "operationId": "createDNSProvider", "summary": "Add a DNS automation provider", "description": "Permission: dns. A token is required. For noobtunnel use a token from the authoritative zone owner's account with dns permission (or full access) on this installation. Returned provider metadata never includes its secret.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DNSProviderCreate"}, "example": {"name": "Shared noobtunnel DNS", "kind": "noobtunnel", "token": "REPLACE_WITH_ZONE_OWNER_DNS_TOKEN", "enabled": true}}}}, "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}}}
    },
    "/api/dns/providers/{id}": {
      "parameters": [{"$ref": "#/components/parameters/ProviderID"}],
      "patch": {"tags": ["DNS"], "operationId": "updateDNSProvider", "summary": "Update DNS provider configuration", "description": "Permission: dns. Send the desired provider configuration. An omitted/empty token retains the stored secret when the provider kind is unchanged. Use this to replace a revoked shared DNS token.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DNSProviderUpdate"}}}}, "responses": {"200": {"$ref": "#/components/responses/Data"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}},
      "delete": {"tags": ["DNS"], "operationId": "deleteDNSProvider", "summary": "Remove a DNS provider", "description": "Permission: dns. Providers still used by domains or pending DNS jobs may be protected from deletion.", "responses": {"200": {"$ref": "#/components/responses/OK"}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    },
    "/api/tokens": {
      "get": {"tags": ["Tokens"], "operationId": "listTokens", "summary": "List your API tokens", "description": "Permission: full. Returns only the current owner's tokens and no token secrets.", "responses": {"200": {"description": "Token metadata.", "content": {"application/json": {"schema": {"type": "object", "properties": {"tokens": {"type": "array", "items": {"$ref": "#/components/schemas/APIToken"}}}}}}}, "403": {"$ref": "#/components/responses/Forbidden"}}},
      "post": {"tags": ["Tokens"], "operationId": "createToken", "summary": "Create an API token", "description": "Permission: full. Plaintext is returned only once. Omitting permissions defaults to read; an empty array is rejected. The new token remains bounded by the owner's current account role.", "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 200}, "permissions": {"type": "array", "minItems": 1, "items": {"$ref": "#/components/schemas/Permission"}, "default": ["read"]}}}, "example": {"name": "dns-automation", "permissions": ["dns"]}}}}, "responses": {"200": {"description": "Copy the plaintext token now; it cannot be retrieved later.", "content": {"application/json": {"schema": {"type": "object", "properties": {"token": {"type": "string"}, "meta": {"$ref": "#/components/schemas/APIToken"}}}}}}, "400": {"$ref": "#/components/responses/BadRequest"}, "403": {"$ref": "#/components/responses/Forbidden"}}}
    },
    "/api/tokens/{id}": {
      "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Token metadata ID, not the plaintext token."}],
      "delete": {"tags": ["Tokens"], "operationId": "revokeToken", "summary": "Revoke one of your tokens", "description": "Permission: full. Revocation stops subsequent API and noobtunnel DNS operations; it does not automatically delete existing DNS records.", "responses": {"200": {"$ref": "#/components/responses/OK"}, "403": {"$ref": "#/components/responses/Forbidden"}, "404": {"$ref": "#/components/responses/NotFound"}}}
    }
  },
  "components": {
    "securitySchemes": {"bearerAuth": {"type": "http", "scheme": "bearer", "bearerFormat": "ntapi_…", "description": "Paste your API token only, without the Bearer prefix. Choose the permissions required by the operation."}},
    "parameters": {
      "AgentID": {"name": "id", "in": "path", "required": true, "schema": {"type": "integer", "minimum": 1}, "description": "Agent ID."},
      "ResourceID": {"name": "id", "in": "path", "required": true, "schema": {"type": "integer", "minimum": 1}, "description": "Resource ID."},
      "ProviderID": {"name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "DNS provider ID."},
      "Hostname": {"name": "hostname", "in": "path", "required": true, "schema": {"type": "string"}, "example": "example.com", "description": "Registered domain/zone hostname, URL-encoded when needed."}
    },
    "responses": {
      "Data": {"description": "Operation result. Fields depend on the endpoint, as described above.", "content": {"application/json": {"schema": {"type": "object"}}}},
      "OK": {"description": "Operation completed.", "content": {"application/json": {"schema": {"type": "object", "properties": {"ok": {"type": "boolean"}}}}}},
      "BadRequest": {"description": "Invalid input, validation failure, or stale record revision.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Unauthorized": {"description": "Missing, invalid, revoked, or inactive-account token.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Forbidden": {"description": "Insufficient token permission or account role.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "NotFound": {"description": "Object does not exist or is not visible to this account.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Conflict": {"description": "Concurrent edit, quota/admission conflict, or conflicting state.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
      "Unavailable": {"description": "Temporarily unavailable; inspect the error and retry appropriately.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
    },
    "schemas": {
      "Error": {"type": "object", "required": ["error"], "properties": {"error": {"type": "string"}}},
      "Permission": {"type": "string", "enum": ["read", "dns", "resources", "agents", "full"]},
      "APIToken": {"type": "object", "properties": {"id": {"type": "string"}, "userId": {"type": "string"}, "name": {"type": "string"}, "role": {"type": "string", "description": "Owner's current account role."}, "createdAt": {"type": "string", "format": "date-time"}, "permissions": {"type": "array", "nullable": true, "items": {"$ref": "#/components/schemas/Permission"}, "description": "Null or absent means legacy full access. An explicit full permission also grants full access."}}},
      "AgentCreate": {"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}, "meshDns": {"type": "string", "description": "Optional short mesh DNS label."}, "advertise": {"type": "array", "items": {"type": "string"}, "description": "Advertised network CIDRs, subject to tenant route validation."}, "advertiseAll": {"type": "boolean", "default": false}}},
      "AgentUpdate": {"type": "object", "properties": {"name": {"type": "string"}, "meshDns": {"type": "string"}, "advertise": {"type": "array", "items": {"type": "string"}}, "advertiseAll": {"type": "boolean"}, "enabled": {"type": "boolean"}, "notes": {"type": "string"}}},
      "TargetInput": {"type": "object", "required": ["agentId", "host", "port"], "properties": {"agentId": {"type": "integer", "minimum": 0, "description": "ID of your enrolled agent. Zero is control-node-local and administrator-only."}, "host": {"type": "string", "example": "127.0.0.1"}, "port": {"type": "integer", "minimum": 1, "maximum": 65535, "example": 8080}, "enabled": {"type": "boolean", "default": true}}},
      "ResourceInput": {"type": "object", "required": ["name", "protocol", "targets"], "properties": {
        "name": {"type": "string"}, "protocol": {"type": "string", "enum": ["http", "https", "https-passthrough", "tcp", "udp"]},
        "targets": {"type": "array", "minItems": 1, "items": {"$ref": "#/components/schemas/TargetInput"}},
        "strategy": {"type": "string", "description": "Backend load-balancing strategy; omit to use the default."},
        "domain": {"type": "string", "description": "Hostname for HTTP/HTTPS; must be available to your account."},
        "exitNodeId": {"type": "string", "description": "Publishing location from GET /api/exitnodes."},
        "exitNodeIds": {"type": "array", "items": {"type": "string"}, "description": "Multiple publishing locations where supported."},
        "allPops": {"type": "boolean", "description": "Publish on every available POP, automatically including new POPs as they join. Requires Admin or Pro."},
        "listenPort": {"type": "integer", "minimum": 0, "maximum": 65535, "description": "Public port; zero requests automatic allocation where supported."},
        "enabled": {"type": "boolean", "default": true}, "proxyProtocol": {"type": "string", "enum": ["", "v1", "v2"]},
        "rules": {"type": "array", "items": {"$ref": "#/components/schemas/AccessRule"}, "description": "Ordered access rules. Preserve existing rules when updating a resource."},
        "srv": {"type": "object", "nullable": true, "properties": {"service": {"type": "string"}, "protocol": {"type": "string"}, "priority": {"type": "integer"}, "weight": {"type": "integer"}}, "description": "Optional SRV publication for eligible TCP/UDP services. Requires a compatible DNS provider."},
        "uncapped": {"type": "boolean", "description": "Administrator-only resource limits."},
        "monthlyQuotaBytes": {"type": "integer", "minimum": 0}, "monthlyRequestQuota": {"type": "integer", "minimum": 0},
        "sustainedBps": {"type": "integer", "minimum": 0}, "burstBps": {"type": "integer", "minimum": 0},
        "peakBps": {"type": "integer", "minimum": 0}, "overQuotaBps": {"type": "integer", "minimum": 0},
        "identity": {"type": "boolean"}, "identityMode": {"type": "string"}, "identityEmails": {"type": "array", "items": {"type": "string", "format": "email"}},
        "blockExploits": {"type": "boolean"}, "blockHighRiskIps": {"type": "boolean"}, "botCheck": {"type": "string"},
        "staticCache": {"type": "boolean"}, "cacheHtml": {"type": "boolean"}, "websockets": {"type": "boolean", "default": true}
      }},
      "AccessRule": {"type": "object", "required": ["field", "operator", "values", "action"], "properties": {"field": {"type": "string", "enum": ["country", "ip", "host", "path", "account"]}, "operator": {"type": "string", "enum": ["is", "is-not", "contains", "starts-with", "ends-with", "matches"], "description": "Available operators depend on the field; countries only support exact comparisons."}, "values": {"type": "array", "items": {"type": "string"}}, "action": {"type": "string", "enum": ["allow", "block"]}}},
      "DomainCreate": {"type": "object", "required": ["hostname"], "properties": {"hostname": {"type": "string", "example": "example.com"}, "hosted": {"type": "boolean", "default": false}, "delegation": {"type": "string", "enum": ["domain", "subdomain"], "description": "For hosted zones: whole registrable domain or explicitly delegated subdomain."}, "providerId": {"type": "string"}, "perSubdomainRecords": {"type": "boolean"}, "exitNodeId": {"type": "string"}, "publicPool": {"type": "boolean", "description": "Administrator-only."}}},
      "DomainUpdate": {"type": "object", "properties": {"providerId": {"type": "string"}, "perSubdomainRecords": {"type": "boolean"}, "exitNodeId": {"type": "string"}, "publicPool": {"type": "boolean", "description": "Administrator-only."}}},
      "DNSRecord": {"type": "object", "required": ["name", "type", "value", "ttl"], "properties": {"name": {"type": "string", "description": "Relative record name, @ for the apex, or a fully qualified name in this zone.", "example": "app"}, "type": {"type": "string", "enum": ["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "CAA"]}, "value": {"type": "string", "description": "Record value. MX uses priority and host; SRV uses priority, weight, port and host.", "example": "192.0.2.10"}, "ttl": {"type": "integer", "minimum": 0, "default": 300}, "countries": {"type": "array", "items": {"type": "string"}, "description": "ISO country codes; requires GeoDNS eligibility."}, "geoAuto": {"type": "boolean"}, "balance": {"type": "boolean"}, "weight": {"type": "integer"}, "healthPort": {"type": "integer", "minimum": 0, "maximum": 65535}}},
      "DNSProviderCreate": {"allOf": [{"$ref": "#/components/schemas/DNSProviderUpdate"}, {"type": "object", "required": ["name", "kind", "token"]}]},
      "DNSProviderUpdate": {"type": "object", "properties": {"name": {"type": "string"}, "kind": {"type": "string", "enum": ["noobtunnel", "cloudflare", "edge"]}, "token": {"type": "string", "format": "password", "writeOnly": true, "description": "Provider credential. For noobtunnel: zone owner's API token with dns permission."}, "zone": {"type": "string", "description": "Provider zone hint where required by the selected provider."}, "enabled": {"type": "boolean"}}}
    }
  }
}
