# TeamSort Public API See also the human-readable version at https://teamsort.world/developers (also available as Markdown via `Accept: text/markdown`). TeamSort is a real-time collaborative ranked-choice voting app. This document describes the public HTTP API for creating polls programmatically — designed for LLMs and other tools that want to hand a user a pre-filled poll creation link. ## Developer resources Every developer resource, at a fixed URL, named so you can ask for it by name: | Resource | URL | |---|---| | TeamSort API documentation | https://teamsort.world/developers | | TeamSort OpenAPI specification (OpenAPI 3.1) | https://teamsort.world/openapi.json | | TeamSort API catalog (RFC 9727 linkset) | https://teamsort.world/.well-known/api-catalog | | TeamSort API authentication | https://teamsort.world/developers#authentication | | TeamSort API rate limits | https://teamsort.world/developers#rate-limits | | TeamSort API error codes | https://teamsort.world/developers#errors | | TeamSort API reference (this file) | https://teamsort.world/llms-full.txt | | TeamSort pricing (plans, prices in USD, free-tier limits) | https://teamsort.world/pricing | `/api/openapi.json` serves the same specification; `/openapi.yaml`, `/api/openapi.yaml` and `/swagger.json` redirect to it, and `/docs`, `/api-docs` and `/api/docs` redirect to `/developers`. Every response from the API carries RFC 8631 `Link` headers pointing at `rel="service-desc"` (the specification) and `rel="service-doc"` (the documentation). **TeamSort MCP server: not available yet.** Until it ships, use the HTTP API described here — most agent frameworks can import the OpenAPI specification directly as a tool definition. **TeamSort webhooks: not available.** There is no callback URL parameter on any endpoint. To be notified when a poll closes, subscribe to results by email on the poll's results tab. ## Overview The API exposes one poll-generation endpoint on two HTTP methods, plus a helper read endpoint: 1. `GET /api/v1/polls` — Same generator as POST, but reads inputs from query parameters. Use this from any tool with a `web_fetch` / browsing capability. Returns a short-link URL. 2. `POST /api/v1/polls` — Same generator as GET, but reads inputs from a JSON body. Use this from tool integrations, custom GPT Actions, MCP servers, or anywhere you can send a JSON body. 3. `GET /api/v1/drafts/:id` — Read back a stored draft by its ref id. Used internally by the review page; most callers won't need this. No authentication is required. CORS is open (`Access-Control-Allow-Origin: *`). Drafts expire after 7 days if unused. The endpoint is rate limited (per IP, plus a global daily ceiling): a `429 Too Many Requests` response carries a `Retry-After` header with the number of seconds to wait. Every call to `/api/v1/polls` mints a new draft — responses are `Cache-Control: no-store, private` and must not be cached. ### Which method should I use? - **Use GET** if you have web browsing / fetch capability but no ability to send a custom HTTP body — this includes most consumer LLMs running inside a chat window with a browsing tool. - **Use POST** if you're calling from a tool integration, a custom GPT Action, an MCP server, a backend job, or any context where you control request headers and body. Both methods accept the same inputs and return the same response shape. Pick whichever matches your execution environment. The intended flow is: 1. Your tool/agent calls `GET` or `POST /api/v1/polls` with a description. 2. The response contains a `url` field pointing at `https://teamsort.world/create?ref=...`. 3. Your tool hands that URL to the user. 4. The user opens the URL → lands on the TeamSort create page with the title, instructions, and items pre-filled → reviews, edits if needed, and clicks "Start sorting together" to publish. 5. Publishing creates a live poll that the user can share with their team for ranked-choice voting. ## GET /api/v1/polls Same generator as the POST endpoint, but inputs are read from query parameters. This is the method to use when your only HTTP capability is a URL fetch (for example, an LLM's web-browsing tool). ### Request - **Method:** `GET` - **URL:** `https://teamsort.world/api/v1/polls?prompt=...&items=...&items=...` - **Headers:** none required - **Query parameters:** | Param | Type | Required | Notes | |-------|------|----------|-------| | `prompt` | string | Yes | Natural language description of what to rank or decide. URL-encoded. 1–2000 chars. | | `items` | string (repeated) | No | Pass 2–10 items by **repeating** the `items=` parameter, one per item. Each item 1–200 chars. **Not** comma-separated. Omit entirely to let the AI generate items from the prompt. | **URL encoding:** spaces become `+` or `%20`, and special characters (`&`, `=`, `#`, `?`, `/`, newlines) must be percent-encoded. Most URL builders handle this for you — if you're constructing the URL by hand, use `encodeURIComponent` (or equivalent) on each param value. **HEAD requests return `405 Method Not Allowed`** — every poll generation mints a fresh draft and burns an LLM call, so we reject HEAD rather than let link checkers and prefetchers run the handler invisibly. ### Success Response Identical to the POST endpoint — `201 Created` with: ```json { "url": "https://teamsort.world/create?ref=aBcDeFgHiJ", "title": "string", "items": ["string", "string", "..."] } ``` Every response carries `Cache-Control: no-store, private`. Do not cache. ### Error Responses Identical to the POST endpoint — `400 Bad Request` on validation failure, `429 Too Many Requests` when rate limited (see `Retry-After`), `500 Internal Server Error` on generation failure. Error body shapes are below in the POST section. ### Examples **1. Simple list (items provided):** ``` curl "https://teamsort.world/api/v1/polls?prompt=Team+offsite+destination&items=Costa+Rica&items=Portugal&items=Japan&items=Iceland" ``` **2. Rich context with items:** ``` curl "https://teamsort.world/api/v1/polls?prompt=We+are+rewriting+our+frontend+and+need+to+pick+a+framework.+Our+team+cares+about+developer+experience%2C+bundle+size%2C+and+hiring+pool.&items=React&items=Vue&items=Svelte&items=SolidJS" ``` **3. Vague description (no items — AI generates everything):** ``` curl "https://teamsort.world/api/v1/polls?prompt=Help+me+pick+toppings+for+the+office+pizza+party" ``` **4. Paste from chat:** ``` curl "https://teamsort.world/api/v1/polls?prompt=Our+Q2+roadmap+backlog+%E2%80%94+help+us+prioritize&items=Dark+mode&items=Mobile+app&items=Public+API&items=Team+permissions&items=SAML+SSO&items=Export+to+CSV" ``` ## POST /api/v1/polls Generates a poll draft using Claude and stores it as a short-lived KV entry. Returns a review URL. ### Request - **Method:** `POST` - **URL:** `https://teamsort.world/api/v1/polls` - **Headers:** `Content-Type: application/json` - **Body:** ```json { "prompt": "string (1-2000 chars, required)", "items": ["string", "string", "..."] } ``` | Field | Type | Required | Notes | |-------|------|----------|-------| | `prompt` | string | Yes | Natural language description of what to rank or decide. 1-2000 chars. | | `items` | string[] | No | Optional pre-defined items. If provided, the AI only generates a title and instructions, using these items as-is. Min 2, max 10 items. Each item 1-200 chars. | **When to provide `items`:** If the user has already listed specific options in the conversation, pass them explicitly. The API generation is faster, cheaper, and more reliable because the AI doesn't have to invent items. **When to omit `items`:** If the user described a topic without listing specific options, omit `items` and let the AI generate everything from the prompt. ### Success Response - **Status:** `201 Created` - **Body:** ```json { "url": "https://teamsort.world/create?ref=aBcDeFgHiJ", "title": "string", "items": ["string", "string", "..."] } ``` | Field | Type | Notes | |-------|------|-------| | `url` | string | Review URL. Open this to see the pre-filled poll creation form. Expires 7 days after generation. | | `title` | string | The generated poll title. Under 80 characters. | | `items` | string[] | The final list of items that will appear on the poll. Matches the input `items` if you provided them. | Note: `items` in the response is always a flat array of strings, even if the AI generated rich item details internally. Those details are preserved in the draft and will appear on the create page — but the API response keeps the shape predictable for LLM tool-use descriptions. ### Error Responses Every error from this API is JSON — never HTML — and always the same shape: | Field | Type | Meaning | |---|---|---| | `error` | string | What went wrong, in a sentence. Safe to show a user. | | `code` | string | Stable, machine-readable identifier. Branch on this, not on `error`. | | `hint` | string | How to resolve it. | | `docs` | string | Link to the documentation for this error. | | `retry_after` | number | Seconds to wait. Present on `429` only; mirrors the `Retry-After` header. | The full set of codes: `invalid_request` (400), `rate_limited` (429), `generation_failed` (500), `draft_not_found` (404), `not_found` (404, unknown path under `/api/v1`), `method_not_allowed` (405). - **400 Bad Request** — Invalid body. ```json { "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 Too Many Requests** — Rate limited (per IP burst limit or the global daily ceiling). Wait for the number of seconds in the `Retry-After` header. ```json { "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 Internal Server Error** — AI generation failed or returned invalid output. ```json { "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" } ``` ### Examples **1. Simple list (items provided):** ``` curl -X POST https://teamsort.world/api/v1/polls \ -H "Content-Type: application/json" \ -d '{ "prompt": "Team offsite destination", "items": ["Costa Rica", "Portugal", "Japan", "Iceland"] }' ``` **2. Rich context with items:** ``` curl -X POST https://teamsort.world/api/v1/polls \ -H "Content-Type: application/json" \ -d '{ "prompt": "We are rewriting our frontend and need to pick a framework. Our team cares about developer experience, bundle size, and hiring pool.", "items": ["React", "Vue", "Svelte", "SolidJS"] }' ``` **3. Vague description (no items — AI generates everything):** ``` curl -X POST https://teamsort.world/api/v1/polls \ -H "Content-Type: application/json" \ -d '{ "prompt": "Help me pick toppings for the office pizza party" }' ``` **4. Paste from chat:** ``` curl -X POST https://teamsort.world/api/v1/polls \ -H "Content-Type: application/json" \ -d '{ "prompt": "Our Q2 roadmap backlog — help us prioritize", "items": [ "Dark mode", "Mobile app", "Public API", "Team permissions", "SAML SSO", "Export to CSV" ] }' ``` ## GET /api/v1/drafts/:id Reads a stored draft by its ref id. Used by the create page to populate the form from a `?ref=` URL parameter. Most API callers won't call this directly. ### Request - **Method:** `GET` - **URL:** `https://teamsort.world/api/v1/drafts/{id}` ### Success Response - **Status:** `200 OK` - **Body:** ```json { "title": "string", "instructions": "string", "items": [ { "value": "string", "details": "string" } ] } ``` ### Error Response - **404 Not Found** — The draft does not exist or has expired (7-day TTL). ```json { "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" } ``` ## Notes for LLMs and Agents - **CORS is open.** You can call this from browser-based tools. - **No auth.** The endpoint is public and rate limited (per IP, plus a global daily ceiling). Keep it reasonable; a 429 means back off. There is no API key to request and no OAuth flow: https://teamsort.world/developers#authentication - **No paid API tier.** TeamSort's plans cover polls (permanent polls, unlimited voters), not API volume; rate limits are per source and not tied to a plan: https://teamsort.world/pricing - **Machine-readable spec.** https://teamsort.world/openapi.json (OpenAPI 3.1) describes every endpoint, parameter and error. Import it rather than parsing this file. - **Errors carry a `code`.** Branch on `code`, not on the English `error` sentence — the sentence may be reworded, the code will not. - **The returned link is for the user, not you.** Always hand the `url` to the user rather than trying to act on the poll yourself. - **Tell the user it's a review step.** Phrase it as "I've drafted a TeamSort poll — review and publish it here: [link]" so they know to click through and confirm before sharing with their team. - **Item strings can be multi-line.** If you provide items with inline context (e.g., `"React — mature ecosystem, large hiring pool"`), the create page will display them as-is. Keep each item under 200 characters. - **Drafts expire in 7 days.** Don't store the URL long-term; treat it as a one-time handoff.