{
  "openapi": "3.0.3",
  "info": {
    "title": "Package URLs control plane",
    "version": "1.0.0",
    "description": "Routes an agent creates while onboarding a Go, PHP, Rust, Swift, Ruby, or Elixir hostname. Customer hostnames are not this API. Errors are {\"message\":\"...\"}. Authorization is a Clerk session JWT, not an API key. The onboarding run is https://packageurls.com/agents.md."
  },
  "servers": [
    {
      "url": "https://api.packageurls.com"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Clerk session JWT from a signed-in packageurls.com browser session."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["message"],
        "properties": {
          "message": {
            "type": "string"
          }
        }
      },
      "Route": {
        "type": "object",
        "required": ["id", "hostname", "git_url", "language", "cname_target", "verified", "verify_token", "created_at", "updated_at"],
        "properties": {
          "id": {
            "type": "string"
          },
          "hostname": {
            "type": "string"
          },
          "git_url": {
            "type": "string",
            "example": "https://github.com"
          },
          "language": {
            "type": "string",
            "enum": ["go", "php", "rust", "swift", "ruby", "elixir"]
          },
          "cname_target": {
            "type": "string",
            "enum": ["go.packageurls.com", "php.packageurls.com", "rust.packageurls.com", "swift.packageurls.com", "ruby.packageurls.com", "elixir.packageurls.com"]
          },
          "verified": {
            "type": "boolean"
          },
          "verify_token": {
            "type": "string"
          },
          "ssl_status": {
            "type": "string",
            "description": "Empty until certificate provisioning starts. active means the hostname may serve. initializing, a pending* status, and a hold* status are still converging."
          },
          "failing_since": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VerifyResult": {
        "type": "object",
        "required": ["outcome", "retryable", "route"],
        "properties": {
          "outcome": {
            "type": "string",
            "enum": ["verified", "token-missing", "nxdomain-or-no-txt", "dns-error"]
          },
          "retryable": {
            "type": "boolean",
            "description": "True only for dns-error. nxdomain-or-no-txt is retryable: false and still means keep polling until the human publishes the TXT record."
          },
          "route": {
            "$ref": "#/components/schemas/Route"
          }
        }
      },
      "User": {
        "type": "object",
        "required": ["id", "email", "created_at", "email_verified", "can_create_routes"],
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "email_verified": {
            "type": "boolean"
          },
          "verify_warned_at": {
            "type": "string",
            "format": "date-time"
          },
          "can_create_routes": {
            "type": "boolean"
          }
        }
      }
    }
  },
  "paths": {
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Account for the session token",
        "responses": {
          "200": {
            "description": "Signed-in account",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["user"],
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or rejected session token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "not signed in"
                }
              }
            }
          }
        }
      }
    },
    "/billing/status": {
      "get": {
        "operationId": "getBillingStatus",
        "summary": "Stored plan. A status other than active, trialing, or past_due is Free.",
        "responses": {
          "200": {
            "description": "Subscription row",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status", "current_period_end", "cancel_at_period_end", "portal", "plan", "paid_review"],
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": ["active", "trialing", "past_due", "canceled", "none"]
                    },
                    "current_period_end": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "cancel_at_period_end": {
                      "type": "boolean"
                    },
                    "portal": {
                      "type": "boolean"
                    },
                    "plan": {
                      "type": "string",
                      "description": "pro_month, pro_year, max_month, max_year, or empty for Free"
                    },
                    "paid_review": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/account/usage": {
      "get": {
        "operationId": "getAccountUsage",
        "summary": "Request count for the current billing period",
        "responses": {
          "200": {
            "description": "Account rollup",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["request_count", "period_start", "period_end", "routes"],
                  "properties": {
                    "request_count": {
                      "type": "integer"
                    },
                    "period_start": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "period_end": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "routes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["id", "request_count"],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "request_count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/routes": {
      "get": {
        "operationId": "listRoutes",
        "summary": "Routes for this account, newest first",
        "responses": {
          "200": {
            "description": "Route list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["routes"],
                  "properties": {
                    "routes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Route"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createRoute",
        "summary": "Create one hostname route",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["hostname", "git_url", "language"],
                "properties": {
                  "hostname": {
                    "type": "string"
                  },
                  "git_url": {
                    "type": "string",
                    "description": "Bare http(s) origin. No path, query, fragment, or credentials."
                  },
                  "language": {
                    "type": "string",
                    "enum": ["go", "php", "rust", "swift", "ruby", "elixir"]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored route, not yet verified",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                }
              }
            }
          },
          "400": {
            "description": "Language, hostname, or forge URL rejected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "language must be go, php, rust, swift, ruby, or elixir"
                }
              }
            }
          },
          "403": {
            "description": "Email is unverified and the account has no live paid plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Verify your email before creating routes."
                }
              }
            }
          },
          "409": {
            "description": "Hostname already claimed, or the account is at its route cap",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/routes/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "operationId": "getRoute",
        "summary": "One route, including ssl_status",
        "responses": {
          "200": {
            "description": "The route",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                }
              }
            }
          },
          "404": {
            "description": "Missing, or owned by someone else",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "route not found"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateGitURL",
        "summary": "Change the forge root. Import paths stay the same.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["git_url"],
                "properties": {
                  "git_url": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated route",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteRoute",
        "summary": "Delete a route. The onboarding run does not call this unless the human asks.",
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/routes/{id}/verify": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "verifyRoute",
        "summary": "Look up _packageurls.<hostname> TXT and compare it to verify_token",
        "responses": {
          "200": {
            "description": "Lookup result. verified is the only outcome that starts certificate provisioning.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResult"
                }
              }
            }
          }
        }
      }
    },
    "/routes/{id}/ssl": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "refreshRouteSSL",
        "summary": "Refresh a converging certificate. Does not fail the run while status is pending.",
        "responses": {
          "200": {
            "description": "Route with the latest ssl_status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Route"
                }
              }
            }
          }
        }
      }
    }
  }
}
