{
  "openapi": "3.1.0",
  "info": {
    "title": "SettleIt API",
    "version": "1.0.0",
    "summary": "Create group decisions, share the link, read the results. No sign-up.",
    "description": "SettleIt makes four kinds of group decision: `poll` (pick one option), `checkin` (In / Maybe / Out), `availability` (find a time), and `ranking` (order options best-first).\n\nTypical agent flow: create a decision, give the people involved its `url`, then read it back later for results. Keep the returned `admin_token` private: it is the only way to close, reopen, or delete the decision.\n\nNo authentication is needed to create, read, or respond. An optional `X-Api-Key` header (issued by SettleIt for integrations) raises rate limits for callers on shared IPs.",
    "license": { "name": "MIT", "identifier": "MIT" }
  },
  "servers": [{ "url": "/api/v1" }],
  "externalDocs": { "description": "Agent guide", "url": "/llms.txt" },
  "paths": {
    "/decisions": {
      "post": {
        "operationId": "createDecision",
        "summary": "Create a decision",
        "description": "Send either `text` (natural language; SettleIt infers the type and parses options) or `question` (+ `options` for polls and rankings). Availability decisions must be created from `text` because SettleIt parses the dates and time window from it.",
        "parameters": [{ "$ref": "#/components/parameters/ApiKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateDecisionRequest" },
              "examples": {
                "pollFromText": { "value": { "text": "Dinner: pizza or thai" } },
                "checkinFromText": { "value": { "text": "Who's in for bowling Thursday?" } },
                "availabilityFromText": { "value": { "text": "Find a time for dinner next week", "timezone": "America/Los_Angeles" } },
                "structuredPoll": { "value": { "type": "poll", "question": "Where should we eat?", "options": ["Pizza", "Thai", "Tacos"], "closes_in_minutes": 120 } },
                "structuredRanking": { "value": { "type": "ranking", "question": "Movie night picks", "options": ["Dune", "Heat", "Up"] } }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "headers": { "Location": { "schema": { "type": "string" }, "description": "API URL of the new decision" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateDecisionResponse" } } }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/decisions/{id}": {
      "parameters": [{ "$ref": "#/components/parameters/DecisionId" }],
      "get": {
        "operationId": "getDecision",
        "summary": "Read a decision and its current results",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DecisionEnvelope" } } } },
          "404": { "$ref": "#/components/responses/Error" }
        }
      },
      "delete": {
        "operationId": "deleteDecision",
        "summary": "Delete a decision permanently",
        "security": [{ "adminToken": [] }],
        "responses": {
          "200": { "description": "Deleted", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "deleted": { "type": "string" } } } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/decisions/{id}/responses": {
      "parameters": [{ "$ref": "#/components/parameters/DecisionId" }],
      "post": {
        "operationId": "respondToDecision",
        "summary": "Vote or respond on behalf of one person",
        "description": "Fields depend on the decision's `type` (also listed in the decision's `respond.fields`):\n\n- `poll`: `voter_id` + `option_id` (or `option` with the exact label)\n- `ranking`: `voter_id` + `order` (every option id or label, best first)\n- `checkin`: `name` + `status` (`in`, `maybe`, `out`)\n- `availability`: `name` + `slot_ids`\n\nResponding again with the same `voter_id` or `name` (case-insensitive) replaces that person's earlier response. Use one stable `voter_id` per human, such as their name, so each person counts once.",
        "parameters": [{ "$ref": "#/components/parameters/ApiKey" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RespondRequest" },
              "examples": {
                "pollVote": { "value": { "voter_id": "Alex", "option": "Thai" } },
                "checkin": { "value": { "name": "Alex", "status": "in" } },
                "availability": { "value": { "name": "Alex", "slot_ids": [101, 102] } },
                "ranking": { "value": { "voter_id": "Alex", "order": ["Heat", "Dune", "Up"] } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "updated", "decision"],
                  "properties": {
                    "ok": { "type": "boolean" },
                    "updated": { "type": "boolean", "description": "True when this replaced the person's earlier response" },
                    "decision": { "$ref": "#/components/schemas/Decision" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "401": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" },
          "409": { "$ref": "#/components/responses/Error" },
          "422": { "$ref": "#/components/responses/Error" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/decisions/{id}/close": {
      "parameters": [{ "$ref": "#/components/parameters/DecisionId" }],
      "post": {
        "operationId": "closeDecision",
        "summary": "Stop accepting responses",
        "security": [{ "adminToken": [] }],
        "responses": {
          "200": { "description": "Closed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DecisionEnvelope" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/decisions/{id}/reopen": {
      "parameters": [{ "$ref": "#/components/parameters/DecisionId" }],
      "post": {
        "operationId": "reopenDecision",
        "summary": "Accept responses again (clears a close time that has passed)",
        "security": [{ "adminToken": [] }],
        "responses": {
          "200": { "description": "Reopened", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DecisionEnvelope" } } } },
          "401": { "$ref": "#/components/responses/Error" },
          "403": { "$ref": "#/components/responses/Error" },
          "404": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "adminToken": { "type": "http", "scheme": "bearer", "description": "The `admin_token` returned when the decision was created." }
    },
    "parameters": {
      "DecisionId": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Decision id, the last path segment of its share URL (`/p/{id}`)." },
      "ApiKey": { "name": "X-Api-Key", "in": "header", "required": false, "schema": { "type": "string" }, "description": "Optional integration key; raises rate limits. Unknown keys are rejected with 401." }
    },
    "responses": {
      "Error": {
        "description": "Error",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limited",
        "headers": { "Retry-After": { "schema": { "type": "integer" }, "description": "Seconds to wait" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "CreateDecisionRequest": {
        "type": "object",
        "properties": {
          "text": { "type": "string", "minLength": 2, "maxLength": 1000, "description": "Natural-language prompt, e.g. \"Dinner: pizza or thai\". Max 200 characters except rankings." },
          "type": { "type": "string", "enum": ["poll", "checkin", "availability", "ranking"], "description": "Optional with `text` (inferred). With `question`, defaults to `poll` when options are given, otherwise `checkin`." },
          "question": { "type": "string", "minLength": 2, "maxLength": 200 },
          "options": { "type": "array", "items": { "type": "string", "maxLength": 80 }, "description": "Polls: 2-5 unique options. Rankings: 2-12." },
          "closes_in_minutes": { "type": "integer", "minimum": 10, "maximum": 10080, "description": "Polls only. Automatically close after this many minutes." },
          "timezone": { "type": "string", "maxLength": 64, "example": "America/Los_Angeles", "description": "The creator's IANA time zone. Availability slots, and words like \"tomorrow\" or \"next week\", are interpreted in it. Recommended for availability; defaults to the server time zone (UTC). Other names (offsets, abbreviations like PST) return 422 `invalid_timezone`." }
        }
      },
      "CreateDecisionResponse": {
        "type": "object",
        "required": ["decision", "admin_token", "manage_url"],
        "properties": {
          "decision": { "$ref": "#/components/schemas/Decision" },
          "admin_token": { "type": "string", "description": "Secret. Send as `Authorization: Bearer` to close, reopen, or delete. Do not share it with participants." },
          "manage_url": { "type": "string", "description": "Browser link that gives the creator admin controls. Secret, like the token." }
        }
      },
      "DecisionEnvelope": {
        "type": "object",
        "required": ["decision"],
        "properties": { "decision": { "$ref": "#/components/schemas/Decision" } }
      },
      "Decision": {
        "type": "object",
        "required": ["id", "type", "question", "status", "url", "api_url", "respond"],
        "properties": {
          "id": { "type": "string" },
          "type": { "type": "string", "enum": ["poll", "checkin", "availability", "ranking"] },
          "question": { "type": "string" },
          "status": { "type": "string", "enum": ["open", "closed"] },
          "closes_at": { "type": ["string", "null"], "format": "date-time" },
          "created_at": { "type": ["string", "null"], "format": "date-time" },
          "url": { "type": "string", "description": "Share this with participants." },
          "api_url": { "type": "string" },
          "respond": { "type": "object", "description": "Fields accepted by POST /decisions/{id}/responses for this type.", "properties": { "fields": { "type": "object", "additionalProperties": { "type": "string" } } } },
          "options": { "type": "array", "description": "poll and ranking", "items": { "$ref": "#/components/schemas/Option" } },
          "total_votes": { "type": "integer", "description": "poll" },
          "participants": { "type": "integer", "description": "ranking and availability" },
          "leaders": { "type": "array", "description": "poll and ranking: options tied for first (empty with no responses)", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "label": { "type": "string" } } } },
          "summary": { "type": "object", "description": "checkin", "properties": { "in": { "type": "integer" }, "maybe": { "type": "integer" }, "out": { "type": "integer" }, "total": { "type": "integer" } } },
          "responses": { "type": "object", "description": "checkin: names by status", "properties": { "in": { "type": "array", "items": { "type": "string" } }, "maybe": { "type": "array", "items": { "type": "string" } }, "out": { "type": "array", "items": { "type": "string" } } } },
          "timezone": { "type": "string", "description": "availability: IANA time zone of the slots (the creator's `timezone`, or UTC)" },
          "slots": { "type": "array", "description": "availability", "items": { "$ref": "#/components/schemas/Slot" } },
          "best_slot_ids": { "type": "array", "description": "availability: slots with the most people (empty with no responses)", "items": { "type": "integer" } }
        }
      },
      "Option": {
        "type": "object",
        "required": ["id", "label"],
        "properties": {
          "id": { "type": "integer" },
          "label": { "type": "string" },
          "votes": { "type": "integer", "description": "poll" },
          "percent": { "type": "integer", "description": "poll" },
          "is_suggestion": { "type": "boolean", "description": "poll: added by a participant" },
          "rank": { "type": "integer", "description": "ranking: current position, 1 = best" },
          "points": { "type": "integer", "description": "ranking: Borda points" },
          "first_place_votes": { "type": "integer", "description": "ranking" },
          "average_rank": { "type": ["number", "null"], "description": "ranking" }
        }
      },
      "Slot": {
        "type": "object",
        "required": ["id", "start_at", "end_at", "label", "count", "names"],
        "properties": {
          "id": { "type": "integer" },
          "start_at": { "type": "string", "format": "date-time" },
          "end_at": { "type": "string", "format": "date-time" },
          "label": { "type": "string", "example": "Tue Nov 3 6:00 PM" },
          "count": { "type": "integer" },
          "names": { "type": "array", "items": { "type": "string" } }
        }
      },
      "RespondRequest": {
        "type": "object",
        "properties": {
          "voter_id": { "type": "string", "maxLength": 128, "description": "poll, ranking: stable id for the person (their name works)" },
          "option_id": { "type": "integer", "description": "poll" },
          "option": { "type": "string", "description": "poll: exact option label, alternative to option_id" },
          "order": { "type": "array", "items": { "type": ["integer", "string"] }, "description": "ranking: every option id or label, best first" },
          "name": { "type": "string", "maxLength": 80, "description": "checkin, availability" },
          "status": { "type": "string", "enum": ["in", "maybe", "out"], "description": "checkin" },
          "slot_ids": { "type": "array", "items": { "type": "integer" }, "description": "availability" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message"],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code.",
                "enum": ["missing_input", "invalid_json", "invalid_type", "invalid_text", "text_too_long", "invalid_question", "invalid_options", "invalid_closes_in", "parse_failed", "no_slots", "content_not_allowed", "not_found", "route_not_found", "decision_closed", "voter_id_required", "invalid_option", "invalid_order", "invalid_name", "invalid_status", "invalid_slots", "admin_token_required", "forbidden", "invalid_api_key", "rate_limited", "internal_error"]
              },
              "message": { "type": "string", "description": "Human-readable explanation, safe to show to the user." },
              "retry_after": { "type": "integer", "description": "Seconds to wait (rate_limited only)" }
            }
          }
        }
      }
    }
  }
}
