{
  "openapi": "3.1.0",
  "info": {
    "title": "rifts.to API",
    "version": "1.0.0",
    "description": "Create live surveys and read their answers. Requires an active rifts.to subscription; entitlement is checked on every request.",
    "contact": { "name": "rifts.to support", "email": "support@rifts.to" }
  },
  "servers": [{ "url": "https://rifts.to" }],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/api/v1/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Check a token and see what it may do",
        "responses": {
          "200": {
            "description": "The token is valid",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/UpgradeRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/surveys": {
      "get": {
        "operationId": "listSurveys",
        "summary": "List the surveys this account owns",
        "parameters": [
          {
            "name": "include_admin",
            "in": "query",
            "required": false,
            "description": "Set to 1 to add admin_token and admin_url to each row. Omitted by default so a routine listing does not return one credential per survey.",
            "schema": { "type": "string", "enum": ["1"] }
          },
          {
            "name": "include_closed",
            "in": "query",
            "required": false,
            "description": "Set to 0 to return only open surveys. Archived surveys are never returned.",
            "schema": { "type": "string", "enum": ["0"] }
          }
        ],
        "responses": {
          "200": {
            "description": "The caller's surveys, newest first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["surveys"],
                  "properties": {
                    "surveys": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/SurveyListItem" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/UpgradeRequired" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "post": {
        "operationId": "createSurvey",
        "summary": "Create a survey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateSurveyRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. This is the only response that always carries admin_token.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/CreatedSurvey" } }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/UpgradeRequired" },
          "409": {
            "description": "The account is at its survey cap, or the requested slug is taken",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "examples": {
                  "survey_limit_reached": {
                    "value": { "error": "survey limit reached", "code": "survey_limit_reached" }
                  },
                  "slug_taken": { "value": { "error": "slug taken", "code": "slug_taken" } }
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/v1/surveys/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The survey slug, for example fuzzy-sleepy-tornado.",
          "schema": { "type": "string" }
        }
      ],
      "get": {
        "operationId": "getSurvey",
        "summary": "Read a survey's questions and every answer",
        "responses": {
          "200": {
            "description": "The survey and its responses. Never carries admin_token.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/SurveyDetail" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/UpgradeRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "patch": {
        "operationId": "setSurveyStatus",
        "summary": "Close a survey, or reopen a closed one",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["status"],
                "properties": { "status": { "type": "string", "enum": ["open", "closed"] } }
              },
              "example": { "status": "closed" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "status"],
                  "properties": {
                    "id": { "type": "string" },
                    "status": { "type": "string", "enum": ["open", "closed"] }
                  }
                },
                "example": { "id": "fuzzy-sleepy-tornado", "status": "closed" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/UpgradeRequired" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": {
            "description": "Reopening a survey whose expiry has already passed. Reopening it would change a column and nothing a respondent would see, so it is refused rather than done quietly. Clear the expiry from the admin dashboard first.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" },
                "example": {
                  "error": "this survey has expired, so reopening it would not accept responses; change its expiry from the admin dashboard first",
                  "code": "survey_expired"
                }
              }
            }
          },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A rifts.to access token, minted at /en/account. Header only; a token in a query string is not accepted."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid JSON, a missing or over-long title, an invalid question shape, an invalid slug, or a PATCH status that is neither \"open\" nor \"closed\". The message is returned verbatim.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "multiple_choice questions must have at least one option" }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, revoked or expired token. Carries a WWW-Authenticate: Bearer challenge.",
        "headers": {
          "WWW-Authenticate": { "schema": { "type": "string" } }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "authentication required" }
          }
        }
      },
      "UpgradeRequired": {
        "description": "The account has no active subscription. Checked on every request, so a lapsed subscription stops working on the next call.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "upgrade required", "code": "entitlement_required" }
          }
        }
      },
      "NotFound": {
        "description": "The survey does not exist, or it belongs to another account. One branch on purpose: a distinct 403 would confirm that an id exists.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "not found" }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. The per-token call limit carries Retry-After; the custom-slug limit on POST does not.",
        "headers": { "Retry-After": { "schema": { "type": "string" } } },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/Error" },
            "example": { "error": "rate limit exceeded" }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string" }
        }
      },
      "Me": {
        "type": "object",
        "required": ["user_id", "scope", "kind"],
        "properties": {
          "user_id": { "type": "string" },
          "scope": { "type": "string" },
          "kind": { "type": "string", "enum": ["pat", "oauth"] }
        },
        "example": {
          "user_id": "8f1c2b6e-2f4a-4f7d-9d6b-1e2a3c4d5e6f",
          "scope": "surveys:read surveys:write",
          "kind": "pat"
        }
      },
      "Question": {
        "type": "object",
        "required": ["type", "text"],
        "properties": {
          "type": { "type": "string", "enum": ["multiple_choice", "free_text", "rating"] },
          "text": { "type": "string" },
          "options": {
            "type": "array",
            "items": { "type": "string" },
            "description": "Required for multiple_choice, with at least one entry. Ignored for the other types."
          },
          "index": {
            "type": "integer",
            "description": "Assigned by the server. Send it or leave it out; it is overwritten either way."
          }
        }
      },
      "CreateSurveyRequest": {
        "type": "object",
        "required": ["title", "questions"],
        "properties": {
          "title": { "type": "string", "maxLength": 200 },
          "questions": {
            "type": "array",
            "minItems": 1,
            "items": { "$ref": "#/components/schemas/Question" }
          },
          "slug": {
            "type": "string",
            "description": "Optional custom slug. Spends a separate 30-per-hour bucket and answers 409 slug_taken if it is in use."
          }
        },
        "example": {
          "title": "Retro sentiment",
          "questions": [
            { "type": "rating", "text": "How did the sprint go?" },
            {
              "type": "multiple_choice",
              "text": "Biggest blocker?",
              "options": ["Scope", "Reviews", "Flaky tests"]
            },
            { "type": "free_text", "text": "Anything else?" }
          ]
        }
      },
      "CreatedSurvey": {
        "type": "object",
        "required": ["id", "title", "url", "admin_token", "admin_url", "created_at"],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "admin_token": { "type": "string", "format": "uuid" },
          "admin_url": { "type": "string", "format": "uri" },
          "created_at": { "type": "string", "format": "date-time" }
        },
        "example": {
          "id": "fuzzy-sleepy-tornado",
          "title": "Retro sentiment",
          "url": "https://rifts.to/en/s/fuzzy-sleepy-tornado",
          "admin_token": "4b2f9a1c-7d3e-4c58-9f10-2a6b8c4d1e33",
          "admin_url": "https://rifts.to/en/admin/4b2f9a1c-7d3e-4c58-9f10-2a6b8c4d1e33",
          "created_at": "2026-08-12T14:03:11.482Z"
        }
      },
      "SurveyListItem": {
        "type": "object",
        "required": ["id", "title", "status", "created_at", "response_count", "url"],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "status": { "type": "string", "enum": ["open", "closed"] },
          "created_at": { "type": "string", "format": "date-time" },
          "response_count": { "type": "integer" },
          "url": { "type": "string", "format": "uri" },
          "admin_token": {
            "type": "string",
            "description": "Only when include_admin=1."
          },
          "admin_url": {
            "type": "string",
            "description": "Only when include_admin=1."
          }
        }
      },
      "ResponseItem": {
        "type": "object",
        "required": ["id", "submitted_at", "answers"],
        "description": "Carries no respondent identifier. The id is the row's own random UUID.",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "submitted_at": { "type": "string", "format": "date-time" },
          "answers": { "type": "array", "items": {} }
        }
      },
      "SurveyDetail": {
        "type": "object",
        "required": [
          "id",
          "title",
          "status",
          "created_at",
          "expires_at",
          "url",
          "questions",
          "response_count",
          "responses"
        ],
        "properties": {
          "id": { "type": "string" },
          "title": { "type": "string" },
          "status": {
            "type": "string",
            "enum": ["open", "closed", "expired"],
            "description": "The status the survey behaves as. expired appears when the stored status is open and expires_at has passed. A survey closed by hand reads closed whatever its expiry says."
          },
          "created_at": { "type": "string", "format": "date-time" },
          "expires_at": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Null for a survey created through the API by a subscribed account."
          },
          "url": { "type": "string", "format": "uri" },
          "questions": { "type": "array", "items": { "$ref": "#/components/schemas/Question" } },
          "response_count": { "type": "integer" },
          "responses": { "type": "array", "items": { "$ref": "#/components/schemas/ResponseItem" } }
        }
      }
    }
  }
}
