{
  "openapi": "3.1.0",
  "info": {
    "title": "rifts.to API",
    "version": "1.1.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"
          }
        },
        "parameters": [
          {
            "name": "view",
            "in": "query",
            "required": false,
            "description": "`full` (the default) returns every response. `aggregate` returns counts instead: a tally for every multiple-choice option including the ones nobody picked, the mean and per-point distribution of each rating, and how many people answered each free-text question. Use it when you want the numbers rather than the rows -- a survey with hundreds of written answers is far larger than its aggregate.",
            "schema": {
              "type": "string",
              "enum": [
                "full",
                "aggregate"
              ]
            }
          }
        ],
        "description": "With `?view=aggregate` it returns per-question counts instead of the responses themselves."
      },
      "patch": {
        "operationId": "updateSurvey",
        "summary": "Change a live survey: status, theme, questions or archived",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "description": "At least one field. Everything sent is validated before anything is written.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "open",
                      "closed"
                    ]
                  },
                  "theme": {
                    "$ref": "#/components/schemas/Theme"
                  },
                  "questions": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/Question"
                    },
                    "description": "Replaces the question list. Once the survey has responses the edit is restricted: questions may be added, but none removed, reordered, retyped, and no multiple-choice option renamed or dropped — answers are keyed by index and matched to options by exact string, so any of those would orphan collected data. Refused with 400."
                  },
                  "archived": {
                    "type": "boolean",
                    "description": "Hides the survey from the owner's listing. Never touches its status, its public link or its admin dashboard. Idempotent."
                  }
                }
              },
              "example": {
                "status": "closed",
                "theme": "ocean"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The survey after the change",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "status"
                  ],
                  "description": "The survey's id and status, plus each field the request changed.",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "open",
                        "closed"
                      ]
                    },
                    "theme": {
                      "$ref": "#/components/schemas/Theme"
                    },
                    "questions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Question"
                      }
                    },
                    "archived": {
                      "type": "boolean"
                    }
                  }
                },
                "example": {
                  "id": "fuzzy-sleepy-tornado",
                  "status": "closed",
                  "theme": "ocean"
                }
              }
            }
          },
          "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"
          }
        }
      }
    },
    "/api/v1/surveys/{id}/clone": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The survey's id (its public slug).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "cloneSurvey",
        "summary": "Run the same survey again as a new one",
        "description": "Copies a survey's questions into a brand new survey with its own link, its own admin token and no responses. The clone does not inherit the original's archived state or expiry: expiry follows the owner's entitlement at creation, so cloning an expired survey while subscribed produces a live one. Without a `theme` the clone keeps the original's palette.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Title for the copy. Defaults to the original's."
                  },
                  "slug": {
                    "type": "string",
                    "description": "Optional custom slug, same 30-per-hour bucket and 409 slug_taken as create."
                  },
                  "theme": {
                    "$ref": "#/components/schemas/Theme"
                  }
                }
              },
              "example": {
                "title": "Standup day check, week 2"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new survey, with both links and its admin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LaunchedSurvey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/UpgradeRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The account is at its survey cap, or the requested slug is taken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/surveys/{id}/save-as-template": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The survey's id (its public slug).",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "saveSurveyAsTemplate",
        "summary": "Save a survey's questions as a reusable template",
        "description": "The other direction from launching a template. The template is a snapshot, not a link: editing the survey afterwards never changes the template, and launching the template later never changes the survey.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name for the template. Defaults to the survey's title."
                  }
                }
              },
              "example": {
                "name": "Weekly standup"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The saved template",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "question_count": {
                      "type": "integer"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "saved_from": {
                      "type": "string",
                      "description": "The survey this template was taken from."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/UpgradeRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "The account is at its template cap",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/templates": {
      "get": {
        "operationId": "listTemplates",
        "summary": "List the saved templates this account owns",
        "responses": {
          "200": {
            "description": "The caller's templates, most recently updated first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "templates"
                  ],
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/UpgradeRequired"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "operationId": "createTemplate",
        "summary": "Save a question set for reuse",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "questions"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "questions": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/Question"
                    }
                  }
                }
              },
              "example": {
                "name": "Weekly standup",
                "questions": [
                  {
                    "type": "rating",
                    "text": "How's the week going?"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The saved template",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/UpgradeRequired"
          },
          "409": {
            "description": "The account is at its template cap",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "template limit reached",
                  "code": "template_limit_reached"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v1/templates/{id}/launch": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "The template id, from GET /api/v1/templates.",
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "launchTemplate",
        "summary": "Launch a template as a new live survey",
        "description": "Copies the template's questions into a fresh survey, open immediately. The copy is taken now: editing the template afterwards never changes a survey already launched from it.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "slug": {
                    "type": "string",
                    "description": "Optional custom slug, same 30-per-hour bucket and 409 slug_taken as create."
                  },
                  "theme": {
                    "$ref": "#/components/schemas/Theme"
                  }
                }
              },
              "example": {
                "theme": "ocean"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The live survey, with both links and its admin token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LaunchedSurvey"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/UpgradeRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "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"
          }
        }
      }
    }
  },
  "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",
        "description": "Properties beyond the ones listed here are stored as sent and returned unchanged by GET /api/v1/surveys/{id}, but nothing interprets them. A response may therefore carry fields this schema does not describe, depending on what created the survey.",
        "required": [
          "type",
          "text"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "multiple_choice",
              "free_text",
              "rating"
            ],
            "description": "A rating question is always answered on a fixed 1 to 10 scale. The scale is not configurable, and a property asking for a different range is stored without effect."
          },
          "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": "The question's position in the list, assigned by the server. Leave it out on a new question. On an edit, send each question back with the index it was last given: when a question carries a conditional rule, that is how a reorder is recognised and the rule moved with its trigger. A reorder the server cannot follow is a 400, not a silently repointed rule."
          },
          "requirement": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Requirement"
              }
            ],
            "description": "Whether an answer is required. Absent means always required, which is what every question created before this field is, so omitting it never changes behaviour."
          }
        }
      },
      "Requirement": {
        "type": "object",
        "description": "Optional or conditional requiredness. A conditional question is still shown to every respondent: the rule decides whether an answer is required, never whether the question appears.",
        "required": [
          "mode"
        ],
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "optional",
              "conditional"
            ],
            "description": "optional is never required. conditional is required exactly when `when` matches."
          },
          "when": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ConditionGroup"
              }
            ],
            "description": "Required when mode is conditional. Ignored otherwise."
          }
        }
      },
      "ConditionGroup": {
        "type": "object",
        "required": [
          "op",
          "conditions"
        ],
        "properties": {
          "op": {
            "type": "string",
            "enum": [
              "all",
              "any"
            ],
            "description": "all is AND across every condition, any is OR."
          },
          "conditions": {
            "type": "array",
            "minItems": 1,
            "maxItems": 5,
            "items": {
              "$ref": "#/components/schemas/Condition"
            }
          }
        }
      },
      "Condition": {
        "type": "object",
        "description": "Matches when the named question was answered with any of values.",
        "required": [
          "questionIndex",
          "values"
        ],
        "properties": {
          "questionIndex": {
            "type": "integer",
            "minimum": 0,
            "description": "Zero-based position in the questions array. Must be strictly earlier than the question carrying the rule, and must name a multiple_choice question. Pointing forward is refused, which is what makes a loop impossible to express."
          },
          "values": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Each entry must be exactly one of that question's options."
          }
        }
      },
      "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."
          },
          "theme": {
            "$ref": "#/components/schemas/Theme"
          }
        },
        "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",
          "theme"
        ],
        "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"
          },
          "theme": {
            "$ref": "#/components/schemas/Theme"
          }
        },
        "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",
          "theme"
        ],
        "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"
            }
          },
          "theme": {
            "$ref": "#/components/schemas/Theme"
          }
        }
      },
      "Theme": {
        "description": "A survey's respondent-page palette: one of the preset keys, or a custom {primary, background} pair of 6-digit hex colors. \"default\" is the house palette and clears any override. Presets are free with an account; a custom pair is part of the paid plan, which every /api/v1 caller already has.",
        "oneOf": [
          {
            "type": "string",
            "enum": [
              "default",
              "sunset",
              "ocean",
              "forest",
              "rose",
              "slate"
            ]
          },
          {
            "type": "object",
            "required": [
              "primary",
              "background"
            ],
            "properties": {
              "primary": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$"
              },
              "background": {
                "type": "string",
                "pattern": "^#[0-9a-fA-F]{6}$"
              }
            }
          }
        ],
        "example": "ocean"
      },
      "Template": {
        "description": "A saved set of questions. Not a live survey: no slug, no admin token, no responses.",
        "type": "object",
        "required": [
          "id",
          "name",
          "questions",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Question"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LaunchedSurvey": {
        "description": "A survey launched from a template. Same shape as a created survey, plus the template it came from.",
        "allOf": [
          {
            "$ref": "#/components/schemas/CreatedSurvey"
          },
          {
            "type": "object",
            "required": [
              "template_id"
            ],
            "properties": {
              "template_id": {
                "type": "string"
              }
            }
          }
        ]
      }
    }
  }
}
