{
  "openapi": "3.1.0",
  "info": {
    "title": "Stacktree API",
    "version": "1.0.0",
    "summary": "Agent-first private-by-default HTML hosting.",
    "description": "Publish, manage, and share static HTML sites from AI agents. Supports anonymous publishing for one-shot artefacts, API-key auth for long-lived agent jobs, and OAuth 2.1 with Dynamic Client Registration for in-product agent connectors (claude.ai, ChatGPT custom connectors, etc.).  See /llms.txt for the agent-readable overview, and /docs for human docs.",
    "contact": {
      "name": "Stacktree",
      "url": "https://stacktr.ee",
      "email": "gm@stacktr.ee"
    },
    "license": {
      "name": "Proprietary"
    },
    "termsOfService": "https://stacktr.ee/terms",
    "x-guidance": "Stacktree turns an HTML file into a private, shareable link. Anonymous: POST /sites with a multipart `file` field to get a private 24h URL, no auth. Persistent: pay $1 once over x402 at POST /provision to receive an stk_live_ API key, then POST /sites with `Authorization: Bearer <key>` for sites that do not expire. Each response returns the private URL. Gate a site with a password or email domain, set an expiry, or attach your own domain via the /sites and /custom-domains routes."
  },
  "servers": [
    {
      "url": "https://api.stacktr.ee",
      "description": "Production API host"
    }
  ],
  "externalDocs": {
    "description": "Human-readable docs",
    "url": "https://stacktr.ee/docs"
  },
  "x-service-info": {
    "categories": [
      "web",
      "storage"
    ],
    "docs": {
      "homepage": "https://stacktr.ee",
      "apiReference": "https://api.stacktr.ee/openapi.json",
      "llms": "https://stacktr.ee/llms.txt"
    }
  },
  "components": {
    "securitySchemes": {
      "bearerApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API key (stk_live_…)",
        "description": "Long-lived API key, created from app.stacktr.ee or via the device-code flow. Pass as `Authorization: Bearer stk_live_…`."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "OAuth 2.1 with PKCE + Dynamic Client Registration (RFC 7591). Used by claude.ai connectors and other agent runtimes.",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://api.stacktr.ee/oauth/authorize",
            "tokenUrl": "https://api.stacktr.ee/oauth/token",
            "refreshUrl": "https://api.stacktr.ee/oauth/token",
            "scopes": {
              "sites:read": "List and read site metadata.",
              "sites:write": "Create, update, and delete sites."
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable error code."
          },
          "error_description": {
            "type": "string",
            "description": "Human-readable detail (optional)."
          }
        },
        "example": {
          "error": "auth required"
        }
      },
      "Site": {
        "type": "object",
        "required": [
          "id",
          "visibility",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable site identifier."
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "description": "Public subdomain slug, if the site is public."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "unlisted",
              "public",
              "burn"
            ],
            "description": "Privacy mode."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical URL to view the site."
          },
          "created_at": {
            "type": "integer",
            "description": "Unix epoch seconds."
          },
          "updated_at": {
            "type": "integer",
            "description": "Unix epoch seconds."
          },
          "expires_at": {
            "type": "integer",
            "nullable": true,
            "description": "Unix epoch seconds; null = no expiry."
          },
          "byte_size": {
            "type": "integer",
            "description": "Total content size in bytes."
          },
          "email_gate": {
            "type": "object",
            "nullable": true,
            "properties": {
              "domain": {
                "type": "string"
              },
              "required": {
                "type": "boolean"
              }
            }
          }
        }
      },
      "SiteUpload": {
        "type": "object",
        "description": "Multipart form body for `POST /sites` and `PUT /sites/{idOrSlug}`. Send `file` as a single .html/.htm/.md document or a .zip with an index.html at the root. All other fields are optional string-encoded knobs.",
        "required": [
          "file"
        ],
        "properties": {
          "file": {
            "type": "string",
            "format": "binary",
            "description": "A single .html, .htm, or .md file, or a .zip containing index.html at the root."
          },
          "slug": {
            "type": "string",
            "description": "Requested public subdomain (makes the site public). Optional; otherwise the site is unlisted with an unguessable token."
          },
          "password": {
            "type": "string",
            "description": "Optional password gate."
          },
          "expires_in_hours": {
            "type": "string",
            "description": "TTL in hours. Anonymous uploads cap at 24h."
          },
          "burn_after_read": {
            "type": "string",
            "description": "\"true\" to delete the site after its first view."
          },
          "e2e": {
            "type": "string",
            "description": "\"true\" if the payload is already AES-GCM ciphertext (end-to-end encrypted; the server stores ciphertext only)."
          },
          "csp_strict": {
            "type": "string",
            "description": "\"true\" to apply a strict Content-Security-Policy."
          },
          "agentation": {
            "type": "string",
            "description": "\"true\" to enable the Agentation annotation toolbar on the published page."
          }
        }
      },
      "ShareToken": {
        "type": "object",
        "required": [
          "token",
          "site_id",
          "created_at"
        ],
        "properties": {
          "token": {
            "type": "string"
          },
          "site_id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL with ?t=<token> embedded; bypasses any password gate."
          },
          "created_at": {
            "type": "integer"
          },
          "expires_at": {
            "type": "integer",
            "nullable": true
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "CustomDomain": {
        "type": "object",
        "required": [
          "hostname",
          "site_id",
          "status"
        ],
        "properties": {
          "hostname": {
            "type": "string",
            "example": "docs.example.com"
          },
          "site_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "verified",
              "failed"
            ]
          },
          "cname_target": {
            "type": "string",
            "description": "CNAME value the user must add to their DNS."
          },
          "verified_at": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": [
          "id",
          "name",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string",
            "description": "First few chars of the key (for display)."
          },
          "created_at": {
            "type": "integer"
          },
          "last_used": {
            "type": "integer",
            "nullable": true
          },
          "key": {
            "type": "string",
            "description": "Full API key. Only present on creation responses."
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid credentials.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "bearerApiKey": []
    },
    {
      "oauth2": [
        "sites:write",
        "sites:read"
      ]
    }
  ],
  "tags": [
    {
      "name": "Sites",
      "description": "Publish, update, and delete static HTML sites."
    },
    {
      "name": "Share tokens",
      "description": "Mint signed URLs that bypass per-site password gates."
    },
    {
      "name": "Custom domains",
      "description": "Bind a user-owned hostname to a Stacktree site."
    },
    {
      "name": "API keys",
      "description": "Long-lived credential management."
    },
    {
      "name": "OAuth",
      "description": "OAuth 2.1 + Dynamic Client Registration (RFC 7591). See /.well-known/oauth-authorization-server."
    },
    {
      "name": "MCP",
      "description": "Model Context Protocol surface — streamable-HTTP transport at /mcp."
    }
  ],
  "paths": {
    "/provision": {
      "post": {
        "tags": [
          "Provision"
        ],
        "summary": "Pay to provision a persistent API key (x402).",
        "description": "Agent-native, no human in the loop. POST with no payment to receive a 402 with x402 payment requirements (PAYMENT-REQUIRED header for v2; the accepts list in the body for v1). Sign the EIP-3009 USDC authorization and retry; on settlement you receive a one-time stk_live_ API key. Live on Base mainnet via the Coinbase CDP facilitator. Other agentic rails (MPP, ACP, AP2) are advertised in the rail menu when enabled.",
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "1.00"
          },
          "protocols": [
            {
              "x402": {}
            },
            {
              "mpp": {
                "method": "evm",
                "intent": "charge",
                "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
              }
            }
          ],
          "intent": "charge",
          "method": "evm",
          "amount": "1000000",
          "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "offers": [
            {
              "amount": "1000000",
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Provision a persistent Stacktree API key (returned once in the body).",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "extensions": {
            "bazaar": {
              "serviceName": "Stacktree",
              "description": "Host an HTML page on a private, unguessable link. POST your HTML and get back a URL an agent can share, with no account and no human in the loop. Good for when an agent produces a report, plan, dashboard, or page and a chat message is not enough.",
              "tags": [
                "html",
                "hosting",
                "publishing",
                "private",
                "static-site",
                "share-link",
                "agents"
              ],
              "info": {
                "input": {
                  "type": "http",
                  "method": "POST",
                  "bodyType": "json",
                  "body": {
                    "filename": "index.html",
                    "expires_in_hours": "never"
                  }
                },
                "output": {
                  "type": "json",
                  "example": {
                    "ok": true,
                    "api_key": "stk_live_…",
                    "usage": "Authorization: Bearer stk_live_…"
                  }
                }
              },
              "schema": {
                "$schema": "https://json-schema.org/draft/2020-12/schema",
                "type": "object",
                "properties": {
                  "input": {
                    "type": "object",
                    "properties": {
                      "body": {
                        "type": "object",
                        "description": "Optional JSON body. Payment is carried in the PAYMENT-SIGNATURE header (x402 v2), so no body is required. Fields apply only to the token-carried rails (ACP / AP2).",
                        "properties": {
                          "acp_shared_payment_token": {
                            "type": "string",
                            "description": "ACP rail: a Stripe Shared Payment Token (spt_...)."
                          },
                          "ap2_mandate": {
                            "type": "object",
                            "description": "AP2 rail: an AP2 mandate object."
                          }
                        }
                      }
                    }
                  },
                  "output": {
                    "type": "object",
                    "properties": {
                      "example": {
                        "type": "object",
                        "description": "Returned once payment settles.",
                        "properties": {
                          "ok": {
                            "type": "boolean"
                          },
                          "protocol": {
                            "type": "string"
                          },
                          "api_key": {
                            "type": "string",
                            "description": "The provisioned stk_live_ API key, shown once."
                          },
                          "prefix": {
                            "type": "string"
                          },
                          "usage": {
                            "type": "string",
                            "description": "How to use the key, e.g. \"Authorization: Bearer stk_live_...\"."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "required": false,
          "description": "No body is required for the x402 rail — payment is carried in the PAYMENT-SIGNATURE header (x402 v2) or X-PAYMENT (v1). Body fields apply only to the token-carried rails (ACP / AP2).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "acp_shared_payment_token": {
                    "type": "string",
                    "description": "ACP rail: a Stripe Shared Payment Token (spt_...)."
                  },
                  "ap2_mandate": {
                    "type": "object",
                    "description": "AP2 rail: an AP2 mandate object (supports human-not-present)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Paid and provisioned. Returns the API key once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "protocol": {
                      "type": "string"
                    },
                    "api_key": {
                      "type": "string"
                    },
                    "prefix": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment Required. x402 requirements are in the PAYMENT-REQUIRED header (v2) and the response body (v1).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "502": {
            "description": "Facilitator verify/settle error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/publish": {
      "post": {
        "tags": [
          "Provision"
        ],
        "summary": "Pay-per-publish: POST HTML + payment, get back a permanent private link.",
        "description": "Agent-native, no human and no account. POST your HTML with no payment to receive a 402 carrying x402 (PAYMENT-REQUIRED header) and MPP-evm (WWW-Authenticate: Payment) requirements. Pay $0.50 in USDC on Base and retry with the same body; on settlement the page publishes to a permanent, unguessable link and you get back the URL plus a one-time claim token. The entry rung of the agentic ladder — vs $1 for a reusable key via /provision. Live on Base mainnet via the Coinbase CDP facilitator.",
        "security": [],
        "x-payment-info": {
          "price": {
            "mode": "fixed",
            "currency": "USD",
            "amount": "0.50"
          },
          "protocols": [
            {
              "x402": {}
            },
            {
              "mpp": {
                "method": "evm",
                "intent": "charge",
                "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
              }
            }
          ],
          "intent": "charge",
          "method": "evm",
          "amount": "500000",
          "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "offers": [
            {
              "amount": "500000",
              "currency": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
              "description": "Publish an HTML page to a permanent private link.",
              "intent": "charge",
              "method": "evm"
            }
          ],
          "extensions": {
            "bazaar": {
              "serviceName": "Stacktree — publish",
              "description": "Publish an HTML page and get back a permanent private link an agent can share. POST your HTML with payment; no account, no key, no human in the loop. Good for a report, plan, dashboard, or page an agent produces when a chat message is not enough.",
              "tags": [
                "html",
                "hosting",
                "publishing",
                "private",
                "static-site",
                "share-link",
                "agents"
              ],
              "info": {
                "input": {
                  "type": "http",
                  "method": "POST",
                  "bodyType": "json",
                  "body": {
                    "html": "<!doctype html>..."
                  }
                },
                "output": {
                  "type": "json",
                  "example": {
                    "url": "https://stacktr.ee/p/<token>/",
                    "claim_token": "<one-time-secret>"
                  }
                }
              },
              "schema": {
                "$schema": "https://json-schema.org/draft/2020-12/schema",
                "type": "object",
                "properties": {
                  "input": {
                    "type": "object",
                    "properties": {
                      "body": {
                        "type": "object",
                        "description": "The page to publish. Send { \"html\": \"<!doctype html>...\" } (or a raw text/html body). Payment is carried in the PAYMENT-SIGNATURE header (x402 v2).",
                        "properties": {
                          "html": {
                            "type": "string",
                            "description": "The full HTML document to host."
                          },
                          "filename": {
                            "type": "string",
                            "description": "Optional logical filename; defaults to index.html."
                          }
                        },
                        "required": [
                          "html"
                        ]
                      }
                    }
                  },
                  "output": {
                    "type": "object",
                    "properties": {
                      "example": {
                        "type": "object",
                        "description": "Returned once payment settles and the page is live.",
                        "properties": {
                          "url": {
                            "type": "string",
                            "description": "The permanent private link to share."
                          },
                          "unlisted_token": {
                            "type": "string"
                          },
                          "claim_token": {
                            "type": "string",
                            "description": "One-time secret to adopt this page into an account later."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "description": "The page to publish. Payment is carried in the PAYMENT-SIGNATURE header (x402 v2) or Authorization: Payment (MPP-evm).",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "html"
                ],
                "properties": {
                  "html": {
                    "type": "string",
                    "description": "The full HTML document to host."
                  },
                  "filename": {
                    "type": "string",
                    "description": "Optional logical filename; defaults to index.html."
                  }
                }
              }
            },
            "text/html": {
              "schema": {
                "type": "string",
                "description": "The raw HTML document."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Paid and published. Returns the permanent URL + one-time claim token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "unlisted_token": {
                      "type": "string"
                    },
                    "claim_token": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "402": {
            "description": "Payment Required. x402 requirements in the PAYMENT-REQUIRED header; MPP-evm in WWW-Authenticate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "502": {
            "description": "Facilitator verify/settle error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/unlock": {
      "get": {
        "tags": [
          "Provision"
        ],
        "summary": "The à-la-carte unlock catalog (SKUs and prices).",
        "security": [],
        "responses": {
          "200": {
            "description": "Catalog of purchasable feature unlocks.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "unlocks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Provision"
        ],
        "summary": "Buy a feature unlock over x402 (make-permanent, custom domain, higher limits).",
        "description": "POST with the feature and no payment to receive a 402 carrying x402 requirements; sign the EIP-3009 USDC authorization and retry to activate the entitlement. Live on Base mainnet.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "oauth2": [
              "sites:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "feature",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The feature SKU to unlock (see GET /unlock for the catalog)."
          }
        ],
        "responses": {
          "200": {
            "description": "Unlock activated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "Payment Required (x402 requirements in the PAYMENT-REQUIRED header and body).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/pay/sessions": {
      "post": {
        "tags": [
          "Provision"
        ],
        "summary": "Create a scan-to-pay session (a human pays by card via QR when there is no wallet).",
        "description": "No wallet, no account required. Returns a short pay URL, a terminal-printable QR, and a poll URL. A human scans and pays by card; the agent polls and continues mid-task. Paying above the price leaves a prepaid balance future paid actions draw from silently.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "feature": {
                    "type": "string",
                    "description": "provision | topup | make_permanent | custom_domain | higher_limits"
                  },
                  "amount_minor": {
                    "type": "integer",
                    "description": "Optional amount in minor units; surplus becomes a prepaid balance."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    },
                    "qr": {
                      "type": "string"
                    },
                    "amount": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/pay/sessions/{code}/poll": {
      "parameters": [
        {
          "name": "code",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Provision"
        ],
        "summary": "Poll a pay session; returns the minted key (provision) or confirms the unlock once paid.",
        "responses": {
          "200": {
            "description": "Session status. Includes the stk_live_ API key once, on a completed provision.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/sites": {
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "List sites belonging to the authenticated principal.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "sites": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Site"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Publish a new site.",
        "description": "Send a multipart/form-data body with a `file` field (a single .html/.htm/.md document, or a .zip containing index.html at the root). Anonymous publishing works with no Authorization header from a direct client connection: the site gets a 24h TTL and is rate-limited per IP. Example: `curl -X POST https://api.stacktr.ee/sites -F file=@page.html`.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "oauth2": [
              "sites:write"
            ]
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/SiteUpload"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Published.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (e.g. not multipart/form-data, missing file, or anonymous upload without a direct client IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/sites/{idOrSlug}": {
      "parameters": [
        {
          "name": "idOrSlug",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Get a site by id or slug.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "put": {
        "tags": [
          "Sites"
        ],
        "summary": "Replace the site contents (full re-upload, same URL).",
        "description": "Multipart/form-data with a `file` field, same shape as POST. Keeps the existing URL while swapping the content. Example: `curl -X PUT https://api.stacktr.ee/sites/<id> -F file=@new.html -H \"Authorization: Bearer <key>\"`.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/SiteUpload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Replaced.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Sites"
        ],
        "summary": "Update site settings (visibility, slug, expiry, password, email gate).",
        "description": "JSON body. Only the fields you send are changed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "unlisted",
                      "public",
                      "burn"
                    ]
                  },
                  "password": {
                    "type": "string",
                    "nullable": true,
                    "description": "Set a password, or null to clear."
                  },
                  "expires_at": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Unix epoch seconds, or null for no expiry."
                  },
                  "email_gate_domain": {
                    "type": "string",
                    "nullable": true,
                    "description": "Require visitors to verify an email on this domain, or null to clear."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Sites"
        ],
        "summary": "Delete a site.",
        "responses": {
          "200": {
            "description": "Deleted."
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/sites/{idOrSlug}/content": {
      "parameters": [
        {
          "name": "idOrSlug",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Sites"
        ],
        "summary": "Read the exact stored HTML source of a site you own.",
        "description": "Returns index.html verbatim (not the rendered page or the text-stripped /raw view), so an agent can read a page, edit it, and PUT it back in place without losing CSS or inline charts. The read→edit→update loop behind \"personalise this template with your agent\". Owner-authed.",
        "responses": {
          "200": {
            "description": "The stored HTML.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/sites/{idOrSlug}/claim": {
      "parameters": [
        {
          "name": "idOrSlug",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Sites"
        ],
        "summary": "Claim an anonymous site into your account (make it permanent).",
        "description": "Adopt an anonymously-published site (no account, 24h TTL) into the authenticated account using the single-use claim_token returned at publish time. Clears the expiry so the site becomes permanent. Single-use, and only while the site is still unowned and unexpired.",
        "security": [
          {
            "bearerApiKey": []
          },
          {
            "oauth2": [
              "sites:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "claim_token"
                ],
                "properties": {
                  "claim_token": {
                    "type": "string",
                    "description": "The secret claim token from the publish response."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Invalid claim token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already claimed by another account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "Expired; can no longer be claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{idOrSlug}/share-tokens": {
      "parameters": [
        {
          "name": "idOrSlug",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Share tokens"
        ],
        "summary": "List share tokens for a site.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "tokens": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ShareToken"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Share tokens"
        ],
        "summary": "Mint a new share token.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string"
                  },
                  "expires_in": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareToken"
                }
              }
            }
          }
        }
      }
    },
    "/share-tokens/{tokenId}": {
      "parameters": [
        {
          "name": "tokenId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "tags": [
          "Share tokens"
        ],
        "summary": "Revoke a share token.",
        "responses": {
          "200": {
            "description": "Revoked."
          }
        }
      }
    },
    "/custom-domains": {
      "get": {
        "tags": [
          "Custom domains"
        ],
        "summary": "List custom domains.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "domains": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CustomDomain"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Custom domains"
        ],
        "summary": "Add a custom domain.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "hostname",
                  "site_id"
                ],
                "properties": {
                  "hostname": {
                    "type": "string"
                  },
                  "site_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomDomain"
                }
              }
            }
          }
        }
      }
    },
    "/custom-domains/{hostname}": {
      "parameters": [
        {
          "name": "hostname",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "patch": {
        "tags": [
          "Custom domains"
        ],
        "summary": "Update settings on a custom domain.",
        "responses": {
          "200": {
            "description": "Updated."
          }
        }
      },
      "delete": {
        "tags": [
          "Custom domains"
        ],
        "summary": "Remove a custom domain.",
        "responses": {
          "200": {
            "description": "Removed."
          }
        }
      }
    },
    "/custom-domains/{hostname}/verify": {
      "parameters": [
        {
          "name": "hostname",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Custom domains"
        ],
        "summary": "Verify domain ownership (checks CNAME).",
        "responses": {
          "200": {
            "description": "Verification result.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomDomain"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys": {
      "get": {
        "tags": [
          "API keys"
        ],
        "summary": "List API keys for the authenticated user.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      },
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Create a new API key. The full key value is only returned in this response.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKey"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "tags": [
          "API keys"
        ],
        "summary": "Revoke an API key.",
        "responses": {
          "200": {
            "description": "Revoked."
          }
        }
      }
    },
    "/.well-known/oauth-authorization-server": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "RFC 8414 discovery metadata.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/.well-known/oauth-protected-resource": {
      "get": {
        "tags": [
          "OAuth"
        ],
        "summary": "RFC 9728 protected-resource metadata.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/oauth/register": {
      "post": {
        "tags": [
          "OAuth"
        ],
        "summary": "RFC 7591 Dynamic Client Registration. Used by claude.ai connectors and other agent runtimes that discover Stacktree via /.well-known/.",
        "security": [],
        "responses": {
          "201": {
            "description": "Client registered."
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "tags": [
          "MCP"
        ],
        "summary": "Model Context Protocol over streamable HTTP. The endpoint exposes site-publishing tools to MCP-aware agents.",
        "description": "Authenticate with a Bearer access token (OAuth 2.1) or an API key. The /.well-known/mcp/server-card.json card describes the transport + capabilities.",
        "responses": {
          "200": {
            "description": "MCP message stream."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "get": {
        "tags": [
          "MCP"
        ],
        "summary": "Open an SSE channel for server-initiated messages.",
        "responses": {
          "200": {
            "description": "SSE stream."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  }
}