{
  "openapi": "3.1.0",
  "info": {
    "title": "crob.at API",
    "version": "1.0.0",
    "description": "Create, retrieve, and generate competitive Pokemon Showdown teams, read reference data, and use optional crob.at session helpers.",
    "termsOfService": "https://crob.at/api#terms",
    "contact": {
      "name": "crob.at API support",
      "url": "https://crob.at/contact",
      "email": "admin@crob.at"
    }
  },
  "externalDocs": {
    "description": "Human-readable API documentation",
    "url": "https://crob.at/api"
  },
  "servers": [
    {
      "url": "https://crob.at",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Teams",
      "description": "Create and retrieve shareable Pokemon Showdown teams."
    },
    {
      "name": "Generation",
      "description": "Generate and optionally save competitive random teams."
    },
    {
      "name": "Reference",
      "description": "Read sample teams and Pokemon reference data."
    },
    {
      "name": "Account",
      "description": "Optional same-origin helpers that use a crob.at browser session."
    },
    {
      "name": "Feedback",
      "description": "Send product feedback to the crob.at team."
    }
  ],
  "paths": {
    "/api/team/{slug}": {
      "get": {
        "tags": ["Teams"],
        "summary": "Retrieve a team",
        "description": "Returns a saved single-team or multi-team paste by its public slug.",
        "operationId": "getTeamBySlug",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The team's public slug.",
            "schema": {
              "type": "string",
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The saved team.",
            "headers": {
              "Cache-Control": {
                "description": "Public response cache policy.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Team" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/team/{slug}/{teamSlug}": {
      "get": {
        "tags": ["Teams"],
        "summary": "Retrieve one team from a multi-team paste",
        "description": "Returns one named team from a saved multi-team paste. Use the child slug shown in the shared URL or derived from the team name.",
        "operationId": "getTeamInPasteBySlug",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The multi-team paste's public slug.",
            "schema": { "type": "string", "minLength": 1 }
          },
          {
            "name": "teamSlug",
            "in": "path",
            "required": true,
            "description": "The selected team's slug within the paste.",
            "schema": { "type": "string", "minLength": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "The selected team.",
            "headers": {
              "Cache-Control": {
                "description": "Public response cache policy.",
                "schema": { "type": "string" }
              }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TeamInPaste" }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/team": {
      "post": {
        "tags": ["Teams"],
        "summary": "Create a team",
        "description": "Creates a shareable team from one or more Pokemon Showdown exports. No account or API key is required.",
        "operationId": "createTeam",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateTeamRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The team was created.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreatedTeam" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/random-team/{format}": {
      "get": {
        "tags": ["Generation"],
        "summary": "Generate a random team",
        "description": "Generates a usage-weighted competitive team without saving it.",
        "operationId": "generateRandomTeam",
        "parameters": [
          { "$ref": "#/components/parameters/Format" }
        ],
        "responses": {
          "200": {
            "description": "A generated Pokemon Showdown team and its rendered card HTML.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GeneratedTeam" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "502": {
            "description": "The upstream usage-data source could not be read.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/random-team/{format}/save": {
      "post": {
        "tags": ["Generation"],
        "summary": "Save a generated team",
        "description": "Creates an unlisted permanent URL for a previously generated team.",
        "operationId": "saveRandomTeam",
        "parameters": [
          { "$ref": "#/components/parameters/Format" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["teamText"],
                "properties": {
                  "teamText": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 500000,
                    "description": "A Pokemon Showdown team export returned by the generation endpoint."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The generated team was saved.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CreatedTeam" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/samples/{tier}": {
      "get": {
        "tags": ["Reference"],
        "summary": "List sample teams",
        "description": "Returns the available Smogon sample teams for a competitive tier.",
        "operationId": "listSampleTeams",
        "parameters": [
          {
            "name": "tier",
            "in": "path",
            "required": true,
            "description": "An alphanumeric Showdown format identifier, such as gen9ou.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sample teams, or an empty array when none are available.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": { "$ref": "#/components/schemas/SampleTeam" }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/api/type-chart-data": {
      "get": {
        "tags": ["Reference"],
        "summary": "Read Pokemon type data",
        "description": "Returns Pokemon names and their types for the interactive type chart.",
        "operationId": "getTypeChartData",
        "responses": {
          "200": {
            "description": "Pokemon names paired with one or two types.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "array",
                    "minItems": 2,
                    "maxItems": 2,
                    "prefixItems": [
                      { "type": "string" },
                      {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 2,
                        "items": { "type": "string" }
                      }
                    ],
                    "items": {}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/me": {
      "get": {
        "tags": ["Account"],
        "summary": "Read the current user",
        "description": "Returns the current crob.at user when a valid browser session cookie is present, otherwise returns a null user.",
        "operationId": "getCurrentUser",
        "security": [
          { "cookieSession": [] },
          {}
        ],
        "responses": {
          "200": {
            "description": "The current user or null.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["user"],
                  "properties": {
                    "user": {
                      "oneOf": [
                        { "$ref": "#/components/schemas/CurrentUser" },
                        { "type": "null" }
                      ]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/feedback": {
      "post": {
        "tags": ["Feedback"],
        "summary": "Send feedback",
        "description": "Sends a feedback message with optional contact details.",
        "operationId": "sendFeedback",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["message"],
                "properties": {
                  "message": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000,
                    "pattern": "\\S",
                    "description": "The trimmed message must contain a non-whitespace character and be at most 2,000 characters."
                  },
                  "contact": {
                    "type": "string",
                    "description": "Optional contact details. Values longer than 100 characters are truncated."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Feedback was accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success"],
                  "properties": {
                    "success": { "const": true }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "429": { "$ref": "#/components/responses/RetryAfterRateLimited" },
          "500": { "$ref": "#/components/responses/ServerError" }
        }
      }
    },
    "/api/showdown/assertion": {
      "post": {
        "tags": ["Account"],
        "summary": "Start browser-side Showdown assertion generation",
        "description": "Redirects the signed-in user's browser to Pokemon Showdown so the challstr-specific assertion is generated from the user's network rather than crob.at's server.",
        "operationId": "createShowdownAssertion",
        "security": [
          { "cookieSession": [] }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["challstr"],
                "properties": {
                  "challstr": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2048,
                    "pattern": "^[^\\r\\n]+$",
                    "description": "A Pokemon Showdown challenge string without line breaks."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "303": {
            "description": "Redirect to Pokemon Showdown's OAuth assertion endpoint.",
            "headers": {
              "Location": {
                "required": true,
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": {
            "description": "A linked, authenticated Showdown account is required.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "503": {
            "description": "Showdown OAuth is not configured.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cookieSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session",
        "description": "Optional crob.at browser session cookie. Public endpoints do not require it."
      }
    },
    "parameters": {
      "Format": {
        "name": "format",
        "in": "path",
        "required": true,
        "description": "An alphanumeric Showdown format identifier, such as gen9ou.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9]+$"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request body or route parameter is invalid.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "NotFound": {
        "description": "The requested resource was not found.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "RateLimited": {
        "description": "The per-client write limit was exceeded.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the request may be retried.",
            "schema": { "type": "integer", "minimum": 1 }
          },
          "X-RateLimit-Limit": {
            "description": "Requests permitted in the current window.",
            "schema": { "type": "integer" }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests remaining in the current window.",
            "schema": { "type": "integer" }
          },
          "X-RateLimit-Reset": {
            "description": "ISO 8601 timestamp when the window resets.",
            "schema": { "type": "string", "format": "date-time" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "RetryAfterRateLimited": {
        "description": "The per-client write limit was exceeded.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the request may be retried.",
            "schema": { "type": "integer", "minimum": 1 }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      },
      "ServerError": {
        "description": "The request could not be completed because of a server error.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" },
          "retryAfter": { "type": "integer", "minimum": 1 }
        }
      },
      "TeamPaste": {
        "type": "object",
        "required": ["paste"],
        "properties": {
          "format": { "type": ["string", "null"] },
          "name": { "type": ["string", "null"] },
          "paste": { "type": "string" }
        }
      },
      "Team": {
        "type": "object",
        "required": ["slug", "name", "author", "description", "public", "image", "source_url", "created_at", "views", "is_multi", "teams"],
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": ["string", "null"] },
          "author": { "type": ["string", "null"] },
          "description": { "type": ["string", "null"] },
          "public": {
            "type": "boolean",
            "description": "Whether the team may appear in the public gallery and search."
          },
          "image": { "type": "string", "format": "uri" },
          "source_url": { "type": ["string", "null"], "format": "uri" },
          "created_at": { "type": "string" },
          "views": { "type": "integer", "minimum": 0 },
          "is_multi": { "type": "boolean" },
          "teams": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/TeamPaste" }
          }
        }
      },
      "TeamInPaste": {
        "type": "object",
        "required": ["paste_slug", "slug", "format", "name", "paste", "url", "image"],
        "properties": {
          "paste_slug": { "type": "string", "description": "The parent multi-team paste's public slug." },
          "slug": { "type": "string", "description": "The selected team's slug within the parent paste." },
          "format": { "type": "string" },
          "name": { "type": "string" },
          "paste": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "image": { "type": "string", "format": "uri" }
        }
      },
      "CreateTeamRequest": {
        "type": "object",
        "required": ["teams"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Optional team name. Values longer than 100 characters are truncated."
          },
          "author": {
            "type": "string",
            "description": "Optional author. Values longer than 50 characters are truncated."
          },
          "description": {
            "type": "string",
            "description": "Optional description. The trimmed value is truncated to 1,000 characters."
          },
          "public": {
            "type": "boolean",
            "default": false,
            "description": "Whether the team may appear in the public gallery."
          },
          "teams": {
            "type": "array",
            "minItems": 1,
            "description": "One or more teams. The combined normalized Showdown export must not exceed 500,000 characters.",
            "items": {
              "type": "object",
              "required": ["paste"],
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Optional team name. Values longer than 100 characters are truncated."
                },
                "format": {
                  "type": "string",
                  "description": "A supported Pokemon Showdown format ID or name. The server normalizes names to IDs."
                },
                "paste": { "type": "string", "minLength": 1, "maxLength": 500000 }
              }
            }
          }
        }
      },
      "CreatedTeam": {
        "type": "object",
        "required": ["slug", "url", "image"],
        "properties": {
          "slug": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "image": { "type": "string", "format": "uri" },
          "management_token": { "type": "string", "minLength": 48, "maxLength": 48, "description": "Private creator capability returned by POST /api/team. Store it securely to change visibility later; do not include it in shared team URLs." }
        }
      },
      "GeneratedTeam": {
        "type": "object",
        "required": ["teamText", "statsDate", "cardsHtml"],
        "properties": {
          "teamText": { "type": "string" },
          "statsDate": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}$" },
          "cardsHtml": { "type": "string" }
        }
      },
      "SampleTeam": {
        "type": "object",
        "required": ["slug", "name", "author", "tier", "views", "created_at", "url", "image", "source_url"],
        "properties": {
          "slug": { "type": "string" },
          "name": { "type": ["string", "null"] },
          "author": { "type": ["string", "null"] },
          "tier": { "type": ["string", "null"] },
          "views": { "type": "integer", "minimum": 0 },
          "created_at": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "image": { "type": "string", "format": "uri" },
          "source_url": { "type": ["string", "null"], "format": "uri" }
        }
      },
      "CurrentUser": {
        "type": "object",
        "required": ["username", "email", "ps_username", "team_count", "favorite_count"],
        "properties": {
          "username": { "type": "string" },
          "email": { "type": ["string", "null"], "format": "email" },
          "ps_username": { "type": ["string", "null"] },
          "team_count": { "type": "integer", "minimum": 0 },
          "favorite_count": { "type": "integer", "minimum": 0 }
        }
      }
    }
  }
}
