{
  "openapi": "3.1.0",
  "info": {
    "title": "TeamSort API",
    "version": "1.0.0",
    "summary": "Draft a TeamSort ranked-choice poll from a prompt and get back a link to hand to the user.",
    "description": "The TeamSort API turns a natural-language prompt into a pre-filled ranked-choice poll and returns a URL. Hand that URL to the person you are helping: they review the draft, edit it if they like, and publish it as a live poll to share with their group.\n\n**No authentication.** There is no API key, no OAuth flow and no bearer token — the API is public and keyless. Callers are identified by IP address for rate limiting only. See https://teamsort.world/developers#authentication.\n\n**Rate limits.** 20 requests per minute per IP address, plus a global ceiling of 3000 generations per UTC day (every call runs an AI generation). A 429 carries `Retry-After`. See https://teamsort.world/developers#rate-limits.\n\n**CORS** is open to all origins; `Link` and `Retry-After` are exposed to browser callers.\n\n**Errors** are always JSON with a machine-readable `code` and a `hint`. See https://teamsort.world/developers#errors.",
    "termsOfService": "https://teamsort.world/terms",
    "contact": {
      "name": "TeamSort support",
      "url": "https://teamsort.world/contact",
      "email": "rob@teamsort.world"
    }
  },
  "externalDocs": {
    "description": "TeamSort API documentation",
    "url": "https://teamsort.world/developers"
  },
  "servers": [
    {
      "url": "https://teamsort.world",
      "description": "TeamSort production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Polls",
      "description": "Draft a poll and read the draft back.",
      "externalDocs": {
        "url": "https://teamsort.world/developers"
      }
    }
  ],
  "paths": {
    "/api/v1/polls": {
      "post": {
        "operationId": "createPollDraft",
        "tags": [
          "Polls"
        ],
        "summary": "Draft a TeamSort poll from a prompt",
        "description": "Generates a title, instructions and items, stores them as a draft, and returns the review URL to hand to the user.",
        "externalDocs": {
          "url": "https://teamsort.world/developers"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PollDraftRequest"
              },
              "examples": {
                "withItems": {
                  "summary": "Items supplied — faster and more faithful",
                  "value": {
                    "prompt": "Which framework for the rewrite?",
                    "items": [
                      "React",
                      "Vue",
                      "Svelte",
                      "SolidJS"
                    ]
                  }
                },
                "promptOnly": {
                  "summary": "Items generated from the prompt",
                  "value": {
                    "prompt": "Where should we go for the team offsite?"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft created.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PollDraftCreated"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. Provide a `prompt` (1-2000 chars) and optional `items` (2-10 strings, each 1-200 chars).",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "Invalid request. Provide a `prompt` (1-2000 chars) and optional `items` (2-10 strings, each 1-200 chars).",
                    "value": {
                      "error": "Invalid request. Provide a `prompt` (1-2000 chars) and optional `items` (2-10 strings, each 1-200 chars).",
                      "code": "invalid_request",
                      "hint": "Check prompt and items against the documented limits, then retry. On GET, repeat the items parameter once per item rather than sending one comma-separated value.",
                      "docs": "https://teamsort.world/developers#errors"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Try again later.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "required": true,
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "examples": [
                    60
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "Rate limit exceeded. Try again later.",
                    "value": {
                      "error": "Rate limit exceeded. Try again later.",
                      "code": "rate_limited",
                      "hint": "Wait the number of seconds in the Retry-After header, then retry. The limit is 20 requests per minute per IP address, plus a global ceiling of 3000 generations per UTC day.",
                      "docs": "https://teamsort.world/developers#errors",
                      "retry_after": 60
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Failed to generate poll. Please try again.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "generation_failed": {
                    "summary": "Failed to generate poll. Please try again.",
                    "value": {
                      "error": "Failed to generate poll. Please try again.",
                      "code": "generation_failed",
                      "hint": "Retry once. If it keeps failing the AI generation step is down; report it from the contact page.",
                      "docs": "https://teamsort.world/developers#errors"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "createPollDraftFromQuery",
        "tags": [
          "Polls"
        ],
        "summary": "Draft a TeamSort poll from query parameters",
        "description": "Identical to the POST, with the inputs in the query string, so an assistant whose only tool is a URL fetch can call it. Not idempotent: every call mints a new draft and runs an AI generation, which is why HEAD is rejected.",
        "externalDocs": {
          "url": "https://teamsort.world/developers"
        },
        "parameters": [
          {
            "name": "prompt",
            "in": "query",
            "required": true,
            "description": "URL-encoded natural-language prompt.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2000
            },
            "example": "Where should we go for the team offsite?"
          },
          {
            "name": "items",
            "in": "query",
            "required": false,
            "description": "Repeat once per item — this is NOT a comma-separated list.",
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "minItems": 2,
              "maxItems": 10,
              "items": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              }
            },
            "example": [
              "Costa Rica",
              "Portugal",
              "Japan",
              "Iceland"
            ]
          }
        ],
        "responses": {
          "201": {
            "description": "Draft created.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PollDraftCreated"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. Provide a `prompt` (1-2000 chars) and optional `items` (2-10 strings, each 1-200 chars).",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_request": {
                    "summary": "Invalid request. Provide a `prompt` (1-2000 chars) and optional `items` (2-10 strings, each 1-200 chars).",
                    "value": {
                      "error": "Invalid request. Provide a `prompt` (1-2000 chars) and optional `items` (2-10 strings, each 1-200 chars).",
                      "code": "invalid_request",
                      "hint": "Check prompt and items against the documented limits, then retry. On GET, repeat the items parameter once per item rather than sending one comma-separated value.",
                      "docs": "https://teamsort.world/developers#errors"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "HEAD is rejected: it would mint a draft and run an AI generation with no visible response, so link checkers and prefetchers are refused. The body is empty.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              },
              "Allow": {
                "description": "Methods this path accepts.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "GET, POST, OPTIONS"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Try again later.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              },
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "required": true,
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "examples": [
                    60
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "rate_limited": {
                    "summary": "Rate limit exceeded. Try again later.",
                    "value": {
                      "error": "Rate limit exceeded. Try again later.",
                      "code": "rate_limited",
                      "hint": "Wait the number of seconds in the Retry-After header, then retry. The limit is 20 requests per minute per IP address, plus a global ceiling of 3000 generations per UTC day.",
                      "docs": "https://teamsort.world/developers#errors",
                      "retry_after": 60
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Failed to generate poll. Please try again.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "generation_failed": {
                    "summary": "Failed to generate poll. Please try again.",
                    "value": {
                      "error": "Failed to generate poll. Please try again.",
                      "code": "generation_failed",
                      "hint": "Retry once. If it keeps failing the AI generation step is down; report it from the contact page.",
                      "docs": "https://teamsort.world/developers#errors"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/drafts/{id}": {
      "get": {
        "operationId": "getPollDraft",
        "tags": [
          "Polls"
        ],
        "summary": "Read a generated draft back",
        "description": "Returns the stored draft for a ref id. Drafts expire 7 days after generation. The create page uses this; most integrations never need it.",
        "externalDocs": {
          "url": "https://teamsort.world/developers"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The ref id from the `url` of a create response (the `?ref=` value).",
            "schema": {
              "type": "string"
            },
            "example": "aBcDeFgHiJ"
          }
        ],
        "responses": {
          "200": {
            "description": "The stored draft.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PollDraft"
                }
              }
            }
          },
          "404": {
            "description": "Draft not found or expired.",
            "headers": {
              "Cache-Control": {
                "description": "Always `no-store, private`. Poll drafts are one-shot and must never be cached or shared.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "no-store, private"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "draft_not_found": {
                    "summary": "Draft not found or expired.",
                    "value": {
                      "error": "Draft not found or expired.",
                      "code": "draft_not_found",
                      "hint": "Draft ids expire 7 days after generation. Create a new one with POST /api/v1/polls.",
                      "docs": "https://teamsort.world/developers#errors"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PollDraftRequest": {
        "type": "object",
        "required": [
          "prompt"
        ],
        "additionalProperties": false,
        "properties": {
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2000,
            "description": "What the group needs to rank or decide, in natural language.",
            "examples": [
              "Where should we go for the team offsite?"
            ]
          },
          "items": {
            "type": "array",
            "minItems": 2,
            "maxItems": 10,
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "description": "The options to rank. Omit to have them generated from the prompt. Item strings may carry inline context, for example \"React — mature ecosystem\".",
            "examples": [
              [
                "Costa Rica",
                "Portugal",
                "Japan",
                "Iceland"
              ]
            ]
          }
        }
      },
      "PollDraftCreated": {
        "type": "object",
        "required": [
          "url",
          "title",
          "items"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The review link to hand to the user. Opens a pre-filled poll composer. Expires after 7 days — do not store it.",
            "examples": [
              "https://teamsort.world/create?ref=aBcDeFgHiJ"
            ]
          },
          "title": {
            "type": "string",
            "description": "The generated poll title (under 80 characters).",
            "examples": [
              "Where should the team offsite be?"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "A flat list of the item labels that will appear on the poll, matching your input if you supplied one.",
            "examples": [
              [
                "Costa Rica",
                "Portugal",
                "Japan",
                "Iceland"
              ]
            ]
          }
        }
      },
      "PollDraft": {
        "type": "object",
        "required": [
          "title",
          "instructions",
          "items"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "instructions": {
            "type": "string",
            "description": "Voter-facing instructions. May be empty."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PollDraftItem"
            }
          }
        }
      },
      "PollDraftItem": {
        "type": "object",
        "required": [
          "value",
          "details"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "The item label."
          },
          "details": {
            "type": "string",
            "description": "Optional supporting detail. May be empty."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "code",
          "hint",
          "docs"
        ],
        "description": "Every error from this API, in one shape. `code` is stable and machine-readable; `error` and `hint` are for humans and for an agent to relay.",
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong, in a sentence."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "rate_limited",
              "generation_failed",
              "draft_not_found",
              "not_found",
              "method_not_allowed"
            ],
            "description": "Stable machine-readable error code."
          },
          "hint": {
            "type": "string",
            "description": "How to resolve it."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Documentation for this error.",
            "examples": [
              "https://teamsort.world/developers#errors"
            ]
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before retrying. Present on 429 only; mirrors the Retry-After header."
          }
        }
      }
    }
  }
}