{
  "openapi": "3.1.0",
  "info": {
    "title": "ship.page",
    "version": "1.0.0",
    "description": "Throw HTML at an API, get an unguessable URL back. Anonymous deploys need no auth; account routes accept an sp_ API key or a Firebase ID token as a Bearer credential. Docs: https://ship.page/docs — agents start at https://ship.page/docs/agents. MCP server: POST https://ship.page/mcp.",
    "contact": { "name": "Bitgate, Inc.", "url": "https://ship.page", "email": "abuse@bitgate.com" }
  },
  "servers": [{ "url": "https://ship.page" }],
  "security": [],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An sp_… API key (minted via POST /keys or the dashboard) or a Firebase ID token."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable message." },
          "code": {
            "type": "string",
            "description": "Machine-stable error code — parse this, not `error`.",
            "enum": [
              "invalid_request",
              "invalid_ttl",
              "invalid_name",
              "invalid_zip",
              "auth_required",
              "subscription_required",
              "payload_too_large",
              "file_cap_exceeded",
              "rate_limited",
              "not_found",
              "method_not_allowed",
              "conflict",
              "gone",
              "upstream_unavailable",
              "internal_error"
            ]
          },
          "hint": { "type": "string", "description": "Human-directed guidance. Agents should relay this to their user verbatim." },
          "upgrade_url": { "type": "string", "format": "uri", "description": "Present when a plan upgrade resolves the failure." },
          "docs_url": { "type": "string", "format": "uri", "description": "Relevant docs page, when one applies." }
        }
      },
      "DeployResponse": {
        "type": "object",
        "required": ["slug", "url", "files", "plan", "expires_at"],
        "properties": {
          "slug": { "type": "string", "example": "vast-juice-c2dse08p" },
          "url": { "type": "string", "format": "uri", "example": "https://vast-juice-c2dse08p.shipped.page/" },
          "files": { "type": "array", "items": { "type": "string" } },
          "plan": { "type": "string", "enum": ["free", "pro", "team"] },
          "expires_at": { "type": ["string", "null"], "format": "date-time", "description": "Null for drops that never expire (subscribers)." },
          "name": { "type": "string", "description": "Present when ?name= was used." },
          "replaced": { "type": "boolean", "description": "Present when ?name= was used; true if an existing named drop was replaced." }
        }
      },
      "AppendResponse": {
        "type": "object",
        "required": ["slug", "url", "files", "total_files", "plan", "expires_at"],
        "properties": {
          "slug": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "files": { "type": "array", "items": { "type": "string" }, "description": "Files added by this request." },
          "total_files": { "type": "integer", "description": "Total files in the drop after appending (cap 10,000)." },
          "plan": { "type": "string", "enum": ["free", "pro", "team"] },
          "expires_at": { "type": ["string", "null"], "format": "date-time" }
        }
      },
      "FilesBody": {
        "type": "object",
        "required": ["files"],
        "properties": {
          "files": {
            "type": "object",
            "description": "Path → content map. Values are a string, or { encoding: \"base64\", content: string } for binary.",
            "additionalProperties": {
              "oneOf": [
                { "type": "string" },
                {
                  "type": "object",
                  "required": ["encoding", "content"],
                  "properties": {
                    "encoding": { "type": "string", "const": "base64" },
                    "content": { "type": "string" }
                  }
                }
              ]
            }
          }
        }
      },
      "Drop": {
        "type": "object",
        "properties": {
          "slug": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "name": { "type": ["string", "null"] },
          "file_count": { "type": "integer" },
          "redeploy_count": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" },
          "expires_at": { "type": ["string", "null"], "format": "date-time" }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Bad request — unparseable body, bad paths, invalid ttl/name, per-request file cap, invalid zip.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "Missing or invalid credential.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PaymentRequired": {
        "description": "Subscriber-gated feature on a free account. The body's `hint` and `upgrade_url` tell the user how to upgrade.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Not found (also returned for resources owned by someone else).",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Conflict": {
        "description": "Conflict — vanity sub taken or already owned.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PayloadTooLarge": {
        "description": "Payload too large — zip over the plan cap, decompression past 4×, or the 10,000-file drop total.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limited. Back off per the Retry-After header (60s).",
        "headers": {
          "Retry-After": { "schema": { "type": "integer", "example": 60 }, "description": "Seconds to wait before retrying." }
        },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "paths": {
    "/deploy": {
      "post": {
        "operationId": "deploy",
        "summary": "Deploy a drop (anonymous allowed)",
        "description": "Body format is picked by Content-Type: `text/html` (or any non-JSON/non-zip type) stores the raw body as index.html; `application/json` expects a files map; `application/zip` unpacks an archive (single root dir auto-stripped). Limits: 100 files/request anonymous+free, 900 paid; zip 25 MB free / 100 MB paid (4× decompression cap); non-zip bodies 10 MB. Rate: abuse guard, 10 deploys/min per IP; 429 responses carry Retry-After.",
        "security": [{}, { "bearerAuth": [] }],
        "parameters": [
          {
            "name": "ttl",
            "in": "query",
            "schema": { "type": "integer", "minimum": 60 },
            "description": "Drop lifetime in seconds. Free caps at 604800 (7 days); subscribers may set anything, and their drops default to no expiry."
          },
          {
            "name": "name",
            "in": "query",
            "schema": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,40}$" },
            "description": "Named drop (subscribers only). Stable alias scoped to the account; redeploying the same name replaces content and keeps the URL."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "text/html": { "schema": { "type": "string" }, "example": "<h1>hello</h1>" },
            "application/json": { "schema": { "$ref": "#/components/schemas/FilesBody" } },
            "application/zip": { "schema": { "type": "string", "format": "binary" } }
          }
        },
        "responses": {
          "200": {
            "description": "Drop deployed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeployResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/deploy/{slug}": {
      "post": {
        "operationId": "appendToDrop",
        "summary": "Append files to an existing drop (subscribers)",
        "description": "Adds files to a drop you own, up to 900 per request and 10,000 total per drop. Same body formats as POST /deploy.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "text/html": { "schema": { "type": "string" } },
            "application/json": { "schema": { "$ref": "#/components/schemas/FilesBody" } },
            "application/zip": { "schema": { "type": "string", "format": "binary" } }
          }
        },
        "responses": {
          "200": {
            "description": "Files appended.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AppendResponse" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/drops": {
      "get": {
        "operationId": "listDrops",
        "summary": "List your drops",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } },
          { "name": "offset", "in": "query", "schema": { "type": "integer", "minimum": 0, "default": 0 } }
        ],
        "responses": {
          "200": {
            "description": "Drops, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["total", "drops"],
                  "properties": {
                    "total": { "type": "integer" },
                    "drops": { "type": "array", "items": { "$ref": "#/components/schemas/Drop" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/drops/{slug}": {
      "delete": {
        "operationId": "deleteDrop",
        "summary": "Delete a drop permanently",
        "description": "Purges files and the index entry. Deleted drops can stay edge-cached for up to 60s.",
        "security": [{ "bearerAuth": [] }],
        "parameters": [
          { "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Drop deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["deleted"],
                  "properties": { "deleted": { "type": "string" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/keys": {
      "post": {
        "operationId": "mintKey",
        "summary": "Mint an sp_ API key",
        "description": "Requires a Firebase ID token (sign in at ship.page). The key is shown once — store it.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "201": {
            "description": "Key minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["accountId", "apiKey", "keyId", "note"],
                  "properties": {
                    "accountId": { "type": "string" },
                    "apiKey": { "type": "string", "description": "sp_… key, shown once." },
                    "keyId": { "type": "string", "description": "SHA-256 of the key." },
                    "note": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/billing/me": {
      "get": {
        "operationId": "billingMe",
        "summary": "Plan and subscription state",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Entitlements for the account.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["accountId", "plan", "activeSubscription"],
                  "properties": {
                    "accountId": { "type": "string" },
                    "plan": { "type": "string", "enum": ["free", "pro", "team"] },
                    "activeSubscription": { "type": "boolean" },
                    "currentPeriodEnd": { "type": ["string", "null"], "format": "date-time" },
                    "cancelAt": { "type": ["string", "null"], "format": "date-time" },
                    "portalAvailable": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/billing/checkout": {
      "post": {
        "operationId": "billingCheckout",
        "summary": "Create a Stripe checkout session",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": { "type": "string", "enum": ["pro", "team"], "default": "pro" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Redirect the user to `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["url", "alreadySubscribed"],
                  "properties": {
                    "url": { "type": "string", "format": "uri" },
                    "alreadySubscribed": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/billing/portal": {
      "post": {
        "operationId": "billingPortal",
        "summary": "Create a Stripe customer portal session",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Redirect the user to `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["url"],
                  "properties": { "url": { "type": "string", "format": "uri" } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/vanity": {
      "get": {
        "operationId": "getVanity",
        "summary": "Get the account's vanity subdomain",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "The claimed sub, or null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["sub"],
                  "properties": { "sub": { "type": ["string", "null"] } }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "operationId": "claimVanity",
        "summary": "Claim a vanity subdomain (Team plan)",
        "description": "3–20 chars of [a-z0-9-], no double hyphens, one per account, first come first served. Named drops then serve at https://<sub>--<name>.shipped.page/.",
        "security": [{ "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["sub"],
                "properties": { "sub": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$" } }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sub claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["sub", "serve_base"],
                  "properties": {
                    "sub": { "type": "string" },
                    "serve_base": { "type": "string", "example": "https://acme--<name>.shipped.page/" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/PaymentRequired" },
          "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "summary": "MCP server (JSON-RPC 2.0, stateless)",
        "description": "Streamable-HTTP-free, stateless MCP endpoint (spec 2026-07-28). Methods: `server/discover`, `tools/list`, `tools/call`, plus legacy `initialize`. Tools: `deploy_html`, `deploy_files`, `list_drops`, `delete_drop`, `get_limits`. Auth is optional pass-through: send `Authorization: Bearer sp_…` to act as an account, omit for anonymous deploys. Client config: `\"ship.page\": { \"url\": \"https://ship.page/mcp\" }`.",
        "security": [{}, { "bearerAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["jsonrpc", "method", "id"],
                "properties": {
                  "jsonrpc": { "type": "string", "const": "2.0" },
                  "id": { "oneOf": [{ "type": "string" }, { "type": "integer" }] },
                  "method": { "type": "string", "enum": ["initialize", "server/discover", "tools/list", "tools/call"] },
                  "params": { "type": "object" }
                }
              },
              "example": {
                "jsonrpc": "2.0",
                "id": 1,
                "method": "tools/call",
                "params": { "name": "deploy_html", "arguments": { "html": "<h1>hello</h1>" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC 2.0 response envelope (`result` or `error`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["jsonrpc", "id"],
                  "properties": {
                    "jsonrpc": { "type": "string", "const": "2.0" },
                    "id": { "oneOf": [{ "type": "string" }, { "type": "integer" }, { "type": "null" }] },
                    "result": {},
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": { "type": "integer" },
                        "message": { "type": "string" },
                        "data": {}
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
