{
  "openapi": "3.1.0",
  "info": {
    "title": "Subo API",
    "version": "1.0.0",
    "description": "The Subo API lets you programmatically create conversational surveys and polls, auto-generate scripts with AI, collect responses in real-time, and run AI analysis \u2014 perfect for developers and autonomous AI agents. Share surveys on the web or natively inside your community's Discord server. Webhooks deliver real-time event notifications when projects open or close, participants submit responses, and AI analysis completes.\n\n## Quickstart\n\nGet up and running in under 5 minutes.\n\n### Step 1: Get Your API Key\n\n1. Go to [https://app.subo.gg/app/account](https://app.subo.gg/app/account)\n2. Navigate to **Community Account Tab \u2192 API Keys**\n3. Create a new key \u2014 it is scoped to that community and inherits your role (optionally limit it to Creator access level)\n4. Copy the key \u2014 it starts with `sbo_live_...` and is shown only once\n\nAll requests must include this key in the header:\n\n```http\nX-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\n### Step 2: Find Your Community ID\n\nMost endpoints require a `communityId`. Retrieve it with:\n\n```bash\ncurl -X GET \"https://api.subo.ai/v1/communities\" \\\n  -H \"X-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx\"\n```\n\nFor Discord communities the community ID is the Discord server (snowflake) ID, returned as a string.\n\n### Step 3: Create Your First Project (with AI Script Generation)\n\nThe fastest path \u2014 pass an `intent` and let AI generate the entire survey script:\n\n```bash\ncurl -X POST \"https://api.subo.ai/v1/communities/{communityId}/projects\" \\\n  -H \"X-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"name\": \"Event Feedback Survey\",\n    \"type\": \"convo\",\n    \"intent\": \"Measure member satisfaction with last week'\\''s community event and gather suggestions for future events\",\n    \"privacy_mode\": \"semi-private\"\n  }'\n```\n\nThe `201` response includes a `script` field with the generated blocks and `credits_used`. You can also provide your own `script.blocks` array manually for full control.\n\n### Step 4: Open the Project (Make It Live)\n\nProjects start with `status: inactive`. Activate so members can respond:\n\n```bash\ncurl -X POST \"https://api.subo.ai/v1/communities/{communityId}/projects/{projectId}/open\" \\\n  -H \"X-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Idempotency-Key: $(uuidgen)\" \\\n  -d '{\n    \"delivery\": {\n      \"audience\": { \"participation\": \"private\" }\n    }\n  }'\n```\n\n### Step 5: Retrieve Responses & Trigger Analysis\n\nList responses:\n\n```bash\ncurl -X GET \"https://api.subo.ai/v1/communities/{communityId}/projects/{projectId}/responses\" \\\n  -H \"X-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx\"\n```\n\nTrigger AI summarization of open-text answers:\n\n```bash\ncurl -X POST \"https://api.subo.ai/v1/communities/{communityId}/projects/{projectId}/analysis\" \\\n  -H \"X-API-Key: sbo_live_xxxxxxxxxxxxxxxxxxxxxxxx\" \\\n  -H \"Idempotency-Key: $(uuidgen)\"\n```\n\n### Next Steps & Best Practices\n\n- **Use Webhooks** (strongly recommended) instead of polling \u2014 register a webhook for `response.submitted` and `analysis.completed` events.\n- Always send an `Idempotency-Key` (UUID v4) on write operations (`POST`, `PUT`, `DELETE`) to make them safe to retry.\n- Check rate limit headers in every response (`X-RateLimit-Remaining`, etc.).\n- For AI agents: see [`llms.txt`](https://subo.gg/llms.txt) for a machine-optimized summary of every endpoint, and the [API quickstart](https://subo.gg/api) for end-to-end recipes.\n\n## Authentication\nPass an API key in the `X-API-Key` header. Keys are scoped to a community and inherit the role of the issuing user. Generate keys under [Account](https://app.subo.gg/app/account).\n\n## Versioning\nBreaking changes are released under a new version prefix (`/v2/`). Additive changes (new fields, endpoints) may be made to v1 without a version bump.\n\n## Rate Limits\nLimits are per API key, per minute (sliding window), based on the community's plan tier:\n\n| Tier | Requests / minute |\n|------|------------------|\n| basic | 60 |\n| premium | 300 |\n| vip | 600 |\n| custom | 1 000 |\n\nEvery response includes these headers:\n\n| Header | Value |\n|--------|-------|\n| `X-RateLimit-Limit` | The limit for your tier |\n| `X-RateLimit-Remaining` | Requests left in the current minute |\n| `X-RateLimit-Reset` | Unix timestamp when the current window resets |\n\nWhen the limit is exceeded the API returns **429** with body `{\"error\": \"rate_limit_exceeded\", \"retry_after\": <seconds>}` and a `Retry-After: <seconds>` header. Wait the indicated number of seconds before retrying.\n\n## Idempotency\nAll write operations accept an `Idempotency-Key` header (UUID v4). Duplicate keys within 24 hours return the cached response without re-executing.\n\n## IDs\nAll IDs are returned as strings. Discord snowflakes are serialized as strings to avoid JavaScript precision loss.",
    "contact": {
      "name": "Subo Support",
      "url": "https://subo.gg/support"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.subo.ai",
      "description": "Production"
    },
    {
      "url": "http://localhost:5002",
      "description": "Local development"
    },
    {
      "url": "{customUrl}",
      "description": "Custom (enter URL)",
      "variables": {
        "customUrl": {
          "default": "",
          "description": "Enter your server URL (e.g. https://subo.store)"
        }
      }
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key scoped to a community. Pass as `X-API-Key: sbo_live_...`"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code. Safe to match programmatically. Common codes: `missing_api_key`, `invalid_api_key`, `invalid_request`, `forbidden`, `not_found`, `conflict`, `payment_required`, `tier_required`, `channel_required`, `rate_limit_exceeded`.",
            "example": "not_found"
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. May be absent on some error codes.",
            "example": "Resource not found"
          }
        },
        "example": {
          "error": "not_found",
          "message": "Resource not found"
        }
      },
      "Error429": {
        "type": "object",
        "required": [
          "error",
          "retry_after"
        ],
        "description": "Rate limit response body. Also includes `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers on every response.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limit_exceeded"
            ],
            "example": "rate_limit_exceeded"
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before retrying. Mirrors the `Retry-After` response header.",
            "example": 14
          }
        },
        "example": {
          "error": "rate_limit_exceeded",
          "retry_after": 14
        }
      },
      "Pagination": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer"
          },
          "per_page": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "Community": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "platform": {
            "type": "string",
            "enum": [
              "discord"
            ]
          },
          "tier": {
            "type": "string",
            "enum": [
              "basic",
              "premium",
              "vip",
              "custom"
            ]
          },
          "bot_credits": {
            "type": "integer"
          },
          "locale": {
            "type": "string",
            "nullable": true
          },
          "icon_url": {
            "type": "string",
            "nullable": true
          },
          "created_date": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "total_project_count": {
            "type": "integer",
            "description": "Total number of projects ever created"
          },
          "active_project_count": {
            "type": "integer",
            "description": "Number of currently active (live or pending) projects"
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Community topic tags"
          },
          "user_setup_id": {
            "type": "string",
            "nullable": true,
            "description": "Internal Subo user ID of the member who last completed the community bot setup wizard. Read-only. Null if setup has not been run."
          }
        }
      },
      "CommunitySettings": {
        "type": "object",
        "properties": {
          "community_id": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "nullable": true
          },
          "default_anonymous_mode": {
            "type": "string",
            "enum": [
              "yes",
              "no",
              "more_anonymous"
            ],
            "nullable": true
          },
          "use_xp": {
            "type": "boolean",
            "nullable": true
          },
          "xp_name": {
            "type": "string",
            "nullable": true
          },
          "xp_per_question": {
            "type": "integer",
            "minimum": 0,
            "nullable": true
          },
          "use_leader_board": {
            "type": "boolean",
            "nullable": true
          },
          "thank_you_type": {
            "type": "string",
            "enum": [
              "default",
              "form_standard",
              "custom"
            ],
            "nullable": true
          },
          "thank_you_custom_message": {
            "type": "string",
            "nullable": true
          },
          "thank_you_custom_label": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "community_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ],
            "description": "Lifecycle state. `POST /open` \u2192 `active` (accepting responses). `POST /close` \u2192 `inactive` (closed). Projects start `inactive` on creation."
          },
          "info": {
            "type": "object",
            "description": "Core identity and aggregate counts for this project",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "convo",
                  "poll"
                ]
              },
              "created_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "block_count": {
                "type": "integer"
              },
              "is_cloneable": {
                "type": "boolean",
                "description": "Whether this project may be cloned by other communities (read-only opt-out flag)"
              },
              "author_id": {
                "type": "string",
                "nullable": true,
                "description": "DB user ID of the project creator"
              },
              "author_name": {
                "type": "string",
                "nullable": true,
                "description": "Display name of the project creator"
              },
              "completion_count": {
                "type": "integer",
                "description": "Number of participant sessions that reached the end of the project (i.e. `saveFinalStatus` was called). For polls this equals the number of votes cast; for convos it counts only fully-completed runs. Decremented when a poll voter retracts their answer."
              },
              "response_count": {
                "type": "integer",
                "description": "Running total of individual question-answer rows written across all participants. Each block a participant answers increments this by 1. For a 5-block convo with 10 completions this can reach 50. To count distinct participant sessions use the `total` field from `GET /responses`."
              }
            }
          },
          "settings": {
            "type": "object",
            "description": "Participation rules and result visibility for this project",
            "properties": {
              "privacy_mode": {
                "type": "string",
                "enum": [
                  "transparent",
                  "semi-private",
                  "anonymous"
                ],
                "nullable": true,
                "description": "Controls response visibility and identity disclosure.\n\n- **`transparent`** \u2014 All community members can see exactly who answered what for every question.\n- **`semi-private`** \u2014 Only the project creator and community admins can view individual responses (who answered what). The rest of the community sees aggregated results only if the creator chooses to share them.\n- **`anonymous`** \u2014 Respondent identity is hidden from everyone, including the creator and admins. Data is pseudo-anonymous: answers are recorded but not linked to a user ID or display name, unless a respondent volunteers that information in an open-text answer. **Caveat:** XP and achievements can still be granted in anonymous projects, so participation is not entirely hidden \u2014 this is especially relevant in small communities where deduction is easier."
              },
              "max_completes_per_user": {
                "type": "integer",
                "minimum": 1,
                "nullable": true
              },
              "change_poll": {
                "type": "boolean",
                "nullable": true,
                "description": "Allow respondents to change their poll answer after submission"
              },
              "poll_results_mode": {
                "type": "string",
                "enum": [
                  "public",
                  "voters_only",
                  "hidden"
                ],
                "nullable": true,
                "description": "`public`: results visible in the poll embed; `voters_only`: only voters can see results; `hidden`: results hidden until poll closes"
              },
              "reveal_results": {
                "type": "boolean",
                "nullable": true,
                "description": "Post a results summary to the results channel when the project closes"
              }
            }
          },
          "delivery": {
            "type": "object",
            "nullable": true,
            "properties": {
              "opening": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "manual",
                      "scheduled"
                    ]
                  },
                  "time": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              },
              "closing": {
                "type": "object",
                "properties": {
                  "mode": {
                    "type": "string",
                    "enum": [
                      "manual",
                      "scheduled"
                    ]
                  },
                  "time": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                }
              },
              "audience": {
                "type": "object",
                "properties": {
                  "participation": {
                    "type": "string",
                    "enum": [
                      "private",
                      "open_web"
                    ]
                  },
                  "response_channel": {
                    "type": "string",
                    "enum": [
                      "discord",
                      "web"
                    ]
                  },
                  "required_role_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "verified_source_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  }
                }
              },
              "invitation": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "post_channel_id": {
                    "type": "string",
                    "nullable": true
                  },
                  "message": {
                    "type": "string",
                    "nullable": true
                  },
                  "thumbnail_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true
                  },
                  "image_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true
                  },
                  "border_color": {
                    "type": "string",
                    "nullable": true,
                    "description": "Hex colour without '#' for the embed border while the project is open (e.g. `e1287e`)"
                  },
                  "border_color_closed": {
                    "type": "string",
                    "nullable": true,
                    "description": "Hex colour without '#' for the embed border when the project is closed (e.g. `745399`). Premium only."
                  },
                  "footer": {
                    "type": "string",
                    "nullable": true,
                    "description": "Text shown below the invitation embed. Premium only."
                  },
                  "embed_config": {
                    "type": "object",
                    "nullable": true,
                    "description": "Invitation embed layout \u2014 whether project info shows inline (`embed`) or behind a button, plus per-row visibility toggles",
                    "properties": {
                      "info_display": {
                        "type": "string",
                        "enum": [
                          "embed",
                          "button"
                        ],
                        "nullable": true
                      },
                      "rows": {
                        "type": "object",
                        "additionalProperties": {
                          "type": "boolean"
                        },
                        "description": "Per-row visibility toggles for the invitation embed (open map, e.g. status/privacyMode/roles/xpReward)"
                      }
                    }
                  },
                  "answer_button_label": {
                    "type": "string",
                    "nullable": true,
                    "description": "Custom label for the answer/vote button. Premium only."
                  },
                  "answer_button_color": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Discord button style for the answer button (1 primary, 2 secondary, 3 success, 4 danger). Premium only."
                  }
                }
              }
            }
          },
          "rewards": {
            "type": "object",
            "nullable": true,
            "properties": {
              "discord_role_id": {
                "type": "string",
                "nullable": true
              },
              "achievement_id": {
                "type": "string",
                "nullable": true
              },
              "use_xp": {
                "type": "boolean",
                "nullable": true
              },
              "achievement_enabled": {
                "type": "boolean",
                "nullable": true,
                "description": "True if this project grants an achievement on completion"
              },
              "achievement_image_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Badge image URL for the achievement"
              },
              "achievement_name": {
                "type": "string",
                "nullable": true,
                "description": "Custom achievement name; null uses the computed default"
              },
              "achievement_role_id": {
                "type": "string",
                "nullable": true,
                "description": "Discord role snowflake granted alongside the achievement"
              }
            }
          },
          "presentation": {
            "type": "object",
            "nullable": true,
            "properties": {
              "theme_id": {
                "type": "integer",
                "nullable": true
              },
              "interviewer_id": {
                "type": "integer",
                "nullable": true
              },
              "branding": {
                "type": "string",
                "nullable": true
              },
              "chart_emoji": {
                "type": "string",
                "nullable": true,
                "maxLength": 1,
                "description": "Emoji used as the bar character in poll result embeds"
              }
            }
          },
          "scoring": {
            "type": "object",
            "nullable": true,
            "description": "Quiz and scoring configuration for this project",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "When true, blocks may carry `correct_answer_index`, `when_correct`, `when_incorrect`, and per-option `score_values` for quiz scoring. Enable via `POST /projects` or `PUT /projects/{projectId}` with `scoring_enabled: true`."
              },
              "buckets": {
                "type": "array",
                "description": "Score dimensions defined for this project. Each bucket's `name` (lowercased, spaces replaced with underscores) is the key used in `score_values` on block options and in template variables such as `[score_gryffindor]`. Empty when `enabled` is false.",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "Display name of the bucket (e.g. `Gryffindor`). Variable key is this value lowercased with spaces replaced by underscores."
                    },
                    "order": {
                      "type": "integer",
                      "description": "Display order (0-based)."
                    }
                  }
                }
              }
            }
          }
        }
      },
      "Condition": {
        "type": "object",
        "description": "A skip-logic condition for `show_when` / `hide_when`. Either a **leaf** (`block` or `variable`, plus `op` and `value`) or a **group** (`all` for AND, `any` for OR, each a list of conditions).",
        "properties": {
          "block": {
            "type": "string",
            "nullable": true,
            "description": "Name of the block to test. Resolved at runtime by its stored, title-cased name (you may pass any casing)."
          },
          "variable": {
            "type": "string",
            "nullable": true,
            "description": "Raw variable token instead of a block name: `score`, `score_<bucket>`, `correct_answers`, `q3`, \u2026"
          },
          "op": {
            "type": "string",
            "nullable": true,
            "enum": [
              "eq",
              "ne",
              "lt",
              "gt",
              "gte",
              "lte",
              "in"
            ],
            "description": "Comparison. `in` requires a list `value`."
          },
          "value": {
            "nullable": true,
            "description": "String, number, boolean, or (for `op: in`) a list. Strings may not contain a double-quote character."
          },
          "all": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/Condition"
            },
            "description": "AND of nested conditions."
          },
          "any": {
            "type": "array",
            "nullable": true,
            "items": {
              "$ref": "#/components/schemas/Condition"
            },
            "description": "OR of nested conditions."
          }
        }
      },
      "Block": {
        "type": "object",
        "required": [
          "type",
          "prompt"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "nullable": true,
            "readOnly": true
          },
          "name": {
            "type": "string",
            "nullable": true,
            "description": "Variable token name for this block. Used in pipe variables (`[q_name]`), precondition expressions (`NOT((q_name=\"value\"))`), and calculated formulas. Auto-assigned as `q0`, `q1`, \u2026 (by position) when omitted. Must be unique within the script."
          },
          "section": {
            "type": "string",
            "nullable": true,
            "enum": [
              "main",
              "intro",
              "outro"
            ],
            "description": "Block section. `intro` blocks are shown before the survey begins (editable and deletable). `outro` blocks are returned only when `GET /script?expand=outro` is used and are read-only in script write endpoints."
          },
          "type": {
            "type": "string",
            "enum": [
              "single_punch",
              "multi_punch",
              "open_text",
              "open_numeric",
              "rating",
              "opinion_scale",
              "nps",
              "ranking",
              "content_block",
              "action_block",
              "calculated_block"
            ]
          },
          "answer_style": {
            "type": "string",
            "enum": [
              "full_text",
              "emoji_only",
              "select_menu",
              "buttons"
            ],
            "nullable": true,
            "description": "How answer options are displayed. `full_text`: labelled buttons; `emoji_only`: keycap emoji buttons; `select_menu`: dropdown. Applies to single_punch and multi_punch blocks, to `ranking` (which takes `full_text` or `emoji_only` only), and to the scale family (`rating` / `opinion_scale` / `nps`), which accept only `buttons` (default) or `select_menu` \u2014 a scale's point rendering is set with `scale.icon`, not here. A select menu is the better widget from six labeled points up, where a button row wraps. Sending `full_text` or `emoji_only` on a scale returns `400 invalid_request`. Null inherits the project default. For Discord-delivered projects, `full_text` and `select_menu` reject options that exceed the Discord character limit (40 for buttons, 50 for select menus) or carry more than one emoji that needs rendering (a button/select label has a single emoji slot) \u2014 the request returns `400 invalid_request` with a `violations` array. Use `emoji_only` (its answer list renders every emoji), shorten the option, or remove the extra emoji."
          },
          "color": {
            "type": "string",
            "nullable": true,
            "description": "Discord embed accent color as a hex string without the leading `#` (e.g. `e1287e`). Applies to content_block and action_block embeds when the project is Discord-delivered. **Requires Premium tier.** Ignored on read if community is on the Basic tier; blocks authored via the API will still persist the value, but it will not render until the community upgrades."
          },
          "image_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Image displayed above the question prompt. **Requires Premium tier** to persist via the editor; readable by all tiers."
          },
          "thumbnail_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Thumbnail image displayed top-right of the Discord embed. Available on all block types. **Requires Premium tier** to persist via the editor; readable by all tiers."
          },
          "prompt": {
            "type": "string",
            "minLength": 1
          },
          "options": {
            "type": "array",
            "nullable": true,
            "description": "Required for single_punch, multi_punch and ranking blocks \u2014 on a ranking these are the ITEMS to be ranked (2-20 of them). Optional for rating / opinion_scale / nps, where options are per-point presentation only: send one option per point with `value` set to that point's NUMBER (e.g. \"1\"\u2026\"5\") carrying its `emoji` or its `label`. A `label` on every point is the Likert mechanism \u2014 the respondent taps \"Strongly agree\" while the stored answer stays the number 5, so averages keep working. Omit options entirely for a plain star or number scale.",
            "items": {
              "type": "object",
              "required": [
                "value"
              ],
              "properties": {
                "id": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Backend answer id. Omit (or send null) to insert a new option; include the existing id to update in place \u2014 preserves response.answer_id references through value / emoji / score / order edits. On reads this is always populated."
                },
                "value": {
                  "type": "string",
                  "minLength": 1
                },
                "label": {
                  "type": "string",
                  "nullable": true
                },
                "emoji": {
                  "type": "string",
                  "nullable": true,
                  "description": "Per-option emoji stored separately from `value`. Displayed alongside the label by the renderer. Discord buttons and select options have a single emoji slot, so this emoji plus any emoji in `value` must total one renderable emoji for `full_text`/`select_menu` styles; otherwise the write is rejected (use `emoji_only`)."
                },
                "score_values": {
                  "type": "object",
                  "nullable": true,
                  "description": "Per-bucket score weights for this option. Keys are bucket names (lowercased, spaces \u2192 underscores); values are numeric weights. Only present when `scoring.enabled` is true on the project.",
                  "additionalProperties": {
                    "type": "number"
                  }
                },
                "display_order": {
                  "type": "integer",
                  "nullable": true,
                  "description": "Explicit 0-based display position within the question. Omit on create to default to array index."
                },
                "anchor_position": {
                  "type": "string",
                  "nullable": true,
                  "enum": [
                    "last",
                    null
                  ],
                  "description": "When set to 'last', this option is pinned to the end of the list instead of being shuffled \u2014 the usual home for an \"Other\" or \"None of the above\" choice. Only has an effect when the block sets `randomize_options: true`; without it every option keeps its stored order anyway. Null = participates in normal ordering."
                }
              }
            }
          },
          "min": {
            "type": "integer",
            "nullable": true,
            "description": "Min selections (multi_punch) or min value (open_numeric)"
          },
          "max": {
            "type": "integer",
            "nullable": true,
            "description": "Max selections (multi_punch) or max value (open_numeric)"
          },
          "required": {
            "type": "boolean",
            "nullable": true,
            "description": "Whether a response is required to advance"
          },
          "precondition": {
            "type": "string",
            "nullable": true,
            "description": "Raw skip-logic **hide expression**: when it evaluates to `true` the block is hidden; when `false` (or null) it is shown. **Prefer the structured `show_when` / `hide_when` fields below** \u2014 they compile to this and avoid its quoting/operator/casing pitfalls. Reference a prior block by its `name` (matched case-sensitively against the stored, title-cased name, e.g. a block named `NotifyPref` is referenced as `Notifypref`) or by position as `q1`, `q2`, \u2026 `qN`; named references are recommended because positions shift when blocks are added or removed. Operators: `=` (or `==`), `!=`, `<`, `>`, `>=`, `<=`, `in` (membership/substring), `AND`, `OR`, `NOT`. String literals use double quotes. To **show only when** a condition holds, wrap it: `NOT((Notifypref = \"Other\"))`. Invalid expressions (unknown variable, syntax error) are rejected with `400 invalid_request` at write time rather than silently failing open. See the Script tag description for the full reference."
          },
          "show_when": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Condition"
              }
            ],
            "nullable": true,
            "description": "Structured skip logic: show this block **only when** the condition is true (and hide it otherwise). The API compiles it to a correct `precondition` for you \u2014 the right operator, quoting, block-name casing, and the hide-when-true inversion. Mutually exclusive with `precondition` and with `hide_when`. Example: `{\"block\": \"Notifytiming\", \"op\": \"eq\", \"value\": \"Other\"}`."
          },
          "hide_when": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Condition"
              }
            ],
            "nullable": true,
            "description": "Structured skip logic: hide this block **when** the condition is true. Mutually exclusive with `precondition` and with `show_when`."
          },
          "action_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "give_xp",
              "give_achievement",
              "give_role"
            ],
            "description": "Required for `action_block`. Null for all other block types."
          },
          "action_config": {
            "type": "object",
            "nullable": true,
            "description": "Configuration for `action_block`.",
            "properties": {
              "xp_amount": {
                "type": "integer",
                "nullable": true,
                "description": "XP to grant for `give_xp`."
              },
              "xp_role_id": {
                "type": "integer",
                "nullable": true,
                "description": "Achievement ID to grant for `give_achievement`."
              },
              "role_id": {
                "type": "string",
                "nullable": true,
                "description": "Discord role snowflake to grant for `give_role`."
              },
              "fire_scope": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "respondent",
                  "session"
                ],
                "description": "Idempotency scope. `respondent`: once total; `session`: once per completion session."
              }
            }
          },
          "correct_answer_ids": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "nullable": true,
            "description": "Canonical correct-answer field. Array of real `option.id` values that are correct. Single-element today (MVP single-select grading); array type future-proofs multi-select. Survives option reorder because ids are stable. Only meaningful when `scoring_enabled` is true on the project."
          },
          "correct_answer_index": {
            "type": "integer",
            "nullable": true,
            "deprecated": true,
            "description": "Deprecated \u2014 use `correct_answer_ids` instead. 0-based position into the current `options` array of the first correct answer. Still accepted on input (translated to the option's `id` at write time) and emitted on output for back-compat, but breaks when options are reordered. Only meaningful when `scoring_enabled` is true."
          },
          "when_correct": {
            "type": "string",
            "nullable": true,
            "description": "Message shown to the respondent immediately after they answer correctly. Only meaningful when `correct_answer_ids` is set. Supports template variables: [UserName], [answer] (their response), [score], [max_score], [score_correct], [max_score_correct], [correct_answers], [max_correct_answers], [score_<bucket>], [max_score_<bucket>], [score_correct_<bucket>], [max_score_correct_<bucket>], and answer-pipe tokens [BlockLabel]. `score_correct` counts only points earned on correctly-answered graded questions; `correct_answers` / `max_correct_answers` count graded questions right vs total."
          },
          "when_incorrect": {
            "type": "string",
            "nullable": true,
            "description": "Message shown to the respondent immediately after they answer incorrectly. Only meaningful when `correct_answer_ids` is set. Supports the same template variables as `when_correct`."
          },
          "scale": {
            "type": "object",
            "nullable": true,
            "description": "Whole-scale presentation for `rating` / `opinion_scale` / `nps`. The point RANGE lives in the block's `min` / `max`, not here; per-point emoji or labels live in `options`. Responses store the picked NUMBER, so a scale answer reads back as an integer and compares numerically in skip logic (e.g. an NPS detractor follow-up is `answer <= 6`).\n\nRanges: `rating` starts at min=1 with 2-10 points (default 1-5); `opinion_scale` starts at 0 or 1 with 2-11 points; `nps` is locked to 0-10 with standard anchors. Out-of-range values return `400 invalid_request`.",
            "properties": {
              "icon": {
                "type": "string",
                "enum": [
                  "numbers",
                  "stars",
                  "emoji"
                ],
                "nullable": true,
                "description": "Point rendering. `stars` is the rating default; `numbers` the scale/NPS default. `emoji` requires one option per point carrying that point's emoji."
              },
              "label_left": {
                "type": "string",
                "nullable": true,
                "description": "Endpoint anchor shown at `min` (e.g. \"Not at all likely\"). Mutually exclusive with per-point `label`s: name the two ends, or name every point, not both. Fixed and translated automatically on `nps` \u2014 sending it there returns `400 invalid_request`."
              },
              "label_center": {
                "type": "string",
                "nullable": true,
                "description": "Optional midpoint anchor. Only meaningful on an odd point count, which is the only shape with a real middle."
              },
              "label_right": {
                "type": "string",
                "nullable": true,
                "description": "Endpoint anchor shown at `max` (e.g. \"Extremely likely\"). Same rules as `label_left`."
              }
            }
          },
          "randomize_options": {
            "type": "boolean",
            "nullable": true,
            "description": "Shuffle the option order per respondent, so the list a participant sees is not the order you authored. Valid on `single_punch`, `multi_punch` and `ranking`; sending it on `rating` / `opinion_scale` / `nps` returns `400 invalid_request` \u2014 a scale is ordered by definition and its points are never shuffled.\n\nIt matters most on `ranking`, where a fixed order is not a cosmetic default but a measurement bias: whichever item sits first gets tapped first, and that bias lands in the average rank the results report.\n\nThe shuffle is seeded per respondent per session, so revisiting a question does not reorder it mid-answer. Options carrying `anchor_position: \"last\"` are appended after the shuffle in their stored order. Defaults to false. Discord poll embeds are out of scope \u2014 one embed renders for every viewer, so there is no per-respondent order to vary."
          },
          "rank_top_n": {
            "type": "integer",
            "nullable": true,
            "minimum": 1,
            "description": "Ranking only: how many items the respondent is asked to put in order (\"rank your top 3\"). Omit or null to have every item ranked. It has its own field rather than riding `max`, which means max SELECTIONS on a multi_punch.\n\nA ranking submits complete or not at all \u2014 every item, or exactly the top N \u2014 so average ranks stay comparable across respondents. Each ranked item is stored as one response row whose `value` is the rank position (1 = first) and whose `option_id` is the item; unranked items in top-N mode have no row.\n\nRanking has one interaction model (tap in order) on both Discord and web, so its `answer_style` is only about how the items are drawn: `full_text` (default) or `emoji_only`, which puts the item names in a list above the buttons and is the way to fit long names on Discord. `select_menu` returns `400 invalid_request` \u2014 a dropdown can't express an order. The item list is capped at 20 \u2014 a full grid of Discord buttons \u2014 and more than about 7 items is ranked unreliably, which is what `rank_top_n` is for."
          },
          "calculated_formula": {
            "type": "string",
            "nullable": true,
            "description": "Formula for `calculated_block` type. Evaluated in script order; earlier calculated blocks can be referenced by subsequent blocks via [BlockLabel]. Three expression types are supported:\n\n1. `argmax([score_a], [score_b], ...)` \u2014 returns the display name of the bucket with the highest value.\n\n2. `if <cond> then <val> else <fallback>` \u2014 keywords are case-insensitive. `<cond>` supports `>=`, `<=`, `!=`, `==`, `>`, `<` (numeric). A bare numeric condition is truthy if non-zero.\n\n3. Arithmetic: any expression using `[variable]` tokens, digits, `+`, `-`, `*`, `/`, `(`, `)`. Unknown variables resolve to 0.\n\nAvailable variables: `[score]`, `[max_score]`, `[score_correct]`, `[max_score_correct]`, `[correct_answers]`, `[max_correct_answers]`, `[score_<bucket>]`, `[max_score_<bucket>]`, `[score_correct_<bucket>]`, `[max_score_correct_<bucket>]`, answer-pipe tokens `[BlockLabel]`, and previously evaluated `[CalcFieldName]` tokens. `score_correct` / `max_score_correct` accumulate only from correctly-answered graded questions; `correct_answers` / `max_correct_answers` are question counts (right vs total gradeable).\n\nExamples:\n  `argmax([score_gryffindor], [score_slytherin], [score_ravenclaw])`\n  `if [score] >= 15 then Excellent else Good`\n  `[score] / [max_score] * 100`\n  `if [correct_answers] == [max_correct_answers] then Perfect! else [correct_answers] out of [max_correct_answers]`\n  `[score_correct] / [max_score_correct] * 100`"
          },
          "position": {
            "type": "integer",
            "nullable": true,
            "description": "0-based position in the script. Omit to append."
          },
          "continue": {
            "type": "object",
            "nullable": true,
            "description": "Advance settings for `content_block` and `action_block`.",
            "properties": {
              "after": {
                "type": "string",
                "enum": [
                  "pause",
                  "click"
                ],
                "description": "`pause`: auto-advance after `continue.pause` seconds; `click`: respondent must press a button to continue."
              },
              "pause": {
                "type": "integer",
                "nullable": true,
                "description": "Seconds to wait before auto-advancing. Only used when `after` is `pause`."
              },
              "label": {
                "type": "string",
                "nullable": true,
                "description": "Button label shown to the respondent. Only used when `after` is `click`. Defaults to `Got it \ud83d\udc4d`."
              }
            }
          }
        }
      },
      "Response": {
        "type": "object",
        "description": "One participant submission. **On a project whose `privacy_mode` is `anonymous`, `user_id`, `platform_id` and `session_number` are always `null`** \u2014 the respondent cannot be identified from the API, matching the XLSX exports and the web Responses tab. `id` still identifies the submission, so a submission's answers stay grouped and fetchable.",
        "properties": {
          "id": {
            "type": "string"
          },
          "project_id": {
            "type": "string"
          },
          "session_number": {
            "type": "integer",
            "nullable": true,
            "description": "The respondent's completion index. Null on anonymous projects \u2014 it counts one person's repeat completions."
          },
          "submitted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "user_id": {
            "type": "string",
            "nullable": true,
            "description": "Globally-unique Subo user id. The canonical key for fetching this exact response \u2014 GET /responses?user_id=&session_number=. Null on anonymous projects."
          },
          "platform_id": {
            "type": "string",
            "nullable": true,
            "description": "The respondent's account id within their platform namespace (the Discord snowflake for a Discord-native community). Only unique when paired with the account namespace, so prefer user_id as a join key. Null on anonymous projects."
          },
          "provider": {
            "type": "string",
            "nullable": true,
            "enum": [
              "discord",
              "youtube",
              "twitch",
              "steam",
              "patreon",
              "web"
            ],
            "description": "Which connected source admitted the respondent for this session (provenance). Verified respondents carry their platform; Discord-native sessions are 'discord' and open-link web sessions are 'web'. Returned on anonymous projects too \u2014 the source is not treated as identifying."
          },
          "answers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "block_id": {
                  "type": "integer"
                },
                "value": {
                  "type": "string",
                  "nullable": true
                },
                "option_id": {
                  "type": "integer",
                  "nullable": true
                }
              }
            }
          }
        }
      },
      "Member": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "platform_id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "nick": {
            "type": "string",
            "nullable": true,
            "description": "Server-specific display name (Discord nickname)"
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "access": {
            "type": "string",
            "enum": [
              "admin",
              "creator",
              "member"
            ]
          },
          "rank": {
            "type": "integer",
            "nullable": true,
            "description": "XP leaderboard rank within the community"
          },
          "xp_total": {
            "type": "integer"
          },
          "xp_monthly": {
            "type": "integer"
          },
          "participation": {
            "type": "integer",
            "description": "Number of surveys completed by this member"
          },
          "achievements": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Achievement records earned by this member"
          },
          "joined_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "left_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "XpLedgerEntry": {
        "type": "object",
        "description": "One append-only entry in a member's XP audit trail.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "delta": {
            "type": "integer",
            "description": "Effective (post-clamp) XP change; negative for deductions"
          },
          "balance_after": {
            "type": "integer",
            "description": "Running lifetime XP total right after this entry"
          },
          "source_kind": {
            "type": "string",
            "enum": [
              "opening_balance",
              "unknown",
              "survey_completion",
              "action_block",
              "poll_vote",
              "poll_unwind",
              "recalculate",
              "admin_manual",
              "reset"
            ],
            "description": "Why the balance moved"
          },
          "source_ref": {
            "type": "integer",
            "nullable": true,
            "description": "Referenced survey/poll id for survey-derived kinds; null otherwise"
          },
          "session_number": {
            "type": "integer",
            "nullable": true,
            "description": "Completion index for survey/poll kinds"
          },
          "actor_name": {
            "type": "string",
            "nullable": true,
            "description": "Admin who awarded, for admin_manual/reset kinds"
          },
          "survey_name": {
            "type": "string",
            "nullable": true,
            "description": "Name of the survey/poll named by source_ref"
          },
          "note": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "community_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "secret_prefix": {
            "type": "string"
          },
          "is_active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "webhook_id": {
            "type": "string"
          },
          "event_type": {
            "type": "string",
            "example": "project.created"
          },
          "event_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "delivered",
              "failed",
              "abandoned"
            ]
          },
          "attempt_count": {
            "type": "integer"
          },
          "response_status": {
            "type": "integer",
            "nullable": true,
            "description": "HTTP status code returned by the endpoint"
          },
          "response_body": {
            "type": "string",
            "nullable": true,
            "description": "First 1000 characters of the endpoint's response body"
          },
          "last_attempt_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "next_retry_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "community_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "key_prefix": {
            "type": "string"
          },
          "is_active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "Script": {
        "type": "object",
        "properties": {
          "project_id": {
            "type": "string"
          },
          "blocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Block"
            }
          }
        }
      },
      "AnalysisDistributionItem": {
        "type": "object",
        "properties": {
          "option_id": {
            "type": "integer",
            "nullable": true
          },
          "value": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "percentage": {
            "type": "number",
            "format": "float"
          }
        }
      },
      "AnalysisStats": {
        "type": "object",
        "description": "Descriptive statistics over the numeric answers. Returned for `open_numeric` and for the scale family (`rating` / `opinion_scale` / `nps`), whose answer is the picked point. `count` is the number of answers that parsed as a number, which can be lower than the block's `response_count`.",
        "properties": {
          "count": {
            "type": "integer"
          },
          "mean": {
            "type": "number",
            "nullable": true
          },
          "min": {
            "type": "number",
            "nullable": true
          },
          "max": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "AnalysisRankingItem": {
        "type": "object",
        "description": "One item of a `ranking` block. **Lower `average_rank` is better** \u2014 it is the item's mean position, computed only over the respondents who ranked it, so read it next to `ranked_count`: in a `rank_top_n` block an item ranked by three people can hold a flattering average. Items nobody ranked are still listed, with `average_rank: null`.",
        "properties": {
          "option_id": {
            "type": "integer",
            "nullable": true
          },
          "value": {
            "type": "string",
            "description": "The item's text, as sent in `options`"
          },
          "average_rank": {
            "type": "number",
            "format": "float",
            "nullable": true
          },
          "first_choice_count": {
            "type": "integer",
            "description": "How many respondents ranked this item first"
          },
          "ranked_count": {
            "type": "integer",
            "description": "How many respondents ranked it at all"
          }
        }
      },
      "AnalysisBlock": {
        "type": "object",
        "properties": {
          "block_id": {
            "type": "integer"
          },
          "block_type": {
            "type": "string",
            "enum": [
              "single_punch",
              "multi_punch",
              "open_text",
              "open_numeric",
              "rating",
              "opinion_scale",
              "nps",
              "ranking",
              "content_block"
            ]
          },
          "prompt": {
            "type": "string"
          },
          "position": {
            "type": "integer"
          },
          "response_count": {
            "type": "integer"
          },
          "distribution": {
            "type": "array",
            "nullable": true,
            "description": "Answer distribution for `single_punch` and `multi_punch` blocks (one entry per chosen option, `option_id` set) and for the scale family (one entry per declared point, low to high, `option_id` null and `value` the point NUMBER \u2014 a point nobody picked is still listed with a count of 0). The glyph or Likert label a person would read lives on the block's `options` / `scale` in the script endpoint.",
            "items": {
              "$ref": "#/components/schemas/AnalysisDistributionItem"
            }
          },
          "stats": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/AnalysisStats"
              },
              {
                "type": "null"
              }
            ],
            "description": "Descriptive statistics for `open_numeric` and scale-family blocks"
          },
          "ranking": {
            "type": "array",
            "nullable": true,
            "description": "Per-item results for `ranking` blocks, best first. Present instead of `distribution`, which cannot express an average rank.",
            "items": {
              "$ref": "#/components/schemas/AnalysisRankingItem"
            }
          },
          "summary": {
            "type": "string",
            "nullable": true,
            "description": "AI-generated summary text for open_text blocks"
          },
          "summary_status": {
            "type": "string",
            "enum": [
              "none",
              "pending",
              "done",
              "failed"
            ],
            "description": "`none`: not applicable or not requested; `pending`: AI job running; `done`: summary available; `failed`: generation failed"
          }
        }
      },
      "Analysis": {
        "type": "object",
        "properties": {
          "project_id": {
            "type": "string"
          },
          "total_responses": {
            "type": "integer"
          },
          "blocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalysisBlock"
            }
          }
        }
      },
      "ProjectLifecycleResponse": {
        "type": "object",
        "properties": {
          "project_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          },
          "notice": {
            "type": "string",
            "nullable": true,
            "description": "Present when the project was activated but no Discord invitation was posted"
          }
        }
      },
      "WebhookCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Webhook"
          }
        ],
        "properties": {
          "signing_secret": {
            "type": "string",
            "description": "Full signing secret \u2014 store immediately, not recoverable after this response",
            "example": "sbo_whsec_..."
          }
        }
      },
      "ApiKeyCreated": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          }
        ],
        "properties": {
          "api_key": {
            "type": "string",
            "description": "Full API key value \u2014 store immediately, not recoverable after this response",
            "example": "sbo_live_..."
          }
        }
      },
      "WebhookPayloadProject": {
        "type": "object",
        "description": "Payload delivered to your endpoint for project lifecycle events (`project.created`, `project.updated`). The `project` field contains the full project object as returned by `GET /projects/{projectId}`.",
        "properties": {
          "event": {
            "type": "string",
            "example": "project.created"
          },
          "community_id": {
            "type": "string",
            "example": "1152727606572093543"
          },
          "project": {
            "$ref": "#/components/schemas/Project"
          }
        },
        "example": {
          "event": "project.created",
          "community_id": "1152727606572093543",
          "project": {
            "id": "789",
            "name": "Q2 Community Survey",
            "status": "inactive"
          }
        }
      },
      "WebhookPayloadProjectRef": {
        "type": "object",
        "description": "Payload delivered for project reference events (`project.opened`, `project.closed`, `project.deleted`). Contains only the project ID and community ID \u2014 fetch the full project if needed.",
        "properties": {
          "event": {
            "type": "string",
            "example": "project.opened"
          },
          "community_id": {
            "type": "string",
            "example": "1152727606572093543"
          },
          "project_id": {
            "type": "string",
            "example": "789"
          }
        },
        "example": {
          "event": "project.opened",
          "community_id": "1152727606572093543",
          "project_id": "789"
        }
      },
      "WebhookPayloadStatusChanged": {
        "type": "object",
        "description": "Payload for `project.status_changed` events (fires on both open and close).",
        "properties": {
          "event": {
            "type": "string",
            "example": "project.status_changed"
          },
          "community_id": {
            "type": "string",
            "example": "1152727606572093543"
          },
          "project_id": {
            "type": "string",
            "example": "789"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "inactive"
            ]
          }
        },
        "example": {
          "event": "project.status_changed",
          "community_id": "1152727606572093543",
          "project_id": "789",
          "status": "active"
        }
      },
      "WebhookPayloadResponseSubmitted": {
        "type": "object",
        "description": "Payload for `response.submitted` events. Fires when a real (non-test) participant completes a project. Premium tier required to subscribe.\n\n**Anonymous projects:** when the project's `privacy_mode` is `anonymous`, `user_id`, `platform_id` and `session_number` are `null`. Use `response_id` to fetch the submission; `provider` is still sent.",
        "properties": {
          "event": {
            "type": "string",
            "example": "response.submitted"
          },
          "community_id": {
            "type": "string",
            "example": "1152727606572093543"
          },
          "project_id": {
            "type": "string",
            "example": "789"
          },
          "response_id": {
            "type": "string",
            "nullable": true,
            "description": "Id of this submission. Pass to GET /communities/{communityId}/projects/{projectId}/responses/{responseId} for the full answer set. Always sent, including on anonymous projects \u2014 it is the only join key that survives there."
          },
          "session_number": {
            "type": "integer",
            "nullable": true,
            "description": "Increments when `max_completes_per_user > 1`; 1 for first session. `null` on anonymous projects (it counts one person's repeat completions)."
          },
          "user_id": {
            "type": "string",
            "nullable": true,
            "description": "Globally-unique Subo user id. Pass to GET /responses?user_id=&session_number= to retrieve the full answer set for this exact submission. `null` on anonymous projects."
          },
          "platform_id": {
            "type": "string",
            "nullable": true,
            "description": "The respondent's account id within their platform namespace (the Discord snowflake for a Discord-native community). Unique only when paired with `provider`. `null` on anonymous projects."
          },
          "provider": {
            "type": "string",
            "enum": [
              "discord",
              "web"
            ],
            "description": "The account namespace `platform_id` belongs to. Pair the two to identify the account, or use `user_id` for an unambiguous lookup. Sent on anonymous projects too \u2014 the source a respondent arrived through is not treated as identifying."
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "example": {
          "event": "response.submitted",
          "community_id": "1152727606572093543",
          "project_id": "789",
          "response_id": "48120",
          "session_number": 1,
          "user_id": "44821",
          "platform_id": "308994132968210433",
          "provider": "discord",
          "completed_at": "2026-04-30T12:00:00+00:00"
        }
      },
      "WebhookPayloadAnalysisCompleted": {
        "type": "object",
        "description": "Payload for `analysis.completed` events. Fires when an AI summarization job finishes.",
        "properties": {
          "event": {
            "type": "string",
            "example": "analysis.completed"
          },
          "community_id": {
            "type": "string",
            "example": "1152727606572093543"
          },
          "project_id": {
            "type": "string",
            "example": "789"
          },
          "blocks_updated": {
            "type": "integer",
            "description": "Number of open_text blocks whose summary was successfully written"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "example": {
          "event": "analysis.completed",
          "community_id": "1152727606572093543",
          "project_id": "789",
          "blocks_updated": 3,
          "completed_at": "2026-04-30T12:00:00+00:00"
        }
      },
      "Theme": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "brand_primary": {
            "type": "string",
            "nullable": true
          },
          "brand_accent": {
            "type": "string",
            "nullable": true
          },
          "font_body": {
            "type": "string",
            "nullable": true
          },
          "is_premium": {
            "type": "boolean"
          }
        }
      },
      "Interviewer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "avatar_url": {
            "type": "string",
            "nullable": true
          },
          "tone": {
            "type": "string",
            "nullable": true
          },
          "style": {
            "type": "string",
            "nullable": true
          },
          "formality": {
            "type": "string",
            "nullable": true
          },
          "is_premium": {
            "type": "boolean"
          }
        }
      },
      "Template": {
        "type": "object",
        "description": "Catalog card for a curated, cloneable template.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "slug": {
            "type": "string",
            "nullable": true,
            "description": "Canonical human-readable join key shared with the marketing site (e.g. 'lore-trivia-quiz')"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "is_poll": {
            "type": "boolean"
          },
          "scoring_enabled": {
            "type": "boolean"
          },
          "question_count": {
            "type": "integer"
          },
          "clone_count": {
            "type": "integer"
          },
          "image_url": {
            "type": "string",
            "nullable": true
          },
          "thumbnail_url": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "nullable": true
          },
          "recipe_url": {
            "type": "string",
            "nullable": true,
            "description": "Recipe walkthrough URL; reserved (always null until the field is populated)"
          }
        }
      },
      "TemplateProperties": {
        "type": "object",
        "description": "Clonable configuration carried by a template (what you get when you clone it).",
        "properties": {
          "is_poll": {
            "type": "boolean"
          },
          "uses_xp": {
            "type": "boolean"
          },
          "anonymous_mode": {
            "type": "integer",
            "nullable": true,
            "description": "1 Semi-private, 2 Transparent, 3 Anonymous"
          },
          "max_completes_per_user": {
            "type": "integer",
            "nullable": true
          },
          "link_mode": {
            "type": "integer",
            "nullable": true,
            "description": "1 Discord, 2 Private web, 3 Open Web"
          },
          "score_buckets": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "[{id, name}]; more than one = multi-dimension scoring"
          },
          "poll_results_mode": {
            "type": "integer",
            "nullable": true
          },
          "reveal_results": {
            "type": "boolean",
            "nullable": true
          },
          "change_poll": {
            "type": "boolean",
            "nullable": true
          },
          "has_invite_embed": {
            "type": "boolean"
          },
          "invite_footer": {
            "type": "string",
            "nullable": true,
            "description": "Text shown below the invitation embed. Premium only."
          },
          "embed_config": {
            "type": "object",
            "nullable": true,
            "description": "Invitation embed layout carried by the template \u2014 info display (`embed`/`button`) + per-row toggles",
            "properties": {
              "info_display": {
                "type": "string",
                "enum": [
                  "embed",
                  "button"
                ],
                "nullable": true
              },
              "rows": {
                "type": "object",
                "additionalProperties": {
                  "type": "boolean"
                }
              }
            }
          },
          "answer_button_label": {
            "type": "string",
            "nullable": true,
            "description": "Custom label for the answer/vote button. Premium only."
          },
          "answer_button_color": {
            "type": "integer",
            "nullable": true,
            "description": "Discord button style for the answer button (1 primary, 2 secondary, 3 success, 4 danger). Premium only."
          }
        }
      },
      "TemplateDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Template"
          },
          {
            "type": "object",
            "properties": {
              "properties": {
                "$ref": "#/components/schemas/TemplateProperties"
              },
              "features_showcased": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Derived capability fingerprint, e.g. ['grading','give_role','skip_logic']"
              },
              "blocks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Block"
                },
                "description": "Full ordered script in the same shape as GET/PUT /script"
              },
              "is_cloneable": {
                "type": "boolean",
                "description": "Whether the source survey allows cloning (info-level opt-out flag)"
              }
            }
          }
        ]
      }
    },
    "parameters": {
      "communityId": {
        "name": "communityId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Community ID (Discord guild snowflake, serialized as string)"
      },
      "projectId": {
        "name": "projectId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Project ID"
      },
      "blockId": {
        "name": "blockId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Block ID"
      },
      "responseId": {
        "name": "responseId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Response ID"
      },
      "memberId": {
        "name": "memberId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Subo internal user ID"
      },
      "webhookId": {
        "name": "webhookId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "keyId": {
        "name": "keyId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "Error401": {
        "description": "Invalid or missing API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing_key": {
                "summary": "X-API-Key header absent",
                "value": {
                  "error": "missing_api_key",
                  "message": "X-API-Key header is required"
                }
              },
              "invalid_key": {
                "summary": "Key invalid, expired, or revoked",
                "value": {
                  "error": "invalid_api_key",
                  "message": "API key is invalid, expired, or revoked"
                }
              }
            }
          }
        }
      },
      "Error403": {
        "description": "Forbidden \u2014 the key or user lacks permission for this resource or operation",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "community_access": {
                "summary": "Key not scoped to this community",
                "value": {
                  "error": "forbidden",
                  "message": "API key does not have access to this community"
                }
              },
              "admin_required": {
                "summary": "Admin role required",
                "value": {
                  "error": "forbidden",
                  "message": "Admin role required"
                }
              },
              "creator_required": {
                "summary": "Creator role or higher required",
                "value": {
                  "error": "forbidden",
                  "message": "Creator role or higher required"
                }
              }
            }
          }
        }
      },
      "Error404": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_found",
              "message": "Resource not found"
            }
          }
        }
      },
      "Error429": {
        "description": "Rate limit exceeded \u2014 wait `retry_after` seconds before retrying. The same value appears in the `Retry-After` header.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait before the next request"
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            },
            "description": "Maximum requests per minute for your tier"
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            },
            "description": "Requests remaining in the current window"
          },
          "X-RateLimit-Reset": {
            "schema": {
              "type": "integer"
            },
            "description": "Unix timestamp when the current window resets"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error429"
            },
            "example": {
              "error": "rate_limit_exceeded",
              "retry_after": 14
            }
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Communities",
      "description": "A community is the top-level tenant, corresponding to a single Discord server (guild). All other resources \u2014 projects, members, webhooks, and API keys \u2014 are scoped to a community.\n\nCommunity settings are grouped into five sections returned by `GET /communities/{communityId}`:\n\n- **`general`** \u2014 feature flags (`text_analysis`, `allow_network_projects`) and topic tags\n- **`defaults`** \u2014 values applied to new projects (privacy mode, closing interval, answer style)\n- **`rewards`** \u2014 XP system configuration (name, rates, leaderboard, achievements)\n- **`experience`** \u2014 respondent-facing messaging and branding customization\n- **`platforms.discord`** \u2014 channel/role mapping; excluded by default, request with `?expand=platforms.discord`\n\nThe `tier` field (`basic`, `premium`, `vip`, `custom`) controls feature availability. Writing a tier-gated field without the required plan returns 402."
    },
    {
      "name": "Projects",
      "description": "A project is a survey or poll that collects structured participant responses. `type` is fixed at creation: `convo` (multi-step conversational survey) or `poll` (single-question vote).\n\n**Lifecycle:** projects have two statuses:\n\n- `inactive` \u2014 draft state; script and settings are editable; no responses accepted\n- `active` \u2014 live; script is locked; responses accepted; triggers `project.opened` webhook\n\nUse `POST /projects/{id}/open` and `/close` to transition between states. Closing fires `project.closed` and allows in-progress participant sessions to complete.\n\n**Sub-objects on each project:**\n\n- **`info`** \u2014 immutable identity fields and aggregate counts (response count, completion count)\n- **`settings`** \u2014 participation rules: privacy mode, max completions per user, result visibility\n- **`delivery`** \u2014 opening/closing schedule, audience (participation mode, Discord roles, and `verified_source_ids` for source-gated verification \u2014 see List audience sources), invitation embed config\n- **`rewards`** \u2014 XP grant and achievement awarded on completion\n- **`presentation`** \u2014 theme, interviewer persona, branding, and poll chart emoji\n\nA project's questions are managed separately via the Script endpoints."
    },
    {
      "name": "Script",
      "description": "A script is the ordered list of blocks (questions and content slides) that make up a project. Scripts can only be modified when the project is `inactive`.\n\n**Block types:**\n\n- `single_punch` \u2014 select exactly one option\n- `multi_punch` \u2014 select one or more options (bounded by `min`/`max`)\n- `open_text` \u2014 free-text response; eligible for AI summarization\n- `open_numeric` \u2014 numeric input bounded by `min`/`max`\n- `rating` \u2014 2-10 points starting at 1, rendered as stars, numbers, or an emoji set (default 1-5 stars)\n- `opinion_scale` \u2014 2-11 points starting at 0 or 1, with endpoint anchors or per-point labels\n- `nps` \u2014 the standard 0-10 recommendation scale with fixed anchors\n- `ranking` \u2014 respondents tap the options into order (best first); `rank_top_n` asks for a partial ranking, and each ranked item is stored as its own response row carrying the rank\n- `content_block` \u2014 display-only slide with no answer\n- `action_block` \u2014 side-effect card with `action_kind` + `action_config` (grant XP, achievement, or role)\n\nBoth `content_block` and `action_block` use the same `continue` object for pacing: `after` (`pause` or `click`), `pause` (seconds), and `label` (button text).\n\nThe public Script API manages **main** section blocks. Use `GET /script?expand=outro` to include post-survey (outro) blocks with `section=outro`. `PUT /script` also supports controlled outro updates (prompt/image/thumbnail/color/pause) by block id, while reward/action identity remains anchored to project rewards/settings. Block CRUD (`/script/blocks`) is for main-section blocks only.\n\nBlocks are 0-indexed by `position`; the script is always returned in position order. Omit `position` on write to append.\n\n**Dynamic variables in block text**\n\n`[bracket]` tokens are substituted at render time in the `prompt` of any `content_block` or `action_block`, and in XP/custom closing messages.\n\n| Token | Resolves to | Notes |\n|---|---|---|\n| `[UserName]` | Respondent display name | Empty for anonymous web respondents |\n| `[SurveyName]` | Survey/project name | |\n| `[SurveyId]` | Numeric survey ID | |\n| `[CreatedBy]` | Discord username of the survey creator | |\n| `[ServerName]` | Community (Discord server) name | |\n| `[ServerId]` | Community (Discord server) ID | |\n| `[BotName]` | Bot product name | |\n| `[InterviewerName]` | AI interviewer name | Falls back to `[BotName]`; web surveys only |\n| `[xp_points]` | XP awarded this completion | XP/closing messages only |\n| `[xp_name]` | Server XP currency name | XP/closing messages only |\n| `[xp_to_next_level]` | XP needed to reach next role threshold | XP/closing messages only |\n| `[next_xp_level]` | Name of the next XP role | XP/closing messages only |\n| `[achievement_name]` | Granted achievement name | `give_achievement` action blocks only |\n| `[role_name]` | Granted Discord role name | `give_role` action blocks only |\n\nUnsupported tokens pass through unchanged. Preview mode in the Script Editor skips substitution so templates remain visible while authoring.\n\n**Skip logic \u2014 two ways to gate a block**\n\n**1. Structured `show_when` / `hide_when` (recommended).** Describe the intent and let the API compile a correct `precondition` for you \u2014 right operator, quoting, block-name casing, and the hide-when-true inversion. A condition is a **leaf** `{block|variable, op, value}` or a **group** `{all:[...]}` (AND) / `{any:[...]}` (OR). `op` is one of `eq, ne, lt, gt, gte, lte, in` (`in` takes a list `value`).\n\n```json\n// show this block only when the previous block \"Notifytiming\" was answered \"Other\"\n{\"type\": \"open_text\", \"prompt\": \"How would you want it to work?\",\n \"show_when\": {\"block\": \"Notifytiming\", \"op\": \"eq\", \"value\": \"Other\"}}\n\n// hide a detractor follow-up unless the score is low\n{\"hide_when\": {\"variable\": \"score\", \"op\": \"gte\", \"value\": 7}}\n\n// show only when one of several options was picked\n{\"show_when\": {\"any\": [\n  {\"block\": \"Topics\", \"op\": \"eq\", \"value\": \"Sports\"},\n  {\"block\": \"Topics\", \"op\": \"eq\", \"value\": \"Gaming\"}]}}\n```\n\n`show_when` means *show only if*; `hide_when` means *hide when*. They are mutually exclusive with each other and with a raw `precondition`.\n\n**2. Raw `precondition` expression.** A **hide expression**: when it evaluates to `true` the block is hidden (no answer recorded); when `false` (or absent) it is shown. To *show only when* a condition holds, wrap it in `NOT((\u2026))`.\n\n**Block references** \u2014 reference a block by its `name`, matched case-sensitively against the stored name, which is **title-cased** (a block named `NotifyPref` is referenced as `Notifypref`). Names are recommended over the positional `q1`, `q2`, \u2026 `qN` tokens, which shift when blocks are added or removed. Score/grading variables (`score`, `score_<bucket>`, `correct_answers`, \u2026) are also referenceable when scoring is enabled. `GET /script` returns the exact spellings in its `variables` array.\n\n**Operators:**\n\n| Operator | Syntax | Notes |\n|---|---|---|\n| Equal | `Notifypref = \"value\"` | `==` is also accepted; case-sensitive for strings |\n| Not equal | `Notifypref != \"value\"` | |\n| Comparisons | `Score < 5`, `Score >= 7`, `Score <= 3`, `Score > 5` | Numeric; values coerced with `float()` |\n| Membership | `Region in [\"a\", \"b\"]` | True if the value matches any item; case-insensitive |\n| Substring | `\"text\" in Feedback` | True if the literal is a substring; case-insensitive |\n| List membership | `\"opt\" in Topics` | For multi-punch, checks if opt is among selected values |\n| AND / OR / NOT | `A = \"x\" AND B > 3` | Keywords are case-insensitive |\n\nString literals must use double quotes (single quotes are a parse error). Identifier names may safely begin with a keyword (e.g. `Notify\u2026`, `Internal\u2026`).\n\n**Value types by block type:**\n\n| Block type | Value | How to test |\n|---|---|---|\n| `single_punch` | The selected option's display value (string) | `Overall = \"Option A\"` |\n| `multi_punch` | List of all selected values | `\"Option A\" in Topics` |\n| `open_text` | The raw text entered | `\"keyword\" in Feedback` |\n| `open_numeric` | Integer | `Age > 7` |\n| `rating` / `opinion_scale` / `nps` | The picked point as an integer | `Nps <= 6` (detractor follow-up) |\n| `ranking` | List of items in rank order, best first | `\"Option A\" in Favorites` (they ranked it at all); `Favorites_1 = \"Option A\"` (it was their top pick) |\n| Yes/No (boolean) | `True` or `False` | `Optin = True` |\n\n**Unanswered or skipped blocks** \u2014 value is `null`. `\"x\" in null` \u2192 `false`. `null = null` \u2192 `true`. Comparisons with `null` evaluate to `false`.\n\n**Validation & error behavior** \u2014 a raw `precondition` that references an unknown variable or fails to parse is **rejected with `400 invalid_request` at write time** (the message names the offending token and suggests a fix). Any expression that still errors at runtime fails **safe**: the block is shown, and the error is logged.\n\n**Examples:**\n\n```\n# Show block only when \"Overall\" was answered \"Dissatisfied\"\nNOT((Overall = \"Dissatisfied\"))\n\n# Show follow-up unless respondent picked \"No, manual is fine\"\nNOT((Notifypref != \"No, manual is fine\"))\n\n# Hide NPS follow-up for high scorers (show only when Score < 7)\nScore >= 7\n\n# Show only when one of several low-satisfaction values was chosen\nNOT((Overall in [\"Dissatisfied\", \"Very Dissatisfied\"]))\n```\n\n**Atomicity:** `PUT /script` replaces all blocks in a single transaction. Individual block endpoints (`POST /blocks`, `PUT /blocks/{id}`, `DELETE /blocks/{id}`) operate on single blocks and always return the full updated script."
    },
    {
      "name": "Responses",
      "description": "A response is one participant's answer record for a project. Each response contains an `answers` array, one entry per answered block:\n\n- For `single_punch` / `multi_punch`: `option_id` identifies the chosen option; `value` is its display label\n- For `open_text` / `open_numeric`: `value` holds the raw input; `option_id` is null\n- For `rating` / `opinion_scale` / `nps`: `value` is the picked point as a number and `option_id` is null even when the block defines per-point emoji, because those options are presentation and the number is the answer\n- For `ranking`: one entry PER RANKED ITEM \u2014 `option_id` is the item and `value` is its rank position (\"1\" = their first choice). Items left unranked in a `rank_top_n` block have no entry at all\n- Skipped blocks (failed precondition or optional and unanswered) have no entry in the array\n\n`session_number` distinguishes repeated completions when `max_completes_per_user > 1`. The `submitted_at` timestamp is set when the participant reaches the final screen.\n\n`provider` records which connected source admitted the respondent (provenance): a verified participant carries their platform (`youtube`, `twitch`, `steam`, `patreon`), a Discord-native session is `discord`, and an open-link web session is `web`.\n\nResponses are immutable once submitted. Two delete operations are available, both irreversible and requiring admin role and Premium tier or above:\n\n- `DELETE /responses` \u2014 bulk delete all responses for a project; resets counters\n- `DELETE /responses/{responseId}` \u2014 delete a single participant session; updates counters automatically"
    },
    {
      "name": "Analysis",
      "description": "Analysis data comes in two forms with different freshness and cost characteristics.\n\n**Computed (free, always current):**\n\n- `distribution` \u2014 for `single_punch` and `multi_punch` blocks: per-option count and percentage, calculated directly from stored responses\n- `stats` \u2014 for `open_numeric` blocks: count, mean, min, max\n- for `rating` / `opinion_scale` / `nps` blocks: **both**, since a scale is a number with a shape \u2014 `distribution` gives per-point counts across the block's full declared range (points nobody picked are included, with count 0) and `stats` gives the average. `option_id` is null on every scale entry, and `value` is the point NUMBER \u2014 the glyph or Likert label a person would read lives on the block's `options` / `scale`, because an API caller is a program\n\n- `ranking` \u2014 for `ranking` blocks: a per-item array with `average_rank` (over the respondents who ranked it, so LOWER is better), `first_choice_count` and `ranked_count`. Items come back best-first, and every item appears even when nobody ranked it. Read `average_rank` next to `ranked_count`: in a `rank_top_n` block, an item ranked by three people can hold a flattering average\n\nThese are recalculated on every `GET /analysis` call and require no credits.\n\n**AI summaries (async, credit-consuming):**\n\n- `summary` \u2014 for `open_text` blocks: a synthesized natural-language summary of all responses\n- `summary_status` \u2014 tracks job state: `none` (not yet requested), `pending` (job running), `done` (summary in `summary` field), `failed` (generation error)\n\nSummaries are generated by calling `POST /analysis`, which enqueues an AI job and returns immediately. Subscribe to the `analysis.completed` webhook to know when to stop polling; the payload includes `blocks_updated` so you know how many summaries were written. Bot credits are consumed only for blocks that complete successfully. `POST /analysis` with `force: true` regenerates all summaries even if they already exist."
    },
    {
      "name": "Members",
      "description": "A member record links a platform user (Discord account) to a community, tracking their access level and XP balance.\n\n**Access levels:**\n\n- `admin` \u2014 full community control; can manage settings, members, keys, and webhooks\n- `creator` \u2014 can create and manage projects; cannot modify community settings or other members\n- `member` \u2014 participate only; no management capabilities\n\n**XP** is community-scoped and split into `xp_total` (all-time) and `xp_monthly` (current calendar month, reset automatically). Use `POST /members/{memberId}/xp` with `operation: add | subtract | set` to adjust balances; `subtract` floors at zero.\n\nMembers can be looked up by Subo internal `id` (returned in response objects) or by `platform_id` (Discord snowflake) via `GET /members?platform_id={snowflake}`. Admin role is required for XP and access mutations."
    },
    {
      "name": "Themes & Interviewers",
      "description": "Read-only catalog endpoints returning available options for the web respondent interface. Reference the returned `id` values in a project's `presentation.theme_id` and `presentation.interviewer_id` fields.\n\n**Themes** control the visual appearance of the web survey rendered to respondents: color palette (`brand_primary`, `brand_accent`) and typography (`font_body`). `is_premium: true` themes require a paid community tier.\n\n**Interviewers** are AI personas that shape how conversational projects (`type: convo`) are delivered. Each persona has a `tone`, `style`, and `formality` descriptor that influences the AI's phrasing, follow-up questions, and overall register. `is_premium: true` interviewers require a paid community tier.\n\nBoth catalogs are pre-filtered for the requesting community's tier \u2014 premium-only entries are excluded if the community does not qualify, so any `id` returned is safe to use."
    },
    {
      "name": "Templates",
      "description": "Curated, cloneable templates \u2014 global reference examples, **not** scoped to your community. A template is a survey/poll flagged for the catalog; each carries catalog metadata plus a full machine-readable script you can read to learn the pattern or re-POST.\n\n- `GET /templates` \u2014 browse the catalog, filterable by `category`, `tag` (repeatable), `search`, or `type`.\n- `GET /templates/{templateId}` \u2014 one template with clonable `properties`, a `features_showcased` fingerprint (e.g. `grading`, `give_role`, `skip_logic`), and its `blocks` in the same shape as `GET/PUT /script`.\n- `POST /communities/{communityId}/templates/{templateId}/clone` \u2014 instantiate a template as a new inactive project in your community (deep clone; source-server bindings stripped). CREATOR/admin required; supports `Idempotency-Key`.\n\nThe `slug` field is the canonical human-readable key shared with the public marketing pages. An `X-API-Key` is required (auth + rate limiting), but the catalog itself spans communities."
    },
    {
      "name": "Webhooks",
      "description": "Webhooks deliver real-time event notifications to an HTTPS endpoint as HTTP POST requests with a JSON body. Admin role required to manage webhook registrations.\n\n**Signature verification:** every delivery includes an `X-Subo-Signature` header containing an HMAC-SHA256 hex digest of the raw request body, keyed with the webhook's signing secret. Always verify this header before processing a payload. Signing secrets are shown once at creation and once after rotation \u2014 store them immediately.\n\n**Retry policy:** failed deliveries (non-2xx or timeout) are retried up to 5 times with exponential backoff. Status progression: `pending` \u2192 `delivered` on 2xx, or `pending` \u2192 `failed` \u2192 `abandoned` after all retries are exhausted. Use `GET /webhooks/{id}/deliveries` to inspect attempt logs.\n\n**Available events:**\n\n- `project.created` / `project.updated` / `project.deleted` \u2014 project lifecycle\n- `project.opened` / `project.closed` \u2014 status transitions (unambiguous direction)\n- `project.status_changed` \u2014 fires on both open and close; payload includes `status` field; kept for backward compatibility\n- `response.submitted` \u2014 a participant completed a project (non-test users only); payload includes `session_number` and `completed_at`\n- `analysis.completed` \u2014 AI summarization job finished; payload includes `blocks_updated` count\n\n---\n\n## Event Payload Shapes\n\nAll payloads are delivered as HTTP POST with `Content-Type: application/json` and `X-Subo-Signature: sha256=<hmac-sha256>` for verification.\n\n### project.created / project.updated\n\nFull project object in the `project` field.\n\n```json\n{\n  \"event\": \"project.created\",\n  \"community_id\": \"1152727606572093543\",\n  \"project\": {\n    \"id\": \"789\",\n    \"community_id\": \"1152727606572093543\",\n    \"name\": \"Event Feedback Survey\",\n    \"status\": \"inactive\",\n    \"info\": {\n      \"type\": \"convo\",\n      \"created_at\": \"2026-05-01T12:00:00+00:00\",\n      \"block_count\": 3,\n      \"author_id\": \"42\",\n      \"completion_count\": 0\n    }\n  }\n}\n```\n\n### project.opened / project.closed / project.deleted\n\n```json\n{\n  \"event\": \"project.opened\",\n  \"community_id\": \"1152727606572093543\",\n  \"project_id\": \"789\"\n}\n```\n\n### project.status_changed\n\nFires on both open and close; includes the new `status` value.\n\n```json\n{\n  \"event\": \"project.status_changed\",\n  \"community_id\": \"1152727606572093543\",\n  \"project_id\": \"789\",\n  \"status\": \"active\"\n}\n```\n\n### response.submitted\n\nFires when a non-test participant completes a project. **Requires Premium tier or above.**\n\n`response_id` fetches the submission's answers; `user_id` is the globally-unique key for the respondent; `platform_id` + `provider` identify their platform account (the Discord snowflake on a Discord-native community).\n\nOn a project whose `privacy_mode` is `anonymous`, `user_id`, `platform_id` and `session_number` are `null` \u2014 use `response_id`.\n\n```json\n{\n  \"event\": \"response.submitted\",\n  \"community_id\": \"1152727606572093543\",\n  \"project_id\": \"789\",\n  \"response_id\": \"48120\",\n  \"session_number\": 47,\n  \"user_id\": \"44821\",\n  \"platform_id\": \"308994132968210433\",\n  \"provider\": \"discord\",\n  \"completed_at\": \"2026-05-01T14:23:00+00:00\"\n}\n```\n\n### analysis.completed\n\nFires when an AI summarization job finishes. `blocks_updated` is the count of blocks that received a new AI summary.\n\n```json\n{\n  \"event\": \"analysis.completed\",\n  \"community_id\": \"1152727606572093543\",\n  \"project_id\": \"789\",\n  \"blocks_updated\": 3,\n  \"completed_at\": \"2026-05-01T14:30:00+00:00\"\n}\n```"
    },
    {
      "name": "API Keys",
      "description": "API keys authenticate requests to this API. Each key is scoped to a single community and inherits the role of the user who created it. Admin role required to create or revoke keys.\n\nKeys are prefixed `sbo_live_`. The full key value is returned **only once** at creation \u2014 store it immediately and treat it as a secret. Subsequent reads return only the `key_prefix` (first 8 characters after the prefix), which is safe to log for identification.\n\nKeys can be set to expire via `expires_at` (ISO 8601 timestamp) or left permanent by omitting the field. Revocation via `DELETE /api-keys/{keyId}` takes effect immediately \u2014 all in-flight and future requests using that key are rejected with 401."
    }
  ],
  "paths": {
    "/v1/communities": {
      "get": {
        "tags": [
          "Communities"
        ],
        "summary": "List communities",
        "description": "List all communities where the API key's user holds a creator or admin role.",
        "operationId": "listCommunities",
        "responses": {
          "200": {
            "description": "List of communities",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Community"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}": {
      "get": {
        "tags": [
          "Communities"
        ],
        "summary": "Get community",
        "description": "Get full community details including identity fields and grouped settings (general, defaults, rewards, experience, usage). Pass `?expand=platforms.discord` to also include Discord-specific settings (channels, roles, bot info). Discord settings are excluded by default because they require additional database queries.",
        "operationId": "getCommunity",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "platforms.discord"
              ]
            },
            "description": "Comma-separated list of sections to include. Currently supports `platforms.discord`.",
            "example": "platforms.discord"
          }
        ],
        "responses": {
          "200": {
            "description": "Community details with grouped settings",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "id": "1152727606572093543",
                    "name": "My Community",
                    "platform": "discord",
                    "tier": "premium",
                    "locale": "en-US",
                    "icon_url": "https://cdn.discordapp.com/icons/...",
                    "general": {
                      "text_analysis": true,
                      "allow_network_projects": false,
                      "created_date": "2023-09-20T12:00:00",
                      "topics": [
                        "Gamer",
                        "Tech"
                      ],
                      "user_setup_id": "123456789"
                    },
                    "defaults": {
                      "privacy_mode": "semi-private",
                      "project_closing": 86400,
                      "invite_message": null,
                      "change_vote_enabled": true,
                      "poll_results_mode": "public",
                      "answer_style": "buttons_full",
                      "final_reveal": true
                    },
                    "rewards": {
                      "use_xp": true,
                      "xp_name": "Points",
                      "xp_per_question": 10,
                      "xp_per_poll": 50,
                      "xp_completion_bonus": 2,
                      "use_leader_board": true,
                      "xp_achievement_announcement": null,
                      "xp_stack_achievements": false,
                      "achievements": []
                    },
                    "experience": {
                      "enable_advertising": true,
                      "closing_message_type": "default",
                      "closing_message": null,
                      "closing_embed_color": null,
                      "closing_message_image_url": null,
                      "closing_message_thumbnail_url": null,
                      "invite_footer": null,
                      "analysis_footer": null,
                      "closing_message_footer": null,
                      "xp_closing_message": null,
                      "xp_closing_embed_color": null,
                      "xp_closing_message_image_url": null,
                      "xp_closing_message_thumbnail_url": null
                    },
                    "usage": {
                      "bot_credits": 100,
                      "total_project_count": 42,
                      "active_project_count": 3
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "put": {
        "tags": [
          "Communities"
        ],
        "summary": "Update community",
        "description": "Update any combination of community identity fields and settings groups in a single call. Admin role required. Only provided fields are written; omitted fields are left unchanged. All groups (general, defaults, rewards, experience, platforms) are optional.",
        "operationId": "updateCommunity",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Display name of the community"
                  },
                  "icon_url": {
                    "type": "string",
                    "description": "URL of the community icon image"
                  },
                  "locale": {
                    "type": "string",
                    "description": "BCP 47 locale code, e.g. en-US, fr-FR",
                    "example": "en-US"
                  },
                  "general": {
                    "type": "object",
                    "description": "General community settings",
                    "properties": {
                      "text_analysis": {
                        "type": "boolean",
                        "description": "Enable AI text analysis for open-ended responses"
                      },
                      "allow_network_projects": {
                        "type": "boolean",
                        "description": "Allow projects to be pushed via the Subo network"
                      },
                      "topics": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "Gamer",
                            "Social",
                            "Crypto",
                            "Finance",
                            "NFT",
                            "Entertainment",
                            "Music",
                            "Sports",
                            "Educational",
                            "Science",
                            "Politics",
                            "News",
                            "Business",
                            "Art",
                            "Content",
                            "Language",
                            "Anime",
                            "Culture",
                            "Faith",
                            "Tech",
                            "TTRPG"
                          ]
                        },
                        "description": "Community topic tags"
                      }
                    }
                  },
                  "defaults": {
                    "type": "object",
                    "description": "Default settings applied to new projects",
                    "properties": {
                      "privacy_mode": {
                        "type": "string",
                        "enum": [
                          "transparent",
                          "semi-private",
                          "anonymous"
                        ],
                        "description": "Default privacy mode applied to new projects. `transparent`: all members see who answered what. `semi-private`: only creator and admins see individual responses; others see aggregated results only if shared. `anonymous`: respondent identity hidden from everyone including admins (pseudo-anonymous \u2014 answers recorded without user ID unless volunteered). Caveat: XP and achievements can still be granted, so participation is not fully hidden."
                      },
                      "project_closing": {
                        "type": "integer",
                        "description": "Default interval before a project closes, in seconds"
                      },
                      "invite_message": {
                        "type": "string",
                        "description": "Default invitation message text"
                      },
                      "change_vote_enabled": {
                        "type": "boolean",
                        "description": "Allow participants to change their vote"
                      },
                      "poll_results_mode": {
                        "type": "string",
                        "enum": [
                          "public",
                          "votersonly",
                          "hidden"
                        ],
                        "description": "Who can see poll results"
                      },
                      "answer_style": {
                        "type": "string",
                        "enum": [
                          "buttons_full",
                          "buttons_emoji",
                          "select_menu"
                        ],
                        "description": "Default display style for multiple-choice answers"
                      },
                      "final_reveal": {
                        "type": "boolean",
                        "description": "Show final results to respondents after closing"
                      }
                    }
                  },
                  "rewards": {
                    "type": "object",
                    "description": "XP and achievement settings",
                    "properties": {
                      "use_xp": {
                        "type": "boolean"
                      },
                      "xp_name": {
                        "type": "string",
                        "description": "Display name for XP points (e.g. Points, Coins). Requires a paid plan.",
                        "example": "Points"
                      },
                      "xp_per_question": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "XP awarded per question answered. Requires a paid plan.",
                        "example": 10
                      },
                      "xp_per_poll": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "XP awarded per poll completed. Requires a paid plan."
                      },
                      "xp_completion_bonus": {
                        "type": "integer",
                        "minimum": 1,
                        "description": "XP multiplier awarded for completing a full project. Requires a paid plan."
                      },
                      "use_leader_board": {
                        "type": "boolean"
                      },
                      "xp_achievement_announcement": {
                        "type": "string",
                        "description": "Message announced when a participant reaches a new XP achievement"
                      },
                      "xp_stack_achievements": {
                        "type": "boolean",
                        "description": "Stack multiple XP achievement role rewards"
                      },
                      "achievements": {
                        "type": "array",
                        "description": "Patch list of achievements to update (by id)",
                        "items": {
                          "type": "object",
                          "required": [
                            "id"
                          ],
                          "properties": {
                            "id": {
                              "type": "integer"
                            },
                            "type": {
                              "type": "string"
                            },
                            "name": {
                              "type": "string"
                            },
                            "badge": {
                              "type": "string",
                              "description": "Image URL"
                            },
                            "description": {
                              "type": "string"
                            },
                            "discord_role_id": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  },
                  "experience": {
                    "type": "object",
                    "description": "Messaging and presentation customization",
                    "properties": {
                      "enable_advertising": {
                        "type": "boolean",
                        "description": "Include Subo promotion in buttons or messages. Disabling requires a paid plan."
                      },
                      "closing_message_type": {
                        "type": "string",
                        "enum": [
                          "default",
                          "formstandard",
                          "custom"
                        ],
                        "description": "Style of the closing message. `custom` requires a paid plan."
                      },
                      "closing_message": {
                        "type": "string",
                        "description": "Message displayed at the end of a convo. Use a `# Heading` markdown line as the first line to set a title."
                      },
                      "closing_embed_color": {
                        "type": "string",
                        "description": "Hex color code, e.g. #FF5733"
                      },
                      "closing_message_image_url": {
                        "type": "string",
                        "description": "URL of an image to render with the custom closing message (Discord embed image / web large image)"
                      },
                      "closing_message_thumbnail_url": {
                        "type": "string",
                        "description": "URL of a thumbnail to render with the custom closing message (Discord embed thumbnail / web small image)"
                      },
                      "invite_footer": {
                        "type": "string",
                        "description": "Footer appended after the project invitation message"
                      },
                      "analysis_footer": {
                        "type": "string",
                        "description": "Footer appended to analysis/export outputs"
                      },
                      "closing_message_footer": {
                        "type": "string",
                        "description": "Footer appended after the closing message"
                      },
                      "xp_closing_message": {
                        "type": "string",
                        "description": "Closing message shown when the respondent earns XP. Use `# Heading` on the first line for a title. Variables: [xp_points], [earned_xp], [month_total_xp], [xp_name], [xp_to_next_level], [next_xp_level], [xp_to_next_monthly_level], [next_monthly_xp_level]."
                      },
                      "xp_closing_embed_color": {
                        "type": "string",
                        "description": "Hex color code for the XP closing embed"
                      },
                      "xp_closing_message_image_url": {
                        "type": "string",
                        "description": "URL of an image to render with the XP closing message"
                      },
                      "xp_closing_message_thumbnail_url": {
                        "type": "string",
                        "description": "URL of a thumbnail to render with the XP closing message"
                      }
                    }
                  },
                  "platforms": {
                    "type": "object",
                    "description": "Platform-specific settings",
                    "properties": {
                      "discord": {
                        "type": "object",
                        "description": "Discord-specific channel and role configuration",
                        "properties": {
                          "participant_channel_id": {
                            "type": "string",
                            "description": "Main participant channel Discord snowflake ID"
                          },
                          "creator_channel_id": {
                            "type": "string",
                            "description": "Creator/admin control channel Discord snowflake ID"
                          },
                          "analysis_channel_id": {
                            "type": "string",
                            "description": "Channel where analysis/results are posted"
                          },
                          "new_response_notifications_channel_id": {
                            "type": "string",
                            "description": "Channel for new response notifications"
                          },
                          "enable_new_response_notifications": {
                            "type": "boolean"
                          },
                          "participants_category_id": {
                            "type": "string",
                            "description": "Discord category for temporary participant channels"
                          },
                          "new_response_notifications_in_threads": {
                            "type": "boolean",
                            "description": "Send new response notifications in Discord threads"
                          }
                        }
                      }
                    }
                  }
                }
              },
              "example": {
                "name": "My Community",
                "defaults": {
                  "privacy_mode": "semi-private"
                },
                "rewards": {
                  "use_xp": true,
                  "xp_name": "Points",
                  "xp_per_question": 10
                },
                "platforms": {
                  "discord": {
                    "participant_channel_id": "1234567890123456789"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Community"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Invalid value for field 'locale'"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "One or more fields require a paid plan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "The following fields require a paid plan: xp_name, xp_per_question"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/sources": {
      "get": {
        "tags": [
          "Communities"
        ],
        "summary": "List audience sources",
        "description": "List the community's audience sources. A source is one place the community's audience lives: the Discord server (created automatically when the bot is installed), a Steam app, a Twitch channel, or a YouTube channel. Verified projects gate respondents on membership in any of the sources attached to the project's audience (`delivery.audience.verified_source_ids`).",
        "operationId": "listSources",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "responses": {
          "200": {
            "description": "Audience sources",
            "content": {
              "application/json": {
                "example": {
                  "data": [
                    {
                      "id": 42,
                      "provider": "discord",
                      "external_id": "1152727606572093543",
                      "display_name": "My Community",
                      "status": "active",
                      "connected_at": "2023-09-20T12:00:00+00:00"
                    },
                    {
                      "id": 87,
                      "provider": "steam",
                      "external_id": "1145360",
                      "display_name": "Hades",
                      "status": "active",
                      "connected_at": "2026-07-11T09:30:00+00:00"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "post": {
        "tags": [
          "Communities"
        ],
        "summary": "Connect audience source",
        "description": "Connect an audience source to the community. Admin role required. Currently only `provider: \"steam\"` can be connected through the API (body: `{\"provider\": \"steam\", \"app_id\": 1145360}`); the appid is validated against the Steam store and the game's title becomes the display name. Twitch and YouTube require an interactive creator OAuth and are connected in the dashboard (Settings > Networks). Reconnecting the same app to the same community is idempotent; a Steam app can only back one community (409 on conflict).",
        "operationId": "connectSource",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "provider",
                  "app_id"
                ],
                "properties": {
                  "provider": {
                    "type": "string",
                    "enum": [
                      "steam"
                    ]
                  },
                  "app_id": {
                    "type": "integer",
                    "description": "Steam appid from the game's store page URL"
                  }
                }
              },
              "example": {
                "provider": "steam",
                "app_id": 1145360
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Source connected",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "id": 87,
                    "provider": "steam",
                    "external_id": "1145360",
                    "display_name": "Hades",
                    "status": "active",
                    "connected_at": "2026-07-11T09:30:00+00:00"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid provider or unknown appid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown Steam app_id 999999999"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "409": {
            "description": "Source already connected to another community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "This Steam app is already connected to another community"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          },
          "502": {
            "description": "Steam store lookup failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "upstream_error",
                  "message": "Steam store lookup failed, try again"
                }
              }
            }
          }
        }
      }
    },
    "/v1/communities/{communityId}/sources/{sourceId}": {
      "delete": {
        "tags": [
          "Communities"
        ],
        "summary": "Disconnect audience source",
        "description": "Disconnect an audience source (soft: the row and its verified memberships remain for reconnection). Admin role required. The Discord source cannot be disconnected here \u2014 it is managed by installing or removing the bot.",
        "operationId": "disconnectSource",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "name": "sourceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Source id from GET /communities/{communityId}/sources"
          }
        ],
        "responses": {
          "200": {
            "description": "Source disconnected",
            "content": {
              "application/json": {
                "example": {
                  "data": {
                    "id": 87,
                    "status": "disconnected"
                  }
                }
              }
            }
          },
          "400": {
            "description": "The Discord source cannot be disconnected via the API",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Disconnect Discord by removing the bot from the server"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "List projects",
        "description": "List all projects in the community. Paginated. Filter by `status`, `type`, and/or creation date range (`created_after` / `created_before`). Pass `?expand=script` to include `script.blocks` on each item (adds extra DB queries per project).",
        "operationId": "listProjects",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Return only projects with this status.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "inactive"
              ]
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Return only projects of this type.",
            "schema": {
              "type": "string",
              "enum": [
                "convo",
                "poll"
              ]
            }
          },
          {
            "name": "created_after",
            "in": "query",
            "description": "Return only projects created at or after this ISO 8601 datetime (e.g. `2025-01-01T00:00:00Z`). Assumed UTC if no timezone is specified.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2025-01-01T00:00:00Z"
            }
          },
          {
            "name": "created_before",
            "in": "query",
            "description": "Return only projects created at or before this ISO 8601 datetime (e.g. `2025-12-31T23:59:59Z`). Assumed UTC if no timezone is specified.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2025-12-31T23:59:59Z"
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "script"
              ]
            },
            "description": "Pass `script` to include `script.blocks` on every item in the response."
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of projects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_parameter",
                  "message": "created_after and created_before must be ISO 8601 datetime strings"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Create project",
        "description": "Create a new project. Starts inactive. Script input options (mutually exclusive):\n\n- **`script.blocks`** \u2014 provide an explicit block list; written immediately, no credits consumed\n- **`intent`** \u2014 natural-language description; AI generates blocks and attaches them; consumes bot credits; returns 402 if insufficient\n- **`source_project_id`** \u2014 fork script from an existing project\n- (none) \u2014 project is created with an empty script\n\nWhen `script` or `intent` is provided the response includes a `script.blocks` field.",
        "operationId": "createProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "type"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "example": "Customer Satisfaction Survey"
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "convo",
                      "poll"
                    ]
                  },
                  "source_project_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Fork script from this project ID"
                  },
                  "max_completes_per_user": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1,
                    "description": "Maximum times a single participant can complete this project"
                  },
                  "privacy_mode": {
                    "type": "string",
                    "enum": [
                      "transparent",
                      "semi-private",
                      "anonymous"
                    ],
                    "description": "Controls response visibility and identity disclosure. If omitted, the community's default privacy mode is used (`GET /v1/communities/{communityId}` \u2192 `defaults.privacy_mode`; `anonymous` for communities that never changed it).\n\n- **`transparent`** \u2014 All community members can see exactly who answered what for every question.\n- **`semi-private`** \u2014 Only the project creator and community admins can view individual responses (who answered what). The rest of the community sees aggregated results only if the creator chooses to share them.\n- **`anonymous`** \u2014 Respondent identity is hidden from everyone, including the creator and admins. Data is pseudo-anonymous: answers are recorded but not linked to a user ID or display name, unless a respondent volunteers that information in an open-text answer. **Caveat:** XP and achievements can still be granted in anonymous projects, so participation is not entirely hidden \u2014 particularly relevant in small communities."
                  },
                  "delivery": {
                    "type": "object",
                    "properties": {
                      "opening": {
                        "type": "object",
                        "properties": {
                          "mode": {
                            "type": "string",
                            "enum": [
                              "manual",
                              "scheduled"
                            ],
                            "default": "manual"
                          },
                          "time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Required when mode is `scheduled`"
                          }
                        }
                      },
                      "closing": {
                        "type": "object",
                        "properties": {
                          "mode": {
                            "type": "string",
                            "enum": [
                              "manual",
                              "scheduled"
                            ],
                            "default": "manual"
                          },
                          "time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Required when mode is `scheduled`"
                          }
                        }
                      },
                      "audience": {
                        "type": "object",
                        "properties": {
                          "participation": {
                            "type": "string",
                            "enum": [
                              "private",
                              "open_web"
                            ],
                            "default": "private",
                            "description": "`private`: invite-only via Discord; `open_web`: public link anyone can open"
                          },
                          "response_channel": {
                            "type": "string",
                            "enum": [
                              "discord",
                              "web"
                            ],
                            "default": "discord",
                            "description": "Where respondents complete the survey"
                          },
                          "required_role_ids": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Discord role snowflakes required to participate"
                          },
                          "verified_source_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "Audience source ids (from GET /communities/{communityId}/sources) a respondent may verify with, any-of. Requires participation 'private' and response_channel 'web'."
                          }
                        }
                      },
                      "invitation": {
                        "type": "object",
                        "properties": {
                          "post_channel_id": {
                            "type": "string",
                            "nullable": true,
                            "description": "Discord channel snowflake where the invitation is posted"
                          },
                          "message": {
                            "type": "string",
                            "nullable": true,
                            "description": "Custom invitation message (Markdown)"
                          },
                          "thumbnail_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true
                          },
                          "image_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true
                          },
                          "border_color": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex colour without '#' for the embed border when the project is open (e.g. `e1287e`)"
                          },
                          "border_color_closed": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex colour without '#' for the embed border when the project is closed (e.g. `745399`). Premium only."
                          },
                          "footer": {
                            "type": "string",
                            "nullable": true,
                            "description": "Text shown below the invitation embed. Premium only."
                          },
                          "embed_config": {
                            "type": "object",
                            "nullable": true,
                            "description": "Invitation embed layout \u2014 whether project info shows inline (`embed`) or behind a button, plus per-row visibility toggles",
                            "properties": {
                              "info_display": {
                                "type": "string",
                                "enum": [
                                  "embed",
                                  "button"
                                ],
                                "nullable": true
                              },
                              "rows": {
                                "type": "object",
                                "additionalProperties": {
                                  "type": "boolean"
                                },
                                "description": "Per-row visibility toggles for the invitation embed (open map, e.g. status/privacyMode/roles/xpReward)"
                              }
                            }
                          },
                          "answer_button_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Custom label for the answer/vote button. Premium only."
                          },
                          "answer_button_color": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Discord button style for the answer button (1 primary, 2 secondary, 3 success, 4 danger). Premium only."
                          }
                        }
                      }
                    }
                  },
                  "rewards": {
                    "type": "object",
                    "properties": {
                      "discord_role_id": {
                        "type": "string",
                        "nullable": true,
                        "description": "Discord role snowflake awarded on completion"
                      },
                      "achievement_id": {
                        "type": "string",
                        "nullable": true,
                        "description": "Achievement granted on completion"
                      },
                      "use_xp": {
                        "type": "boolean",
                        "description": "Award XP to participants on completion"
                      },
                      "achievement_enabled": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Enable achievement grant on completion"
                      },
                      "achievement_image_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true,
                        "description": "Badge image URL for the achievement"
                      },
                      "achievement_name": {
                        "type": "string",
                        "nullable": true,
                        "description": "Custom achievement name"
                      },
                      "achievement_role_id": {
                        "type": "string",
                        "nullable": true,
                        "description": "Discord role snowflake granted alongside the achievement"
                      }
                    }
                  },
                  "presentation": {
                    "type": "object",
                    "properties": {
                      "theme_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "ID from GET /v1/themes"
                      },
                      "interviewer_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "ID from GET /v1/interviewers"
                      },
                      "branding": {
                        "type": "string",
                        "nullable": true,
                        "description": "Branding shown in the web survey interface: logo in the header, favicon, and page title"
                      },
                      "chart_emoji": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 1,
                        "description": "Emoji used as the bar character in poll result embeds"
                      }
                    }
                  },
                  "settings": {
                    "type": "object",
                    "properties": {
                      "change_poll": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Allow respondents to change their poll answer after submission"
                      },
                      "poll_results_mode": {
                        "type": "string",
                        "enum": [
                          "public",
                          "voters_only",
                          "hidden"
                        ],
                        "nullable": true,
                        "description": "`public`: results visible in the poll embed; `voters_only`: only voters can see results; `hidden`: results hidden until poll closes"
                      },
                      "reveal_results": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Post a results summary to the results channel when the project closes"
                      }
                    }
                  },
                  "scoring_enabled": {
                    "type": "boolean",
                    "nullable": true,
                    "description": "Enable quiz/scoring mode. When `true`, question blocks may carry `correct_answer_index`, `when_correct`, `when_incorrect`, and per-option `score_values`. Set this **before** writing a script that uses score variables \u2014 without it, `score_values` weights are silently ignored and `[score]`/`[correct_answers]` variables render as literal `[brackets]`. Do not also set `rewards.use_xp: true` when using an in-survey `give_xp` action block with a `source_key` \u2014 that combination awards XP twice."
                  },
                  "script": {
                    "type": "object",
                    "description": "Explicit block list. Mutually exclusive with `intent` and `source_project_id`. The response includes `script.blocks` with the persisted blocks.",
                    "properties": {
                      "blocks": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Block"
                        }
                      }
                    }
                  },
                  "intent": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Natural-language description of the survey goal. Mutually exclusive with `script`. AI generates blocks; response includes `script.blocks` and `credits_used`.",
                    "example": "Measure how satisfied members are with last week's event"
                  },
                  "max_blocks": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 20,
                    "default": 5,
                    "description": "Maximum number of blocks the AI should generate. Only meaningful when `intent` is set."
                  }
                }
              },
              "examples": {
                "general": {
                  "summary": "Customer satisfaction survey (no scoring)",
                  "value": {
                    "name": "Customer Satisfaction Survey",
                    "type": "convo",
                    "privacy_mode": "semi-private",
                    "script": {
                      "blocks": [
                        {
                          "type": "open_text",
                          "prompt": "What did you enjoy most about the event?"
                        },
                        {
                          "type": "single_punch",
                          "prompt": "How would you rate it?",
                          "options": [
                            {
                              "value": "Excellent"
                            },
                            {
                              "value": "Good"
                            },
                            {
                              "value": "Fair"
                            },
                            {
                              "value": "Poor"
                            }
                          ]
                        }
                      ]
                    }
                  }
                },
                "worldCapitalsQuiz": {
                  "summary": "World Capitals Quiz \u2014 create project with scoring enabled",
                  "description": "Creates a right/wrong graded quiz project with scoring enabled in one call. `correct_answer_index` is the 0-based position of the correct option in the `options` array \u2014 no second pass needed. The script here is a short 2-question sample \u2014 see the full 10-question example in `PUT /script`. Do NOT set `rewards.use_xp: true`; the in-survey `give_xp` block handles XP from `[score]`.",
                  "value": {
                    "name": "World Capitals Quiz",
                    "type": "convo",
                    "privacy_mode": "semi-private",
                    "scoring_enabled": true,
                    "script": {
                      "blocks": [
                        {
                          "type": "single_punch",
                          "name": "q1",
                          "prompt": "What is the capital of Australia?",
                          "correct_answer_index": 2,
                          "when_correct": "\u2705 Correct! It's Canberra \u2014 often confused with Sydney.",
                          "when_incorrect": "\u274c Not quite. The answer is **Canberra**.",
                          "options": [
                            {
                              "value": "Sydney"
                            },
                            {
                              "value": "Melbourne"
                            },
                            {
                              "value": "Canberra",
                              "score_values": {
                                "score": 10
                              }
                            },
                            {
                              "value": "Brisbane"
                            }
                          ]
                        },
                        {
                          "type": "single_punch",
                          "name": "q2",
                          "prompt": "What is the capital of Canada?",
                          "correct_answer_index": 3,
                          "when_correct": "\u2705 Correct! Ottawa is the capital.",
                          "when_incorrect": "\u274c Wrong \u2014 it's **Ottawa**, not Toronto.",
                          "options": [
                            {
                              "value": "Toronto"
                            },
                            {
                              "value": "Vancouver"
                            },
                            {
                              "value": "Montreal"
                            },
                            {
                              "value": "Ottawa",
                              "score_values": {
                                "score": 10
                              }
                            }
                          ]
                        },
                        {
                          "type": "action_block",
                          "action_kind": "give_xp",
                          "name": "xp_award",
                          "prompt": "You scored [score] / [max_score] \u2014 here's your XP!",
                          "action_config": {
                            "source_key": "score"
                          }
                        }
                      ]
                    }
                  }
                },
                "championsLeaguePredictionPoll": {
                  "summary": "Prediction poll \u2014 single-question poll, score known up-front, correct answer set after the event",
                  "description": "Creates a `type: \"poll\"` (single-question Discord poll) with per-option score weights. Use this pattern for prediction markets, match outcomes, election calls \u2014 anything where members vote BEFORE the answer is known. The score on each option encodes how rare or risky the pick was (e.g. favourite = 167, underdog = 230). After the event resolves, set `correct_answer_index` via `PUT /projects/{projectId}/script` (0-based position of the winning option in `options`), then award XP from the Responses tab using **`score_correct`** as the dynamic source \u2014 voters who picked correctly get XP equal to their option's weight; voters who missed get zero.",
                  "value": {
                    "name": "2026 Champions League Winner: PSG vs Arsenal",
                    "type": "poll",
                    "privacy_mode": "semi-private",
                    "scoring_enabled": true,
                    "script": {
                      "blocks": [
                        {
                          "type": "single_punch",
                          "name": "winner",
                          "prompt": "Who wins the 2026 Champions League \u2014 PSG or Arsenal?",
                          "options": [
                            {
                              "value": "PSG",
                              "label": "\ud83d\udd35 PSG",
                              "score_values": {
                                "score": 230
                              }
                            },
                            {
                              "value": "Arsenal",
                              "label": "\ud83d\udd34 Arsenal",
                              "score_values": {
                                "score": 167
                              }
                            }
                          ]
                        }
                      ]
                    }
                  }
                },
                "infinityMathQuizPoll": {
                  "summary": "Math quiz poll \u2014 single-question poll with correct answer known up-front",
                  "description": "Creates a `type: \"poll\"` (single-question Discord poll) where the correct answer is known at creation time. Set `correct_answer_index` directly (0-based) and add `when_correct` / `when_incorrect` for instant feedback. The correct option carries `score_values`; wrong options are omitted (resolve to zero). Voters see \u2713 on the correct option only AFTER the poll closes \u2014 the answer key is never leaked while voting is still open.",
                  "value": {
                    "name": "Daily Math Quiz: What is \u221e?",
                    "type": "poll",
                    "privacy_mode": "semi-private",
                    "scoring_enabled": true,
                    "script": {
                      "blocks": [
                        {
                          "type": "single_punch",
                          "name": "infinity",
                          "prompt": "What is \u221e?",
                          "correct_answer_index": 0,
                          "when_correct": "\u2705 Correct! The lemniscate \u221e represents infinity.",
                          "when_incorrect": "\u274c Not quite \u2014 the answer is **Infinity Symbol**.",
                          "options": [
                            {
                              "value": "Infinity Symbol",
                              "score_values": {
                                "score": 100
                              }
                            },
                            {
                              "value": "Hi"
                            },
                            {
                              "value": "ROBLOX"
                            },
                            {
                              "value": "Harry Potter"
                            }
                          ]
                        }
                      ]
                    }
                  }
                },
                "hogwartsHouseSortingHat": {
                  "summary": "Hogwarts House Sorting Hat \u2014 create project with 4-bucket scoring",
                  "description": "Creates a personality quiz project with multi-bucket scoring enabled in one call. Score buckets are **auto-created** from the names used as keys in `score_values` (e.g. `gryffindor`, `slytherin`) \u2014 no separate setup step needed. The script here shows two sample questions \u2014 see the full example in `PUT /script`. Do NOT set `rewards.use_xp: true`.",
                  "value": {
                    "name": "Hogwarts House Sorting Hat",
                    "type": "convo",
                    "privacy_mode": "semi-private",
                    "scoring_enabled": true,
                    "script": {
                      "blocks": [
                        {
                          "type": "single_punch",
                          "name": "q1",
                          "prompt": "A troll has broken into the school. You...",
                          "options": [
                            {
                              "value": "\u2694\ufe0f Charge it head-on",
                              "score_values": {
                                "gryffindor": 3,
                                "slytherin": 1
                              }
                            },
                            {
                              "value": "\ud83e\udde0 Find its weakness first",
                              "score_values": {
                                "ravenclaw": 3,
                                "hufflepuff": 1
                              }
                            },
                            {
                              "value": "\ud83e\udd1d Rally others to help",
                              "score_values": {
                                "hufflepuff": 3,
                                "gryffindor": 1
                              }
                            },
                            {
                              "value": "\ud83c\udfaf Outmanoeuvre it cleverly",
                              "score_values": {
                                "slytherin": 3,
                                "ravenclaw": 1
                              }
                            }
                          ]
                        },
                        {
                          "type": "calculated_block",
                          "name": "house_result",
                          "prompt": "Your house is: [house_result]",
                          "calculated_formula": "argmax([score_gryffindor],[score_slytherin],[score_ravenclaw],[score_hufflepuff])"
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Response includes `data.script.blocks` when `script` or `intent` was provided. If AI generation failed after the project was created, `data.script` is `null` and `data.script_error` is `\"ai_failure\"` \u2014 the project still exists and the script can be added separately.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Project"
                        }
                      ],
                      "properties": {
                        "script": {
                          "oneOf": [
                            {
                              "$ref": "#/components/schemas/Script"
                            },
                            {
                              "type": "null"
                            }
                          ],
                          "description": "Present when `script` or `intent` was provided; null if AI generation failed"
                        },
                        "script_error": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "ai_failure"
                          ],
                          "description": "Set to `ai_failure` when AI script generation failed; script can be added separately"
                        },
                        "credits_used": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Bot credits consumed by AI script generation. Present only when `intent` was provided."
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "789",
                    "community_id": "1152727606572093543",
                    "name": "Event Feedback Survey",
                    "status": "inactive",
                    "info": {
                      "type": "convo",
                      "created_at": "2026-05-01T12:00:00+00:00",
                      "block_count": 3,
                      "is_cloneable": true,
                      "author_id": "42",
                      "author_name": "Alice",
                      "completion_count": 0,
                      "response_count": 0
                    },
                    "settings": {
                      "privacy_mode": "semi-private",
                      "max_completes_per_user": 1,
                      "change_poll": null,
                      "poll_results_mode": null,
                      "reveal_results": null
                    },
                    "delivery": {
                      "opening": {
                        "mode": "manual",
                        "time": null
                      },
                      "closing": {
                        "mode": "manual",
                        "time": null
                      },
                      "audience": {
                        "participation": "private",
                        "response_channel": "discord",
                        "required_role_ids": []
                      },
                      "invitation": {
                        "post_channel_id": null,
                        "message": null,
                        "thumbnail_url": null,
                        "image_url": null,
                        "border_color": null,
                        "border_color_closed": null,
                        "footer": null,
                        "embed_config": null,
                        "answer_button_label": null,
                        "answer_button_color": null
                      }
                    },
                    "rewards": {
                      "discord_role_id": null,
                      "achievement_id": null,
                      "use_xp": true,
                      "achievement_enabled": false
                    },
                    "presentation": {
                      "theme_id": null,
                      "interviewer_id": null,
                      "branding": null,
                      "chart_emoji": null
                    },
                    "script": {
                      "project_id": "789",
                      "blocks": [
                        {
                          "id": 1,
                          "type": "single_punch",
                          "prompt": "How would you rate last week's event overall?",
                          "options": [
                            {
                              "id": 1,
                              "value": "Excellent",
                              "label": null,
                              "emoji": null
                            },
                            {
                              "id": 2,
                              "value": "Good",
                              "label": null,
                              "emoji": null
                            },
                            {
                              "id": 3,
                              "value": "Fair",
                              "label": null,
                              "emoji": null
                            },
                            {
                              "id": 4,
                              "value": "Poor",
                              "label": null,
                              "emoji": null
                            }
                          ],
                          "required": true,
                          "position": 0,
                          "answer_style": null,
                          "precondition": null
                        },
                        {
                          "id": 2,
                          "type": "open_text",
                          "prompt": "What did you enjoy most about the event?",
                          "options": null,
                          "required": false,
                          "position": 1,
                          "answer_style": null,
                          "precondition": null
                        },
                        {
                          "id": 3,
                          "type": "open_text",
                          "prompt": "What could we improve for next time?",
                          "options": null,
                          "required": false,
                          "position": 2,
                          "answer_style": null,
                          "precondition": null
                        }
                      ]
                    },
                    "credits_used": 1
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "mutually_exclusive": {
                    "summary": "script and intent both set",
                    "value": {
                      "error": "invalid_request",
                      "message": "'script' and 'intent' cannot both be set"
                    }
                  },
                  "unknown_block_type": {
                    "summary": "Unrecognised block type",
                    "value": {
                      "error": "invalid_request",
                      "message": "Unknown block type: fancy_block"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Insufficient bot credits (only when `intent` is provided), or a Premium-only invitation field (`border_color_closed`, `footer`, `answer_button_label`, `answer_button_color`) was set on a Basic tier",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "bot_credits": {
                    "summary": "AI generation without enough credits",
                    "value": {
                      "error": "payment_required",
                      "message": "Insufficient bot credits"
                    }
                  },
                  "premium_invitation": {
                    "summary": "Premium-only invitation field on a Basic tier",
                    "value": {
                      "error": "payment_required",
                      "message": "The following invitation fields require a Premium plan: footer, answer_button_label"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}": {
      "get": {
        "tags": [
          "Projects"
        ],
        "summary": "Get project",
        "description": "Get full details for a single project. Pass `?expand=script` to include `script.blocks` in the response.",
        "operationId": "getProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "script"
              ]
            },
            "description": "Pass `script` to include `script.blocks` in the response."
          }
        ],
        "responses": {
          "200": {
            "description": "Project details. Includes `data.script.blocks` when `?expand=script` is passed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Project"
                        }
                      ],
                      "properties": {
                        "script": {
                          "oneOf": [
                            {
                              "$ref": "#/components/schemas/Script"
                            },
                            {
                              "type": "null"
                            }
                          ],
                          "description": "Present only when `?expand=script` is passed"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "put": {
        "tags": [
          "Projects"
        ],
        "summary": "Update project",
        "description": "Update project metadata and deployment settings. Cannot change `type` after creation. Provide `script.blocks` to atomically replace the script in the same call \u2014 project must be inactive when `script` is included.",
        "operationId": "updateProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "max_completes_per_user": {
                    "type": "integer",
                    "minimum": 1
                  },
                  "privacy_mode": {
                    "type": "string",
                    "enum": [
                      "transparent",
                      "semi-private",
                      "anonymous"
                    ],
                    "description": "Controls response visibility and identity disclosure.\n\n- **`transparent`** \u2014 All community members can see exactly who answered what for every question.\n- **`semi-private`** \u2014 Only the project creator and community admins can view individual responses (who answered what). The rest of the community sees aggregated results only if the creator chooses to share them.\n- **`anonymous`** \u2014 Respondent identity is hidden from everyone, including the creator and admins. Data is pseudo-anonymous: answers are recorded but not linked to a user ID or display name, unless a respondent volunteers that information in an open-text answer. **Caveat:** XP and achievements can still be granted in anonymous projects, so participation is not entirely hidden \u2014 particularly relevant in small communities."
                  },
                  "delivery": {
                    "type": "object",
                    "properties": {
                      "opening": {
                        "type": "object",
                        "properties": {
                          "mode": {
                            "type": "string",
                            "enum": [
                              "manual",
                              "scheduled"
                            ]
                          },
                          "time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Required when mode is `scheduled`"
                          }
                        }
                      },
                      "closing": {
                        "type": "object",
                        "properties": {
                          "mode": {
                            "type": "string",
                            "enum": [
                              "manual",
                              "scheduled"
                            ]
                          },
                          "time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "Required when mode is `scheduled`"
                          }
                        }
                      },
                      "audience": {
                        "type": "object",
                        "properties": {
                          "participation": {
                            "type": "string",
                            "enum": [
                              "private",
                              "open_web"
                            ],
                            "description": "`private`: invite-only via Discord; `open_web`: public link anyone can open"
                          },
                          "response_channel": {
                            "type": "string",
                            "enum": [
                              "discord",
                              "web"
                            ],
                            "description": "Where respondents complete the survey"
                          },
                          "required_role_ids": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Discord role snowflakes required to participate"
                          },
                          "verified_source_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "Audience source ids (from GET /communities/{communityId}/sources) a respondent may verify with, any-of. Requires participation 'private' and response_channel 'web'."
                          }
                        }
                      },
                      "invitation": {
                        "type": "object",
                        "properties": {
                          "post_channel_id": {
                            "type": "string",
                            "nullable": true,
                            "description": "Discord channel snowflake where the invitation is posted"
                          },
                          "message": {
                            "type": "string",
                            "nullable": true,
                            "description": "Custom invitation message (Markdown)"
                          },
                          "thumbnail_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true
                          },
                          "image_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true
                          },
                          "border_color": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex colour without '#' for the embed border when the project is open (e.g. `e1287e`)"
                          },
                          "border_color_closed": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex colour without '#' for the embed border when the project is closed (e.g. `745399`). Premium only."
                          },
                          "footer": {
                            "type": "string",
                            "nullable": true,
                            "description": "Text shown below the invitation embed. Premium only."
                          },
                          "embed_config": {
                            "type": "object",
                            "nullable": true,
                            "description": "Invitation embed layout \u2014 whether project info shows inline (`embed`) or behind a button, plus per-row visibility toggles",
                            "properties": {
                              "info_display": {
                                "type": "string",
                                "enum": [
                                  "embed",
                                  "button"
                                ],
                                "nullable": true
                              },
                              "rows": {
                                "type": "object",
                                "additionalProperties": {
                                  "type": "boolean"
                                },
                                "description": "Per-row visibility toggles for the invitation embed (open map, e.g. status/privacyMode/roles/xpReward)"
                              }
                            }
                          },
                          "answer_button_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Custom label for the answer/vote button. Premium only."
                          },
                          "answer_button_color": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Discord button style for the answer button (1 primary, 2 secondary, 3 success, 4 danger). Premium only."
                          }
                        }
                      }
                    }
                  },
                  "rewards": {
                    "type": "object",
                    "properties": {
                      "discord_role_id": {
                        "type": "string",
                        "nullable": true,
                        "description": "Discord role snowflake awarded on completion"
                      },
                      "achievement_id": {
                        "type": "string",
                        "nullable": true,
                        "description": "Achievement granted on completion"
                      },
                      "use_xp": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Award XP to participants on completion"
                      },
                      "achievement_enabled": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Enable achievement grant on completion"
                      },
                      "achievement_image_url": {
                        "type": "string",
                        "format": "uri",
                        "nullable": true,
                        "description": "Badge image URL for the achievement"
                      },
                      "achievement_name": {
                        "type": "string",
                        "nullable": true,
                        "description": "Custom achievement name"
                      },
                      "achievement_role_id": {
                        "type": "string",
                        "nullable": true,
                        "description": "Discord role snowflake granted alongside the achievement"
                      }
                    }
                  },
                  "presentation": {
                    "type": "object",
                    "properties": {
                      "theme_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "ID from GET /v1/themes"
                      },
                      "interviewer_id": {
                        "type": "integer",
                        "nullable": true,
                        "description": "ID from GET /v1/interviewers"
                      },
                      "branding": {
                        "type": "string",
                        "nullable": true,
                        "description": "Branding shown in the web survey interface: logo in the header, favicon, and page title"
                      },
                      "chart_emoji": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 1,
                        "description": "Emoji used as the bar character in poll result embeds"
                      }
                    }
                  },
                  "settings": {
                    "type": "object",
                    "properties": {
                      "change_poll": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Allow respondents to change their poll answer after submission"
                      },
                      "poll_results_mode": {
                        "type": "string",
                        "enum": [
                          "public",
                          "voters_only",
                          "hidden"
                        ],
                        "nullable": true,
                        "description": "`public`: results visible in the poll embed; `voters_only`: only voters can see results; `hidden`: results hidden until poll closes"
                      },
                      "reveal_results": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Post a results summary to the results channel when the project closes"
                      }
                    }
                  },
                  "scoring_enabled": {
                    "type": "boolean",
                    "nullable": true,
                    "description": "Enable or disable quiz/scoring mode. When `true`, question blocks may carry `correct_answer_index`, `when_correct`, `when_incorrect`, and per-option `score_values`. **Must be set before writing a script** that uses score variables \u2014 without it, `score_values` weights are silently ignored and `[score]`/`[correct_answers]` variables render as literal `[brackets]`. Do not also set `rewards.use_xp: true` when using an in-survey `give_xp` action block with a `source_key` \u2014 that combination awards XP twice."
                  },
                  "script": {
                    "type": "object",
                    "description": "Atomically replace the project's script. Project must be inactive. Response includes `script.blocks` with the persisted blocks.",
                    "properties": {
                      "blocks": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Block"
                        }
                      }
                    }
                  }
                }
              },
              "examples": {
                "general": {
                  "summary": "Update name and script",
                  "value": {
                    "name": "Q3 Feedback",
                    "privacy_mode": "transparent",
                    "script": {
                      "blocks": [
                        {
                          "type": "open_text",
                          "prompt": "What could we improve?"
                        },
                        {
                          "type": "single_punch",
                          "prompt": "Would you recommend us?",
                          "options": [
                            {
                              "value": "Yes"
                            },
                            {
                              "value": "No"
                            }
                          ]
                        }
                      ]
                    }
                  }
                },
                "enableScoring": {
                  "summary": "Enable scoring (prerequisite for quiz projects)",
                  "description": "Step 1 of 2 for quiz setup. Enable scoring on the project first, then write the script with `score_values` / `correct_answer_index` in a separate `PUT /script` call. Do NOT set `rewards.use_xp: true` when the script includes a score-based `give_xp` block.",
                  "value": {
                    "scoring_enabled": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated project. Includes `data.script.blocks` when `script` was provided.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/Project"
                        }
                      ],
                      "properties": {
                        "script": {
                          "oneOf": [
                            {
                              "$ref": "#/components/schemas/Script"
                            },
                            {
                              "type": "null"
                            }
                          ],
                          "description": "Present only when `script.blocks` was included in the request"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown block type: fancy_block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "A Premium-only invitation field (`border_color_closed`, `footer`, `answer_button_label`, `answer_button_color`) was set on a Basic tier",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payment_required",
                  "message": "The following invitation fields require a Premium plan: footer, answer_button_label"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Script cannot be modified while the project is active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Script cannot be modified while project is active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "delete": {
        "tags": [
          "Projects"
        ],
        "summary": "Delete project",
        "description": "Permanently delete a project and all responses. Irreversible. Only permitted when status is `inactive`.",
        "operationId": "deleteProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Project must be inactive before deletion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Project must be inactive before deletion"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/clone": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Clone project",
        "description": "Copy this project (script + settings) into a new inactive project in the same community. **Requires Premium tier or above.** Basic-tier keys receive a `402`.",
        "operationId": "cloneProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Defaults to original name + ' (copy)'"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cloned project",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Project"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Project cloning requires Premium or above",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Cloning projects requires a Premium, VIP, or Custom plan."
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/open": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Open project",
        "description": "Activate the project for participant responses. Project must be `inactive`.\n\nOptionally pass a `delivery` body to override audience, closing time, or invitation settings at the moment of opening \u2014 without a separate PUT call. All fields are optional.\n\nIf `delivery.invitation.post_channel_id` is not set and the project uses Discord delivery, the project is still activated but no invitation is posted; a `notice` field is included in the response.",
        "operationId": "openProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "delivery": {
                    "type": "object",
                    "description": "Optional delivery overrides applied at activation time.",
                    "properties": {
                      "audience": {
                        "type": "object",
                        "properties": {
                          "participation": {
                            "type": "string",
                            "enum": [
                              "private",
                              "open_web"
                            ],
                            "description": "`private`: invite-only via Discord; `open_web`: public link anyone can open"
                          },
                          "response_channel": {
                            "type": "string",
                            "enum": [
                              "discord",
                              "web"
                            ],
                            "description": "Where respondents complete the survey"
                          },
                          "required_role_ids": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Discord role snowflakes required to participate"
                          },
                          "verified_source_ids": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "Audience source ids (from GET /communities/{communityId}/sources) a respondent may verify with, any-of. Requires participation 'private' and response_channel 'web'."
                          }
                        }
                      },
                      "closing": {
                        "type": "object",
                        "properties": {
                          "time": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "ISO 8601 datetime at which the project automatically closes"
                          }
                        }
                      },
                      "invitation": {
                        "type": "object",
                        "properties": {
                          "post_channel_id": {
                            "type": "string",
                            "nullable": true,
                            "description": "Discord channel snowflake where the invitation embed is posted on activation"
                          },
                          "message": {
                            "type": "string",
                            "nullable": true,
                            "description": "Custom invitation message (Markdown)"
                          },
                          "thumbnail_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true
                          },
                          "image_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true,
                            "description": "Image URL shown in the invitation embed"
                          },
                          "border_color": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex colour without '#' for the embed border when the project is open (e.g. `e1287e`)"
                          },
                          "border_color_closed": {
                            "type": "string",
                            "nullable": true,
                            "description": "Hex colour without '#' for the embed border when the project is closed (e.g. `745399`). Premium only."
                          },
                          "footer": {
                            "type": "string",
                            "nullable": true,
                            "description": "Text shown below the invitation embed. Premium only."
                          },
                          "embed_config": {
                            "type": "object",
                            "nullable": true,
                            "description": "Invitation embed layout \u2014 whether project info shows inline (`embed`) or behind a button, plus per-row visibility toggles",
                            "properties": {
                              "info_display": {
                                "type": "string",
                                "enum": [
                                  "embed",
                                  "button"
                                ],
                                "nullable": true
                              },
                              "rows": {
                                "type": "object",
                                "additionalProperties": {
                                  "type": "boolean"
                                },
                                "description": "Per-row visibility toggles for the invitation embed (open map, e.g. status/privacyMode/roles/xpReward)"
                              }
                            }
                          },
                          "answer_button_label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Custom label for the answer/vote button. Premium only."
                          },
                          "answer_button_color": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Discord button style for the answer button (1 primary, 2 secondary, 3 success, 4 danger). Premium only."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project activated. Includes a `notice` field if no Discord invitation was posted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ProjectLifecycleResponse"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "A Premium-only invitation override (`border_color_closed`, `footer`, `answer_button_label`, `answer_button_color`) was set on a Basic tier",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payment_required",
                  "message": "The following invitation fields require a Premium plan: footer, answer_button_label"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Project is already active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Project is already active"
                }
              }
            }
          },
          "422": {
            "description": "Discord channel required \u2014 set `delivery.invitation.post_channel_id` on the project first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "channel_required",
                  "message": "Project delivery requires a Discord channel. Set delivery.invitation.post_channel_id on the project before opening."
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/close": {
      "post": {
        "tags": [
          "Projects"
        ],
        "summary": "Close project",
        "description": "Close the project. In-progress participant sessions are allowed to complete.",
        "operationId": "closeProject",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "responses": {
          "200": {
            "description": "Project closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ProjectLifecycleResponse"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Project is not active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Project is not active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/script": {
      "get": {
        "tags": [
          "Script"
        ],
        "summary": "Get script",
        "description": "Get the full ordered main-section script: blocks with type, answer_style, prompt, options, constraints, and action_block config. Pass `?expand=outro` to include read-only post-survey blocks (including outro `title`).",
        "operationId": "getScript",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "outro"
              ]
            },
            "description": "Pass `outro` to include post-survey blocks in read-only mode."
          }
        ],
        "responses": {
          "200": {
            "description": "Script with ordered blocks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Script"
                    }
                  }
                },
                "examples": {
                  "mixedInSurveyAndOutro": {
                    "summary": "GET /script?expand=outro returns main + read-only outro blocks",
                    "value": {
                      "data": {
                        "project_id": "1416",
                        "blocks": [
                          {
                            "id": 201,
                            "section": "main",
                            "type": "single_punch",
                            "prompt": "How satisfied are you with our service?",
                            "options": [
                              {
                                "id": 1,
                                "value": "very_satisfied",
                                "label": "Very satisfied",
                                "emoji": null
                              },
                              {
                                "id": 2,
                                "value": "satisfied",
                                "label": "Satisfied",
                                "emoji": null
                              },
                              {
                                "id": 3,
                                "value": "dissatisfied",
                                "label": "Dissatisfied",
                                "emoji": null
                              }
                            ],
                            "position": 0
                          },
                          {
                            "id": 202,
                            "section": "main",
                            "type": "action_block",
                            "prompt": "Thanks for sharing.",
                            "thumbnail_url": "https://cdn.example.com/role-thumb.png",
                            "action_kind": "give_role",
                            "action_config": {
                              "role_id": "1502388086208987256",
                              "fire_scope": "session"
                            },
                            "continue": {
                              "after": "click",
                              "label": "Continue"
                            },
                            "position": 1
                          },
                          {
                            "id": 301,
                            "section": "outro",
                            "type": "action_block",
                            "title": "Achievement Unlocked",
                            "prompt": "You earned a new achievement.",
                            "action_kind": "give_achievement",
                            "action_config": {
                              "xp_role_id": 1234,
                              "fire_scope": "respondent"
                            },
                            "position": 2
                          },
                          {
                            "id": 302,
                            "section": "outro",
                            "type": "content_block",
                            "title": "Thanks for participating",
                            "prompt": "Your feedback helps us improve.",
                            "image_url": "https://cdn.example.com/thanks.png",
                            "position": 3
                          }
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "put": {
        "tags": [
          "Script"
        ],
        "summary": "Replace script",
        "description": "Replace the entire main-section script atomically. Only permitted when project is `inactive`. You may also include `section=outro` blocks (with `id`) to update outro presentation (`prompt`, `image_url`, `thumbnail_url`, `color`) and pause (`continue.pause`). Reward/action identity for outro blocks remains managed by project rewards/settings.",
        "operationId": "replaceScript",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "blocks"
                ],
                "properties": {
                  "blocks": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Block"
                    }
                  }
                }
              },
              "examples": {
                "basicSatisfaction": {
                  "summary": "Basic satisfaction survey",
                  "description": "Minimal pattern: a welcoming intro content block, one rating question, a conditional follow-up, and an XP reward. q1 is the intro (counts in skip-logic positions); the rating is q2.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Hey [UserName]! Got 30 seconds to tell us how we're doing? \ud83d\udcac",
                        "image_url": "https://i.ibb.co/GfB0H7Z6/subo-equalizer.gif",
                        "continue": {
                          "after": "click",
                          "label": "Let's go! \ud83d\ude80"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "How satisfied are you with our service?",
                        "options": [
                          {
                            "value": "very_satisfied",
                            "label": "\ud83e\udd29 Very satisfied"
                          },
                          {
                            "value": "satisfied",
                            "label": "\ud83d\ude42 Satisfied"
                          },
                          {
                            "value": "dissatisfied",
                            "label": "\ud83d\ude15 Dissatisfied"
                          }
                        ]
                      },
                      {
                        "type": "open_text",
                        "prompt": "What could we do better? \u270d\ufe0f",
                        "required": false,
                        "precondition": "NOT (q2 = \"dissatisfied\")"
                      },
                      {
                        "type": "action_block",
                        "prompt": "Thanks for sharing! \u2728",
                        "color": "e1287e",
                        "action_kind": "give_xp",
                        "action_config": {
                          "xp_amount": 10,
                          "fire_scope": "respondent"
                        },
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      }
                    ]
                  }
                },
                "welcomeQuiz": {
                  "summary": "Welcome quiz (region, language, notifications)",
                  "description": "Onboards new members with three single-question segmentations. q1 is the intro; q2 = region, q3 = language, q4 = notifications. Region and language are recorded as **achievements** (each linked to a Discord role so the badge shows on the member roster). Notification preferences are pure Discord **roles** for opt-in mentions. Replace `xp_role_id` and `role_id` placeholders with values from your community's Rewards configuration. Multi-punch values use list membership: `NOT (\"announcements\" in q4)`.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Hey [UserName]! Welcome to the community \ud83c\udf89 Ready to introduce yourself?",
                        "image_url": "https://i.ibb.co/GfB0H7Z6/subo-equalizer.gif",
                        "continue": {
                          "after": "click",
                          "label": "Let's go! \ud83d\ude80"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Which region are you in?",
                        "options": [
                          {
                            "value": "Americas",
                            "label": "\ud83c\udf0e Americas"
                          },
                          {
                            "value": "Europe",
                            "label": "\ud83c\udf0d Europe"
                          },
                          {
                            "value": "Asia-Pacific",
                            "label": "\ud83c\udf0f Asia-Pacific"
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Which language do you prefer for community discussions?",
                        "options": [
                          {
                            "value": "English",
                            "label": "\ud83c\uddec\ud83c\udde7 English"
                          },
                          {
                            "value": "Spanish",
                            "label": "\ud83c\uddea\ud83c\uddf8 Espa\u00f1ol"
                          },
                          {
                            "value": "French",
                            "label": "\ud83c\uddeb\ud83c\uddf7 Fran\u00e7ais"
                          },
                          {
                            "value": "Japanese",
                            "label": "\ud83c\uddef\ud83c\uddf5 \u65e5\u672c\u8a9e"
                          }
                        ]
                      },
                      {
                        "type": "multi_punch",
                        "prompt": "Which notifications would you like to receive?",
                        "min": 0,
                        "options": [
                          {
                            "value": "announcements",
                            "label": "\ud83d\udce3 Announcements"
                          },
                          {
                            "value": "giveaways",
                            "label": "\ud83c\udf81 Giveaways"
                          },
                          {
                            "value": "events",
                            "label": "\ud83d\udcc5 Events"
                          }
                        ]
                      },
                      {
                        "type": "action_block",
                        "prompt": "Welcome to the Americas crew! \ud83c\udf0e",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 1001
                        },
                        "precondition": "NOT (q2 = \"Americas\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "Welcome to the Europe crew! \ud83c\udf0d",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 1002
                        },
                        "precondition": "NOT (q2 = \"Europe\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "Welcome to the Asia-Pacific crew! \ud83c\udf0f",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 1003
                        },
                        "precondition": "NOT (q2 = \"Asia-Pacific\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "Marking you as a Spanish speaker \u2014 \u00a1bienvenido! \ud83c\uddea\ud83c\uddf8",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 1011
                        },
                        "precondition": "NOT (q3 = \"Spanish\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "Marking you as a French speaker \u2014 bienvenue ! \ud83c\uddeb\ud83c\uddf7",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 1012
                        },
                        "precondition": "NOT (q3 = \"French\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "Marking you as a Japanese speaker \u2014 \u3088\u3046\u3053\u305d\uff01\ud83c\uddef\ud83c\uddf5",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 1013
                        },
                        "precondition": "NOT (q3 = \"Japanese\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "You're subscribed to Announcements \ud83d\udce3",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417454"
                        },
                        "precondition": "NOT (\"announcements\" in q4)",
                        "continue": {
                          "after": "pause",
                          "pause": 1
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "You're subscribed to Giveaways \ud83c\udf81",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417455"
                        },
                        "precondition": "NOT (\"giveaways\" in q4)",
                        "continue": {
                          "after": "pause",
                          "pause": 1
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "You're subscribed to Events \ud83d\udcc5",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417456"
                        },
                        "precondition": "NOT (\"events\" in q4)",
                        "continue": {
                          "after": "pause",
                          "pause": 1
                        }
                      }
                    ]
                  }
                },
                "hogwartsHouseSortingHat": {
                  "summary": "Hogwarts House Sorting Hat (personality quiz)",
                  "description": "Personality quiz with multi-bucket scoring and `argmax` resolution. **Prerequisite:** call `PUT /projects/{projectId}` with `scoring_enabled: true` and do NOT set `rewards.use_xp: true` (the in-survey give_xp block handles XP; enabling both double-awards). Five emoji-only questions distribute hidden points across four House buckets (Gryffindor, Slytherin, Ravenclaw, Hufflepuff) via `score_values` on each option. A `calculated_block` running `argmax([score_gryffindor],\u2026)` resolves the winning House as a string. Four reveal `content_block`s are each hidden with `NOT((q7=\"<House>\"))` \u2014 exactly one fires. A second `calculated_block` computes Gryffindor alignment % (`[score_gryffindor]/[max_score_gryffindor]*100`); a bonus `give_xp` block fires only when that value > 95 with `source_key: \"score_gryffindor\"` so XP equals the raw score rather than a fixed amount. **Outro blocks are omitted** \u2014 they are pre-synthesized by the platform and require their existing `id` to update; manage completion XP via the project's `rewards` settings instead. See `docs/recipes/hogwarts-house-sorting-quiz.md` for the full walkthrough.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Welcome to Hogwarts, [UserName], where every great journey begins with a choice.\n\nI shall ask you a handful of questions, and together we'll see which House calls to you most strongly.\n\nAre you ready to be sorted?",
                        "image_url": "https://static.klipy.com/ii/d7aec6f6f171607374b2065c836f92f4/8b/74/m8Nr5iZF.gif",
                        "continue": {
                          "after": "click",
                          "label": "I'm ready! \ud83e\ude84"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Which quality do you value most?",
                        "answer_style": "emoji_only",
                        "options": [
                          {
                            "value": "\ud83e\udd81 Bravery",
                            "emoji": "\ud83e\udd81",
                            "score_values": {
                              "gryffindor": 3,
                              "hufflepuff": 1
                            }
                          },
                          {
                            "value": "\ud83e\udd89 Intelligence",
                            "emoji": "\ud83e\udd89",
                            "score_values": {
                              "ravenclaw": 3,
                              "slytherin": 1
                            }
                          },
                          {
                            "value": "\ud83e\udd85 Ambition",
                            "emoji": "\ud83e\udd85",
                            "score_values": {
                              "gryffindor": 1,
                              "slytherin": 3
                            }
                          },
                          {
                            "value": "\ud83e\udda1 Loyalty",
                            "emoji": "\ud83e\udda1",
                            "score_values": {
                              "gryffindor": 1,
                              "hufflepuff": 3
                            }
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "What activity do you enjoy the most?",
                        "answer_style": "emoji_only",
                        "options": [
                          {
                            "value": "\u2694\ufe0f Sports",
                            "emoji": "\u2694\ufe0f",
                            "score_values": {
                              "gryffindor": 2,
                              "hufflepuff": 1
                            }
                          },
                          {
                            "value": "\ud83d\udcda Reading",
                            "emoji": "\ud83d\udcda",
                            "score_values": {
                              "ravenclaw": 2,
                              "hufflepuff": 1
                            }
                          },
                          {
                            "value": "\ud83c\udfa8 Arts",
                            "emoji": "\ud83c\udfa8",
                            "score_values": {
                              "ravenclaw": 2,
                              "slytherin": 1
                            }
                          },
                          {
                            "value": "\ud83c\udf0d Adventure",
                            "emoji": "\ud83c\udf0d",
                            "score_values": {
                              "gryffindor": 2,
                              "slytherin": 1
                            }
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "How do you handle challenges?",
                        "answer_style": "emoji_only",
                        "options": [
                          {
                            "value": "\ud83d\udcaa Face them head-on",
                            "emoji": "\ud83d\udcaa",
                            "score_values": {
                              "gryffindor": 3,
                              "slytherin": 1
                            }
                          },
                          {
                            "value": "\ud83e\udde0 Analyze and plan",
                            "emoji": "\ud83e\udde0",
                            "score_values": {
                              "ravenclaw": 3,
                              "slytherin": 1
                            }
                          },
                          {
                            "value": "\ud83d\ude80 Take risks",
                            "emoji": "\ud83d\ude80",
                            "score_values": {
                              "gryffindor": 1,
                              "slytherin": 3
                            }
                          },
                          {
                            "value": "\ud83e\udd1d Seek help from friends",
                            "emoji": "\ud83e\udd1d",
                            "score_values": {
                              "ravenclaw": 1,
                              "hufflepuff": 3
                            }
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "What would you do with a day off?",
                        "answer_style": "emoji_only",
                        "options": [
                          {
                            "value": "\ud83c\udfae Play video games",
                            "emoji": "\ud83c\udfae",
                            "score_values": {
                              "ravenclaw": 1,
                              "slytherin": 1
                            }
                          },
                          {
                            "value": "\ud83c\udf3f Explore nature",
                            "emoji": "\ud83c\udf3f",
                            "score_values": {
                              "gryffindor": 1,
                              "hufflepuff": 2
                            }
                          },
                          {
                            "value": "\ud83c\udfdb Visit a museum",
                            "emoji": "\ud83c\udfdb",
                            "score_values": {
                              "ravenclaw": 2,
                              "slytherin": 1
                            }
                          },
                          {
                            "value": "\ud83d\udcd6 Read a book",
                            "emoji": "\ud83d\udcd6",
                            "score_values": {
                              "ravenclaw": 2,
                              "hufflepuff": 1
                            }
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Describe your ideal magical creature.",
                        "answer_style": "emoji_only",
                        "options": [
                          {
                            "value": "\ud83e\udd85 Phoenix",
                            "emoji": "\ud83e\udd85",
                            "score_values": {
                              "ravenclaw": 1,
                              "gryffindor": 3
                            }
                          },
                          {
                            "value": "\ud83e\udd89 Owl",
                            "emoji": "\ud83e\udd89",
                            "score_values": {
                              "ravenclaw": 3,
                              "hufflepuff": 1
                            }
                          },
                          {
                            "value": "\ud83e\udd86 Niffler",
                            "emoji": "\ud83e\udd86",
                            "score_values": {
                              "gryffindor": 1,
                              "slytherin": 3
                            }
                          },
                          {
                            "value": "\ud83d\udc0d Snake",
                            "emoji": "\ud83d\udc0d",
                            "score_values": {
                              "ravenclaw": 1,
                              "slytherin": 3
                            }
                          }
                        ]
                      },
                      {
                        "type": "calculated_block",
                        "prompt": "",
                        "calculated_formula": "argmax([score_gryffindor],[score_slytherin],[score_ravenclaw],[score_hufflepuff])"
                      },
                      {
                        "type": "content_block",
                        "prompt": "Courage burns brightly within you, [UserName].\n\n# Better be\u2026 GRYFFINDOR! \ud83e\udd81",
                        "image_url": "https://static.klipy.com/ii/c3a19a0b747a76e98651f2b9a3cca5ff/97/3f/rM3ru698.gif",
                        "precondition": "NOT((q7=\"Gryffindor\"))",
                        "continue": {
                          "after": "pause",
                          "pause": 4
                        }
                      },
                      {
                        "type": "content_block",
                        "prompt": "Your mind is sharp and curious, [UserName].\n\n# Clearly, you belong in\u2026 RAVENCLAW! \ud83e\udd89",
                        "image_url": "https://static.klipy.com/ii/c3a19a0b747a76e98651f2b9a3cca5ff/97/3f/rM3ru698.gif",
                        "precondition": "NOT((q7=\"Ravenclaw\"))",
                        "continue": {
                          "after": "pause",
                          "pause": 4
                        }
                      },
                      {
                        "type": "content_block",
                        "prompt": "Ambitious and determined, [UserName].\n\n# SLYTHERIN! \ud83d\udc0d",
                        "image_url": "https://static.klipy.com/ii/4e7bea9f7a3371424e6c16ebc93252fe/df/26/roHWU8L0kMuzXES.gif",
                        "precondition": "NOT((q7=\"Slytherin\"))",
                        "continue": {
                          "after": "pause",
                          "pause": 4
                        }
                      },
                      {
                        "type": "content_block",
                        "prompt": "Kind, patient, and fiercely loyal, [UserName].\n\n# HUFFLEPUFF! \ud83e\udda1",
                        "image_url": "https://static.klipy.com/ii/7607a26399874a14744aa5e7accfa062/0b/7a/6HtcJQ15.gif",
                        "precondition": "NOT((q7=\"Hufflepuff\"))",
                        "continue": {
                          "after": "pause",
                          "pause": 4
                        }
                      },
                      {
                        "type": "calculated_block",
                        "prompt": "",
                        "calculated_formula": "[score_gryffindor]/[max_score_gryffindor]*100"
                      },
                      {
                        "type": "action_block",
                        "action_kind": "give_xp",
                        "action_config": {
                          "source_key": "score_gryffindor",
                          "fire_scope": "respondent"
                        },
                        "precondition": "NOT((q12>95))",
                        "color": "740001",
                        "prompt": "You are not just Gryffindor \u2014 you are **pure Gryffindor!**\n\nScore: [score_gryffindor] / [max_score_gryffindor]\n[xp_points] [xp_name] bonus!",
                        "continue": {
                          "after": "pause",
                          "pause": 4
                        }
                      },
                      {
                        "type": "content_block",
                        "prompt": "# Your final scores\n\nGryffindor: [score_gryffindor] (max: [max_score_gryffindor])\nSlytherin: [score_slytherin] (max: [max_score_slytherin])\nHufflepuff: [score_hufflepuff] (max: [max_score_hufflepuff])\nRavenclaw: [score_ravenclaw] (max: [max_score_ravenclaw])",
                        "image_url": "https://contentful.harrypotter.com/usf1vwtuqyxm/3e6KEgfIcdQwZf4ss6wI77/731fe1da0bde50b22301d8e744610f80/the-sorting-hat_1_1800x1248.png",
                        "continue": {
                          "after": "click",
                          "label": "I understand where I belong! \ud83d\ude0f"
                        }
                      }
                    ]
                  }
                },
                "predictionPollSingleQuestion": {
                  "summary": "Prediction poll \u2014 replace script with a single-question poll body, weighted options",
                  "description": "Replaces the script of an existing `type: \"poll\"` project with a single graded question. Polls accept exactly one question block in the main section \u2014 multiple question blocks will be rejected. **Workflow:** create the project with `scoring_enabled: true`; write this single-block script before the event; publish + collect votes; once the outcome is known, re-call this endpoint with `correct_answer_index` set, then open the Responses tab and pick `score_correct` as the dynamic XP source to award proportionally.",
                  "value": {
                    "blocks": [
                      {
                        "type": "single_punch",
                        "name": "winner",
                        "prompt": "Who wins the 2026 Champions League \u2014 PSG or Arsenal?",
                        "options": [
                          {
                            "value": "PSG",
                            "label": "\ud83d\udd35 PSG",
                            "score_values": {
                              "score": 230
                            }
                          },
                          {
                            "value": "Arsenal",
                            "label": "\ud83d\udd34 Arsenal",
                            "score_values": {
                              "score": 167
                            }
                          }
                        ]
                      }
                    ]
                  }
                },
                "worldCapitalsQuiz": {
                  "summary": "World Capitals geography quiz (right/wrong grading, score-based XP)",
                  "description": "Traditional 10-question quiz with one correct answer per question, instant per-question feedback, and XP automatically equal to the final score. **Prerequisite:** call `PUT /projects/{projectId}` with `scoring_enabled: true` before writing this script, and do NOT set `rewards.use_xp: true` (double-awards XP). Without `scoring_enabled`, `score_values` weights are ignored and `[score]`/`[correct_answers]` variables render as literal `[brackets]`. Each question sets `correct_answer_index` (0-based position of the correct option) and a `when_correct` / `when_incorrect` message. The correct option carries `score_values: {\"score\": 10}`; all others are omitted. A closing `content_block` shows the final tally; score-based XP is awarded via a main-section `give_xp` action block with `source_key: \"score\"` \u2014 XP = 10 \u00d7 correct answers, no per-question logic needed. **Outro blocks are omitted** \u2014 they are pre-synthesized by the platform and require their existing `id` to update; manage completion messaging via the project's `rewards` settings instead. See `docs/recipes/world-capitals-quiz.md` for the full question set and variations.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Ready to test your world geography knowledge, [UserName]?\n\n10 questions. 10 XP each for a correct answer. Maximum **100 XP**.\n\nA warning: some of these are trickier than they look. \ud83d\ude0f",
                        "continue": {
                          "after": "click",
                          "label": "Let's go! \ud83c\udf0d"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "What is the capital of Australia?",
                        "correct_answer_index": 1,
                        "when_correct": "\u2705 Correct! Canberra has been Australia's capital since 1913 \u2014 chosen as a compromise between Sydney and Melbourne. Running score: [score].",
                        "when_incorrect": "\u274c Not quite! The answer is **Canberra**, not Sydney. Australia's largest city is not its capital.",
                        "options": [
                          {
                            "value": "Sydney",
                            "label": "\ud83c\udfd9 Sydney"
                          },
                          {
                            "value": "Canberra",
                            "label": "\ud83c\udfdb Canberra",
                            "score_values": {
                              "score": 10
                            }
                          },
                          {
                            "value": "Melbourne",
                            "label": "\u2615 Melbourne"
                          },
                          {
                            "value": "Brisbane",
                            "label": "\u2600\ufe0f Brisbane"
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "What is the capital of Brazil?",
                        "correct_answer_index": 2,
                        "when_correct": "\u2705 Correct! Bras\u00edlia was purpose-built as Brazil's new capital and inaugurated in 1960. Score so far: [score] / [max_score].",
                        "when_incorrect": "\u274c Not quite! **Bras\u00edlia** is the capital \u2014 built from scratch to replace Rio de Janeiro.",
                        "options": [
                          {
                            "value": "Rio de Janeiro",
                            "label": "\ud83c\udfd6 Rio de Janeiro"
                          },
                          {
                            "value": "S\u00e3o Paulo",
                            "label": "\ud83c\udfd9 S\u00e3o Paulo"
                          },
                          {
                            "value": "Bras\u00edlia",
                            "label": "\ud83c\udfdb Bras\u00edlia",
                            "score_values": {
                              "score": 10
                            }
                          },
                          {
                            "value": "Salvador",
                            "label": "\ud83e\udd41 Salvador"
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "What is the capital of New Zealand?",
                        "correct_answer_index": 2,
                        "when_correct": "\u2705 Correct! Wellington has been New Zealand's capital since 1865. [correct_answers] / [max_correct_answers] correct so far.",
                        "when_incorrect": "\u274c Not quite! **Wellington** is the capital. Auckland is the largest city but is not the seat of government.",
                        "options": [
                          {
                            "value": "Auckland",
                            "label": "\u26f5 Auckland"
                          },
                          {
                            "value": "Christchurch",
                            "label": "\ud83c\udf3f Christchurch"
                          },
                          {
                            "value": "Wellington",
                            "label": "\ud83c\udfdb Wellington",
                            "score_values": {
                              "score": 10
                            }
                          },
                          {
                            "value": "Dunedin",
                            "label": "\ud83d\udc27 Dunedin"
                          }
                        ]
                      },
                      {
                        "type": "content_block",
                        "prompt": "# Your result\n\nYou got **[correct_answers] out of [max_correct_answers]** correct.\n\nTotal score: **[score] / [max_score]** points. \ud83c\udf0d",
                        "continue": {
                          "after": "click",
                          "label": "Claim my XP reward \ud83c\udf81"
                        }
                      },
                      {
                        "type": "action_block",
                        "action_kind": "give_xp",
                        "action_config": {
                          "source_key": "score",
                          "fire_scope": "session"
                        },
                        "prompt": "You answered [correct_answers] / [max_correct_answers] correctly and earned **[xp_points] [xp_name]**!\n\n- Total XP: [earned_xp]\n- This month: [month_total_xp]",
                        "continue": {
                          "after": "pause",
                          "pause": 4
                        }
                      }
                    ]
                  }
                },
                "volunteerModeratorFunnel": {
                  "summary": "Volunteer / moderator recruitment funnel",
                  "description": "Single-decisive-question screening. q1 is the intro; q2 (weekly commitment) gates Mod Trial vs Waitlist via two mutually-exclusive action blocks. q3\u2013q5 collect data for manual review.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Hey [UserName]! Thanks for considering helping us moderate \ud83d\udee1 A few quick questions.",
                        "image_url": "https://i.ibb.co/GfB0H7Z6/subo-equalizer.gif",
                        "continue": {
                          "after": "click",
                          "label": "Let's go! \ud83d\ude80"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "How much time can you commit to moderating each week?",
                        "options": [
                          {
                            "value": "5+ hours",
                            "label": "\ud83c\udfc6 5+ hours"
                          },
                          {
                            "value": "1-4 hours",
                            "label": "\ud83e\udd1d 1\u20134 hours"
                          },
                          {
                            "value": "Less than 1 hour",
                            "label": "\ud83d\udc4b Less than 1 hour"
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Which time zone bucket fits your usual online hours?",
                        "options": [
                          {
                            "value": "Americas-East",
                            "label": "\ud83c\udf0e Americas (East)"
                          },
                          {
                            "value": "Americas-West",
                            "label": "\ud83c\udf0e Americas (West)"
                          },
                          {
                            "value": "Europe",
                            "label": "\ud83c\udf0d Europe / Africa"
                          },
                          {
                            "value": "Asia-Pacific",
                            "label": "\ud83c\udf0f Asia-Pacific"
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Have you moderated a community before?",
                        "options": [
                          {
                            "value": "Yes \u2014 paid/professional",
                            "label": "\ud83d\udcbc Yes \u2014 paid/professional"
                          },
                          {
                            "value": "Yes \u2014 volunteer",
                            "label": "\ud83e\udd32 Yes \u2014 volunteer"
                          },
                          {
                            "value": "No \u2014 first time",
                            "label": "\ud83c\udf31 No, first time"
                          }
                        ]
                      },
                      {
                        "type": "open_text",
                        "prompt": "In 2\u20133 sentences: how would you handle a heated argument between two members? \u270d\ufe0f",
                        "required": true
                      },
                      {
                        "type": "action_block",
                        "prompt": "\ud83d\udee1 Mod Trial granted \u2014 you'll get a DM with next steps. Welcome to the trial team!",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417460"
                        },
                        "precondition": "NOT (q2 = \"5+ hours\")",
                        "continue": {
                          "after": "click",
                          "label": "Continue"
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "\ud83d\ude4f Thanks for your interest! We're keeping your application on file for the next round.",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417461"
                        },
                        "precondition": "q2 = \"5+ hours\"",
                        "continue": {
                          "after": "click",
                          "label": "Continue"
                        }
                      }
                    ]
                  }
                },
                "eventRsvpStreak": {
                  "summary": "Event RSVP with attendance streaks",
                  "description": "RSVP funnel: q1 is the intro; q2 gates the `RSVPed` role. Maybe/No respondents still earn XP for engaging. Streaks emerge by running this project per event with `max_completes_per_user > 1` and pairing it with a post-event attendance project \u2014 accumulated XP crosses tier-achievement thresholds organically. The give-xp block uses `fire_scope: \"session\"` so the bonus repeats per session.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Hey [UserName]! Friday's AMA is around the corner \ud83c\udf99 Got a sec to lock in your RSVP?",
                        "image_url": "https://i.ibb.co/GfB0H7Z6/subo-equalizer.gif",
                        "continue": {
                          "after": "click",
                          "label": "Let's go! \ud83d\ude80"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Will you join Friday's community AMA?",
                        "options": [
                          {
                            "value": "Yes",
                            "label": "\u2705 Yes, I'll be there"
                          },
                          {
                            "value": "Maybe",
                            "label": "\ud83e\udd14 Maybe"
                          },
                          {
                            "value": "No",
                            "label": "\u274c Can't make it this time"
                          }
                        ]
                      },
                      {
                        "type": "multi_punch",
                        "prompt": "Which topics would you most like covered?",
                        "min": 0,
                        "options": [
                          {
                            "value": "roadmap",
                            "label": "\ud83d\uddfa Roadmap & upcoming features"
                          },
                          {
                            "value": "community",
                            "label": "\ud83e\udd1d Community & moderation"
                          },
                          {
                            "value": "behind-scenes",
                            "label": "\ud83c\udfac Behind-the-scenes"
                          },
                          {
                            "value": "ama",
                            "label": "\u2753 Open AMA questions"
                          }
                        ],
                        "precondition": "q2 = \"No\""
                      },
                      {
                        "type": "open_text",
                        "prompt": "Any specific question you'd like answered live? \u2728",
                        "required": false,
                        "precondition": "q2 = \"No\""
                      },
                      {
                        "type": "action_block",
                        "prompt": "\ud83d\uddd3 RSVPed! You'll get a reminder DM an hour before the AMA.",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417470"
                        },
                        "precondition": "NOT (q2 = \"Yes\")",
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "\ud83d\ude4f Thanks for stopping by! You earned XP for sharing your interest.",
                        "action_kind": "give_xp",
                        "action_config": {
                          "xp_amount": 10,
                          "fire_scope": "session"
                        },
                        "continue": {
                          "after": "pause",
                          "pause": 2
                        }
                      }
                    ]
                  }
                },
                "playtesterSelection": {
                  "summary": "Playtester selection (indie game dev)",
                  "description": "Recruits qualified playtesters, not just enthusiastic ones. q1 is the intro; q2 (weekly availability) is the decisive gate. The qualifying applicant gets a **Beta Tester achievement** (persistent profile badge plus the linked Discord role); everyone else is added to a **Waitlist** role for the next round. The required open-text answer filters out low-effort applicants.",
                  "value": {
                    "blocks": [
                      {
                        "type": "content_block",
                        "prompt": "Hey [UserName]! We're picking the next round of playtesters \ud83c\udfae Want in?",
                        "image_url": "https://i.ibb.co/GfB0H7Z6/subo-equalizer.gif",
                        "continue": {
                          "after": "click",
                          "label": "Let's go! \ud83d\ude80"
                        }
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Can you commit to a weekly 2\u20133 hour session for the next 4 weeks?",
                        "options": [
                          {
                            "value": "Yes, every week",
                            "label": "\ud83d\udcaa Yes, every week"
                          },
                          {
                            "value": "Yes, most weeks",
                            "label": "\ud83d\udc4d Yes, most weeks"
                          },
                          {
                            "value": "Occasionally",
                            "label": "\ud83e\udd1e Occasionally"
                          },
                          {
                            "value": "Not right now",
                            "label": "\ud83d\udc4b Not right now"
                          }
                        ]
                      },
                      {
                        "type": "multi_punch",
                        "prompt": "Which platforms can you test on?",
                        "min": 1,
                        "options": [
                          {
                            "value": "PC",
                            "label": "\ud83d\udda5 PC (Windows)"
                          },
                          {
                            "value": "Mac",
                            "label": "\ud83c\udf4e Mac"
                          },
                          {
                            "value": "Steam Deck",
                            "label": "\ud83c\udfae Steam Deck"
                          },
                          {
                            "value": "Console",
                            "label": "\ud83d\udd79 Console"
                          }
                        ]
                      },
                      {
                        "type": "single_punch",
                        "prompt": "Which genres do you play most?",
                        "options": [
                          {
                            "value": "RPG",
                            "label": "\u2694\ufe0f RPG"
                          },
                          {
                            "value": "Strategy",
                            "label": "\u265f Strategy"
                          },
                          {
                            "value": "Action",
                            "label": "\ud83d\udca5 Action"
                          },
                          {
                            "value": "Puzzle",
                            "label": "\ud83e\udde9 Puzzle"
                          },
                          {
                            "value": "Sim",
                            "label": "\ud83d\ude9c Simulation"
                          }
                        ]
                      },
                      {
                        "type": "open_text",
                        "prompt": "Briefly: a game you've given feedback on, and one thing you noticed others missed. \u270d\ufe0f",
                        "required": true
                      },
                      {
                        "type": "action_block",
                        "prompt": "\ud83e\uddea You're in! Beta Tester achievement unlocked \u2014 watch for a DM with the build link.",
                        "action_kind": "give_achievement",
                        "action_config": {
                          "xp_role_id": 2001
                        },
                        "precondition": "NOT (q2 = \"Yes, every week\")",
                        "continue": {
                          "after": "click",
                          "label": "Got it"
                        }
                      },
                      {
                        "type": "action_block",
                        "prompt": "\ud83d\ude4f Thanks for applying! You're on the waitlist \u2014 we'll reach out if a slot opens up.",
                        "action_kind": "give_role",
                        "action_config": {
                          "role_id": "1382089724239417480"
                        },
                        "precondition": "q2 = \"Yes, every week\"",
                        "continue": {
                          "after": "click",
                          "label": "Got it"
                        }
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated script",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Script"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown block type: fancy_block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Script cannot be replaced while the project is active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Script cannot be modified while project is active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/script/blocks": {
      "post": {
        "tags": [
          "Script"
        ],
        "summary": "Add block",
        "description": "Append a new main-section block, or insert at a specific `position`. Only permitted when project is `inactive`. This endpoint does not create outro blocks; use `PUT /script` for controlled outro updates.",
        "operationId": "createBlock",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Block"
              },
              "example": {
                "type": "action_block",
                "prompt": "Role granted.",
                "thumbnail_url": "https://example.com/role-thumb.png",
                "action_kind": "give_role",
                "action_config": {
                  "role_id": "1382089724239417454",
                  "fire_scope": "session"
                },
                "continue": {
                  "after": "click",
                  "label": "Continue"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Updated script with new block",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Script"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid block definition",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown block type: fancy_block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Blocks cannot be added while the project is active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Blocks cannot be added while project is active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/script/blocks/{blockId}": {
      "put": {
        "tags": [
          "Script"
        ],
        "summary": "Update block",
        "description": "Update a single main-section block's content, options, constraints, action config, or position. Project must be `inactive`. This endpoint does not update outro blocks; use `PUT /script` with `section=outro` + `id` for outro content/pause updates.",
        "operationId": "updateBlock",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "$ref": "#/components/parameters/blockId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All fields optional. Only provided fields are updated.",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The block's variable token, e.g. `belonging` -> `[Belonging]`. Renaming does not rewrite existing references."
                  },
                  "section": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "main",
                      "intro",
                      "outro"
                    ]
                  },
                  "type": {
                    "type": "string",
                    "enum": [
                      "single_punch",
                      "multi_punch",
                      "open_text",
                      "open_numeric",
                      "rating",
                      "opinion_scale",
                      "nps",
                      "ranking",
                      "content_block",
                      "action_block",
                      "calculated_block"
                    ]
                  },
                  "answer_style": {
                    "type": "string",
                    "enum": [
                      "full_text",
                      "emoji_only",
                      "select_menu",
                      "buttons"
                    ],
                    "nullable": true
                  },
                  "image_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true
                  },
                  "thumbnail_url": {
                    "type": "string",
                    "format": "uri",
                    "nullable": true
                  },
                  "prompt": {
                    "type": "string",
                    "minLength": 1
                  },
                  "options": {
                    "type": "array",
                    "nullable": true,
                    "items": {
                      "type": "object",
                      "required": [
                        "value"
                      ],
                      "properties": {
                        "id": {
                          "type": "integer",
                          "nullable": true,
                          "description": "Backend answer id. Omit (or send null) to insert; include the existing id to update in place. Preserves response.answer_id references."
                        },
                        "value": {
                          "type": "string",
                          "minLength": 1
                        },
                        "label": {
                          "type": "string",
                          "nullable": true
                        },
                        "emoji": {
                          "type": "string",
                          "nullable": true
                        },
                        "score_values": {
                          "type": "object",
                          "nullable": true,
                          "additionalProperties": {
                            "type": "number"
                          }
                        },
                        "display_order": {
                          "type": "integer",
                          "nullable": true
                        },
                        "anchor_position": {
                          "type": "string",
                          "nullable": true,
                          "enum": [
                            "last",
                            null
                          ]
                        }
                      }
                    }
                  },
                  "min": {
                    "type": "integer",
                    "nullable": true
                  },
                  "max": {
                    "type": "integer",
                    "nullable": true
                  },
                  "required": {
                    "type": "boolean",
                    "nullable": true
                  },
                  "precondition": {
                    "type": "string",
                    "nullable": true
                  },
                  "action_kind": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "give_xp",
                      "give_achievement",
                      "give_role"
                    ]
                  },
                  "action_config": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "xp_amount": {
                        "type": "integer",
                        "nullable": true
                      },
                      "xp_role_id": {
                        "type": "integer",
                        "nullable": true
                      },
                      "role_id": {
                        "type": "string",
                        "nullable": true
                      },
                      "fire_scope": {
                        "type": "string",
                        "nullable": true,
                        "enum": [
                          "respondent",
                          "session"
                        ]
                      }
                    }
                  },
                  "position": {
                    "type": "integer",
                    "nullable": true
                  },
                  "continue": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "after": {
                        "type": "string",
                        "enum": [
                          "pause",
                          "click"
                        ]
                      },
                      "pause": {
                        "type": "integer",
                        "nullable": true
                      },
                      "label": {
                        "type": "string",
                        "nullable": true
                      }
                    }
                  }
                }
              },
              "example": {
                "action_kind": "give_achievement",
                "action_config": {
                  "xp_role_id": 123
                },
                "continue": {
                  "after": "click",
                  "label": "Let's go! \ud83d\ude80"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated script",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Script"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid block definition",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown block type: fancy_block"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "description": "Project or block not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "not_found",
                  "message": "Block not found in this project"
                }
              }
            }
          },
          "409": {
            "description": "Blocks cannot be modified while the project is active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Blocks cannot be modified while project is active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "delete": {
        "tags": [
          "Script"
        ],
        "summary": "Delete block",
        "description": "Remove a block from the script. Project must be `inactive`. Skip logic referencing this block is not automatically updated.",
        "operationId": "deleteBlock",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "$ref": "#/components/parameters/blockId"
          }
        ],
        "responses": {
          "200": {
            "description": "Updated script (block removed)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Script"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "description": "Project or block not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "not_found",
                  "message": "Block not found in this project"
                }
              }
            }
          },
          "409": {
            "description": "Blocks cannot be deleted while the project is active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Blocks cannot be deleted while project is active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/generate": {
      "post": {
        "tags": [
          "Script"
        ],
        "summary": "Generate script from intent",
        "description": "Generate a complete script from a description of the conversational goal. Does not create a project. Returns a draft `blocks` array that can be passed directly as the body to `PUT /v1/communities/{communityId}/projects/{projectId}/script` \u2014 no transformation needed. Typical 3-step workflow: (1) POST /projects/generate with `intent` to get `blocks`; (2) POST /projects to create the project and obtain its `id`; (3) PUT /projects/{id}/script with `{\"blocks\": <blocks from step 1>}`. Alternatively, skip steps 1\u20133 and use POST /projects with `intent` for a single-call workflow. Consumes bot credits. Returns 402 if credit balance is insufficient.",
        "operationId": "generateScript",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "intent"
                ],
                "properties": {
                  "intent": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The goal or purpose of the convo in plain language",
                    "example": "Measure how satisfied members are with last week's event"
                  },
                  "max_blocks": {
                    "type": "integer",
                    "default": 5,
                    "minimum": 1,
                    "maximum": 20
                  },
                  "locale": {
                    "type": "string",
                    "description": "BCP 47 language override (defaults to community locale)",
                    "example": "en-US"
                  }
                }
              },
              "example": {
                "intent": "Measure how satisfied members are with last week's event",
                "max_blocks": 5
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generated blocks array \u2014 pass directly to `PUT /script` with no transformation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "blocks": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Block"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "blocks": [
                      {
                        "id": null,
                        "type": "single_punch",
                        "prompt": "How would you rate last week's event overall?",
                        "options": [
                          {
                            "id": null,
                            "value": "Excellent"
                          },
                          {
                            "id": null,
                            "value": "Good"
                          },
                          {
                            "id": null,
                            "value": "Fair"
                          },
                          {
                            "id": null,
                            "value": "Poor"
                          }
                        ],
                        "required": true,
                        "position": 0,
                        "answer_style": null,
                        "precondition": null
                      },
                      {
                        "id": null,
                        "type": "open_text",
                        "prompt": "What did you enjoy most about the event?",
                        "options": null,
                        "required": false,
                        "position": 1,
                        "answer_style": null,
                        "precondition": null
                      },
                      {
                        "id": null,
                        "type": "open_text",
                        "prompt": "What could we improve for next time?",
                        "options": null,
                        "required": false,
                        "position": 2,
                        "answer_style": null,
                        "precondition": null
                      },
                      {
                        "id": null,
                        "type": "single_punch",
                        "prompt": "Would you attend a similar event in the future?",
                        "options": [
                          {
                            "id": null,
                            "value": "Definitely"
                          },
                          {
                            "id": null,
                            "value": "Probably"
                          },
                          {
                            "id": null,
                            "value": "Probably not"
                          },
                          {
                            "id": null,
                            "value": "Definitely not"
                          }
                        ],
                        "required": true,
                        "position": 3,
                        "answer_style": null,
                        "precondition": null
                      },
                      {
                        "id": null,
                        "type": "open_numeric",
                        "prompt": "On a scale of 1\u201310, how likely are you to recommend our community events to a friend?",
                        "options": null,
                        "min": 1,
                        "max": 10,
                        "required": true,
                        "position": 4,
                        "answer_style": null,
                        "precondition": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "intent is required"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Insufficient bot credits",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payment_required",
                  "message": "Insufficient bot credits"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/responses": {
      "get": {
        "tags": [
          "Responses"
        ],
        "summary": "List responses",
        "description": "List individual participant responses. Paginated. Optional filters narrow the result to one respondent or one exact submission: `user_id` (globally-unique, canonical), `platform_id` + `provider` (the respondent's platform account, which must be paired because `platform_id` is only unique within its namespace), and `session_number`. Combine a respondent filter with `session_number` \u2014 e.g. `?user_id=44821&session_number=1` \u2014 to fetch the single submission behind a `response.submitted` webhook.\n\n**Anonymous projects:** if the project's `privacy_mode` is `anonymous`, `user_id`, `platform_id` and `session_number` are returned as `null` and **all three are rejected as filters with a `400`** \u2014 otherwise the redaction would be cosmetic, since a caller who filters by respondent and reads back a non-empty page has learned who answered. Fetch a single anonymous submission by its `id` via `GET .../responses/{responseId}` instead.",
        "operationId": "listResponses",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          },
          {
            "name": "user_id",
            "in": "query",
            "required": false,
            "description": "Filter to one respondent by globally-unique Subo user id. The unambiguous exact-fetch key. Rejected with a 400 on anonymous projects.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "platform_id",
            "in": "query",
            "required": false,
            "description": "Filter by the respondent's platform account id (e.g. Discord snowflake). Must be paired with `provider`; returns 400 otherwise. Rejected with a 400 on anonymous projects.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "provider",
            "in": "query",
            "required": false,
            "description": "Account namespace for `platform_id`.",
            "schema": {
              "type": "string",
              "enum": [
                "discord",
                "web"
              ]
            }
          },
          {
            "name": "session_number",
            "in": "query",
            "required": false,
            "description": "Filter to a single completion index. Combine with a respondent filter for exactly one submission. Rejected with a 400 on anonymous projects.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of responses",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Response"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "r_001",
                      "project_id": "789",
                      "session_number": 1,
                      "submitted_at": "2026-05-01T14:32:00+00:00",
                      "user_id": "44821",
                      "platform_id": "308994132968210433",
                      "provider": "discord",
                      "answers": [
                        {
                          "block_id": 1,
                          "value": "Excellent",
                          "option_id": 1
                        },
                        {
                          "block_id": 2,
                          "value": "The networking opportunities were fantastic!",
                          "option_id": null
                        },
                        {
                          "block_id": 5,
                          "value": "9",
                          "option_id": null
                        }
                      ]
                    },
                    {
                      "id": "r_002",
                      "project_id": "789",
                      "session_number": 1,
                      "submitted_at": "2026-05-01T15:10:00+00:00",
                      "user_id": "45102",
                      "platform_id": "a1f4c9e2-7b3d-4e51-9c8a-2d6f0b7e1a44",
                      "provider": "youtube",
                      "answers": [
                        {
                          "block_id": 1,
                          "value": "Good",
                          "option_id": 2
                        },
                        {
                          "block_id": 3,
                          "value": "More hands-on workshops would be great.",
                          "option_id": null
                        },
                        {
                          "block_id": 5,
                          "value": "7",
                          "option_id": null
                        }
                      ]
                    }
                  ],
                  "pagination": {
                    "page": 1,
                    "per_page": 20,
                    "total": 2,
                    "has_more": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "Respondent filter used on an anonymous project",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "This project's privacy mode is anonymous, so it cannot be filtered by respondent. Remove: user_id."
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "delete": {
        "tags": [
          "Responses"
        ],
        "summary": "Delete all responses",
        "description": "Delete all responses for this project. Irreversible. Resets counters. Admin role required. **Requires Premium tier or above.** Basic-tier keys receive a `402`.",
        "operationId": "deleteResponses",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted \u2014 all responses and counters reset"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Bulk response deletion requires Premium or above",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Deleting responses requires a Premium, VIP, or Custom plan."
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/responses/{responseId}": {
      "get": {
        "tags": [
          "Responses"
        ],
        "summary": "Get response",
        "description": "Get the complete answer set for one participant response. Works on anonymous projects \u2014 the answers are returned, with `user_id`, `platform_id` and `session_number` as `null`. This is the way to follow a `response.submitted` webhook on an anonymous project, using its `response_id`.",
        "operationId": "getResponse",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "$ref": "#/components/parameters/responseId"
          }
        ],
        "responses": {
          "200": {
            "description": "Response details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Response"
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "r_001",
                    "project_id": "789",
                    "session_number": 1,
                    "submitted_at": "2026-05-01T14:32:00+00:00",
                    "provider": "discord",
                    "answers": [
                      {
                        "block_id": 1,
                        "value": "Excellent",
                        "option_id": 1
                      },
                      {
                        "block_id": 2,
                        "value": "The networking opportunities were fantastic!",
                        "option_id": null
                      },
                      {
                        "block_id": 5,
                        "value": "9",
                        "option_id": null
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "delete": {
        "tags": [
          "Responses"
        ],
        "summary": "Delete response",
        "description": "Permanently delete one participant response session (all answer rows and the completion record). **Requires Premium tier or above.** Basic-tier keys receive a `402`. The project response and completion counters are updated automatically.",
        "operationId": "deleteResponse",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          },
          {
            "$ref": "#/components/parameters/responseId"
          }
        ],
        "responses": {
          "200": {
            "description": "Response deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "project_id": {
                          "type": "string"
                        },
                        "response_id": {
                          "type": "string"
                        },
                        "deleted": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Individual response deletion requires Premium or above",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Deleting individual responses requires a Premium, VIP, or Custom plan."
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/projects/{projectId}/analysis": {
      "get": {
        "tags": [
          "Analysis"
        ],
        "summary": "Get analysis",
        "description": "Get current analysis: choice distributions, numeric stats, and AI summaries for open_text blocks.",
        "operationId": "getAnalysis",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "responses": {
          "200": {
            "description": "Analysis result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Analysis"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "post": {
        "tags": [
          "Analysis"
        ],
        "summary": "Trigger AI analysis",
        "description": "Trigger AI summarization for open_text blocks. Consumes bot credits. Returns immediately; poll GET /analysis for results. Returns 402 if credit balance is zero.",
        "operationId": "triggerAnalysis",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/projectId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean",
                    "default": false,
                    "description": "Regenerate all summaries even if they already exist"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Processing started",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "project_id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "processing",
                            "no_eligible_blocks"
                          ],
                          "description": "`processing`: AI jobs queued; `no_eligible_blocks`: no open_text blocks found"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Bot credit balance is zero",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "payment_required",
                  "message": "Bot credit balance is zero"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creators can only access their own projects"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/members": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "List members",
        "description": "List community members with access level and XP. Supports name search and platform_id lookup.",
        "operationId": "listMembers",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "platform_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Lookup by Discord snowflake"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Member"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/members/{memberId}": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "Get member",
        "operationId": "getMember",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/memberId"
          }
        ],
        "responses": {
          "200": {
            "description": "Member details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Member"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/members/{memberId}/xp/history": {
      "get": {
        "tags": [
          "Members"
        ],
        "summary": "List XP history",
        "description": "Append-only XP audit trail for a member: every survey completion, action block, poll vote, admin award, recalculation, and reset. Newest first, paginated. Admin role required.",
        "operationId": "listXpHistory",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/memberId"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated XP ledger entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/XpLedgerEntry"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/members/{memberId}/xp": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Modify XP",
        "description": "Add, subtract, or set a member's XP balance. Admin role required.",
        "operationId": "modifyXp",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/memberId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "operation",
                  "amount"
                ],
                "properties": {
                  "operation": {
                    "type": "string",
                    "enum": [
                      "add",
                      "subtract",
                      "set"
                    ],
                    "description": "add \u2014 increment; subtract \u2014 decrement (floor 0); set \u2014 absolute value"
                  },
                  "amount": {
                    "type": "integer",
                    "minimum": 0
                  }
                }
              },
              "example": {
                "operation": "add",
                "amount": 50
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated member",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Member"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "invalid_operation": {
                    "summary": "Unrecognised operation",
                    "value": {
                      "error": "invalid_request",
                      "message": "operation must be add, subtract, or set"
                    }
                  },
                  "invalid_amount": {
                    "summary": "Negative amount",
                    "value": {
                      "error": "invalid_request",
                      "message": "amount must be non-negative"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/members/{memberId}/access": {
      "put": {
        "tags": [
          "Members"
        ],
        "summary": "Update access level",
        "description": "Set access level to member, creator, or admin. Cannot demote the community owner. Admin role required.",
        "operationId": "updateAccess",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/memberId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "access"
                ],
                "properties": {
                  "access": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "creator",
                      "member"
                    ],
                    "description": "admin \u2014 full control; creator \u2014 create/manage projects; member \u2014 participate only"
                  }
                }
              },
              "example": {
                "access": "creator"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated member",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Member"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid access level",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "access must be admin, creator, or member"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "owner": {
                    "summary": "Cannot demote community owner",
                    "value": {
                      "error": "forbidden",
                      "message": "Cannot change access level of community owner"
                    }
                  },
                  "admin_required": {
                    "summary": "Admin role required",
                    "value": {
                      "error": "forbidden",
                      "message": "Admin role required"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/members/{memberId}/accomplishments": {
      "post": {
        "tags": [
          "Members"
        ],
        "summary": "Modify accomplishments",
        "description": "Award or revoke an achievement (XP role) for a community member. Admin role required.",
        "operationId": "modifyAccomplishments",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/memberId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "operation",
                  "achievement_id"
                ],
                "properties": {
                  "operation": {
                    "type": "string",
                    "enum": [
                      "award",
                      "revoke"
                    ],
                    "description": "award \u2014 grant the achievement; revoke \u2014 remove it"
                  },
                  "achievement_id": {
                    "type": "integer",
                    "description": "ID of the XP role / achievement to award or revoke"
                  }
                }
              },
              "example": {
                "operation": "award",
                "achievement_id": 123
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accomplishment updated"
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "operation must be award or revoke"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "description": "Member or achievement not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "member": {
                    "summary": "Member not found",
                    "value": {
                      "error": "not_found",
                      "message": "Resource not found"
                    }
                  },
                  "achievement": {
                    "summary": "Achievement not found",
                    "value": {
                      "error": "not_found",
                      "message": "Achievement not found in this community"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/themes": {
      "get": {
        "tags": [
          "Themes & Interviewers"
        ],
        "summary": "List themes",
        "description": "List available visual themes. Premium-only themes are filtered by community tier.",
        "operationId": "listThemes",
        "responses": {
          "200": {
            "description": "List of themes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Theme"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/interviewers": {
      "get": {
        "tags": [
          "Themes & Interviewers"
        ],
        "summary": "List interviewers",
        "description": "List available AI interviewer personas. Premium-only personas are filtered by community tier.",
        "operationId": "listInterviewers",
        "responses": {
          "200": {
            "description": "List of interviewer personas",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Interviewer"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/templates": {
      "get": {
        "tags": [
          "Templates"
        ],
        "summary": "List templates",
        "description": "List curated, cloneable templates across communities. Filter by category, tag (repeatable), search, or type.",
        "operationId": "listTemplates",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Exact catalog category filter"
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Repeatable tag filter (OR within the facet)"
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive substring over name/category/tags"
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "poll",
                "convo"
              ]
            },
            "description": "Filter by project type"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Viewer locale (e.g. 'fr', 'de', 'es-ES', 'pt-BR'); localizes each template's name/description, with English as a per-field fallback. Omit for English."
          }
        ],
        "responses": {
          "200": {
            "description": "List of templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/templates/{templateId}": {
      "get": {
        "tags": [
          "Templates"
        ],
        "summary": "Get template",
        "description": "Get one curated template including clonable properties, a derived feature fingerprint, and its full machine-readable script (blocks) in the same shape as GET/PUT /script.",
        "operationId": "getTemplate",
        "parameters": [
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template ID (internal survey ID)"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Viewer locale (e.g. 'fr'); localizes the template's name/description, English per-field fallback. Omit for English."
          }
        ],
        "responses": {
          "200": {
            "description": "Template detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TemplateDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/templates/{templateId}/clone": {
      "post": {
        "tags": [
          "Templates"
        ],
        "summary": "Clone template",
        "description": "Instantiate a curated template as a new **inactive** project in your community. Deep clone: script, scoring buckets, grading, action blocks, calculated fields and outro carry over; source-server bindings (channels, roles, XP economy, theme, schedule) are stripped to a clean slate. Requires CREATOR or admin in the community. Send an `Idempotency-Key` (UUID v4) to make retries safe \u2014 a duplicate key replays the same clone instead of creating a second project. Open the result with `POST /projects/{id}/open`.",
        "operationId": "cloneTemplate",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "name": "templateId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Template ID (internal survey ID)"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Defaults to the template name + ' (copy)'"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Cloned project (inactive)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Project"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "forbidden",
                  "message": "Creator role or higher required"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List webhooks",
        "description": "List webhook registrations. Signing secrets are never returned. Admin role required.",
        "operationId": "listWebhooks",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "responses": {
          "200": {
            "description": "List of webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Create webhook",
        "description": "Register a webhook. Returns the signing secret once \u2014 store it immediately, it cannot be retrieved again. Admin role required. Subscribing to `response.submitted` **requires Premium tier or above** \u2014 Basic-tier keys receive a `402`.",
        "operationId": "createWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "HTTPS endpoint that will receive POST requests",
                    "example": "https://example.com/webhooks/subo"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100,
                    "description": "Human-readable label for this webhook"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "project.created",
                        "project.updated",
                        "project.opened",
                        "project.closed",
                        "project.status_changed",
                        "project.deleted",
                        "response.submitted",
                        "analysis.completed"
                      ]
                    },
                    "description": "Event types to subscribe to"
                  }
                }
              },
              "example": {
                "url": "https://example.com/webhooks/subo",
                "name": "Project lifecycle events",
                "events": [
                  "project.opened",
                  "project.closed",
                  "response.submitted"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created webhook with signing_secret (shown once \u2014 store immediately)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/WebhookCreated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown events: [\"invalid.event\"]"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Subscribing to `response.submitted` requires Premium or above",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "tier_required",
                  "required_tier": "premium"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/webhooks/{webhookId}": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Get webhook",
        "operationId": "getWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "put": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Update webhook",
        "description": "Update URL, name, events, or active status. Does not rotate the signing secret. Subscribing to `response.submitted` **requires Premium tier or above** \u2014 Basic-tier keys receive a `402`.",
        "operationId": "updateWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "All fields optional. Only provided fields are updated.",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 100
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "enum": [
                        "project.created",
                        "project.updated",
                        "project.opened",
                        "project.closed",
                        "project.status_changed",
                        "project.deleted",
                        "response.submitted",
                        "analysis.completed"
                      ]
                    }
                  },
                  "is_active": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "is_active": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated webhook",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "Unknown events: [\"invalid.event\"]"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "402": {
            "description": "Subscribing to `response.submitted` requires Premium or above",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "tier_required",
                  "required_tier": "premium"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete webhook",
        "description": "Delete registration and cancel pending deliveries.",
        "operationId": "deleteWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/webhooks/{webhookId}/test": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Test webhook",
        "description": "Send a ping event to verify connectivity and signature verification.",
        "operationId": "testWebhook",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "Ping queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "queued": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "409": {
            "description": "Webhook is not active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "conflict",
                  "message": "Webhook is not active"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/webhooks/{webhookId}/rotate-secret": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Rotate signing secret",
        "description": "Issue a new signing secret for this webhook. The old secret stops working immediately \u2014 update your endpoint to verify with the new secret before rotating.\n\nReturns the new secret once. It cannot be retrieved again.",
        "operationId": "rotateWebhookSecret",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/webhookId"
          }
        ],
        "responses": {
          "200": {
            "description": "New signing secret (shown once)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "webhook_id": {
                          "type": "integer"
                        },
                        "secret_prefix": {
                          "type": "string"
                        },
                        "signing_secret": {
                          "type": "string",
                          "description": "Full signing secret \u2014 store immediately, not recoverable",
                          "example": "sbo_whsec_..."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/webhooks/{webhookId}/deliveries": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List delivery history",
        "description": "List delivery attempts for a webhook, ordered newest first. Use this to debug failed deliveries \u2014 each record shows the HTTP status returned by your endpoint, the response body, attempt count, and next retry time.\n\nDelivery lifecycle: `pending` \u2192 `delivered` on 2xx, or `pending` (retried) \u2192 `failed` \u2192 eventually `abandoned` after 5 attempts.",
        "operationId": "listWebhookDeliveries",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/webhookId"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "delivered",
                "failed",
                "abandoned"
              ]
            },
            "description": "Filter by delivery status"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "per_page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of delivery attempts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/api-keys": {
      "get": {
        "tags": [
          "API Keys"
        ],
        "summary": "List API keys",
        "description": "List API keys for this community. Key values are never returned. Admin role required.",
        "operationId": "listApiKeys",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "responses": {
          "200": {
            "description": "List of API keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      },
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Create API key",
        "description": "Create a new API key. Returns the full key value once \u2014 store it immediately. Admin role required.",
        "operationId": "createApiKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 100
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "expires_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO 8601 expiry timestamp. Omit for non-expiring key.",
                    "example": "2027-01-01T00:00:00Z"
                  }
                }
              },
              "example": {
                "name": "Production integration",
                "description": "Used by the data pipeline"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created key with api_key value (shown once \u2014 store immediately)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ApiKeyCreated"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "invalid_request",
                  "message": "expires_at must be ISO 8601 format"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/api-keys/{keyId}": {
      "delete": {
        "tags": [
          "API Keys"
        ],
        "summary": "Revoke API key",
        "description": "Revoke a key immediately. In-flight requests using this key will be rejected. Admin role required.",
        "operationId": "deleteApiKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/keyId"
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "$ref": "#/components/responses/Error404"
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    },
    "/v1/communities/{communityId}/api-keys/{keyId}/regenerate": {
      "post": {
        "tags": [
          "API Keys"
        ],
        "summary": "Regenerate API key",
        "description": "Issue a new key value for an existing API key. The old value is immediately invalidated \u2014 all in-flight requests using it will be rejected. The key's ID, name, community binding, and access level are preserved.\n\nReturns the new full key value once \u2014 store it immediately. Admin role required.",
        "operationId": "regenerateApiKey",
        "parameters": [
          {
            "$ref": "#/components/parameters/communityId"
          },
          {
            "$ref": "#/components/parameters/keyId"
          }
        ],
        "responses": {
          "200": {
            "description": "New key value (shown once \u2014 store immediately)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "key_id": {
                          "type": "string"
                        },
                        "key_prefix": {
                          "type": "string"
                        },
                        "api_key": {
                          "type": "string",
                          "description": "Full new API key value \u2014 store immediately, not recoverable",
                          "example": "sbo_live_..."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error401"
          },
          "403": {
            "$ref": "#/components/responses/Error403"
          },
          "404": {
            "description": "Key not found or not active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "not_found",
                  "message": "Resource not found"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/Error429"
          }
        }
      }
    }
  },
  "webhooks": {
    "project.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Project created",
        "description": "Fires when a new project is created. Payload contains the full project object.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadProject"
              },
              "example": {
                "event": "project.created",
                "community_id": "1152727606572093543",
                "project": {
                  "id": "789",
                  "name": "Q2 Community Survey",
                  "status": "inactive",
                  "info": {
                    "type": "convo",
                    "created_at": "2026-05-01T12:00:00+00:00",
                    "block_count": 3
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "project.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Project updated",
        "description": "Fires when project metadata or settings are updated. Payload contains the full updated project object.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadProject"
              },
              "example": {
                "event": "project.updated",
                "community_id": "1152727606572093543",
                "project": {
                  "id": "789",
                  "name": "Q2 Community Survey (updated)",
                  "status": "inactive",
                  "info": {
                    "type": "convo",
                    "created_at": "2026-05-01T12:00:00+00:00",
                    "block_count": 5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "project.opened": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Project opened",
        "description": "Fires when a project transitions to `active` (accepting responses). Prefer this over `project.status_changed` when you only care about the open direction.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadProjectRef"
              },
              "example": {
                "event": "project.opened",
                "community_id": "1152727606572093543",
                "project_id": "789"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "project.closed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Project closed",
        "description": "Fires when a project transitions to `inactive`. In-progress participant sessions are allowed to complete after this event fires.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadProjectRef"
              },
              "example": {
                "event": "project.closed",
                "community_id": "1152727606572093543",
                "project_id": "789"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "project.status_changed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Project status changed",
        "description": "Fires on both open and close transitions. The `status` field indicates direction. Kept for backward compatibility \u2014 prefer `project.opened` / `project.closed` for new integrations.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadStatusChanged"
              },
              "examples": {
                "opened": {
                  "summary": "Project activated",
                  "value": {
                    "event": "project.status_changed",
                    "community_id": "1152727606572093543",
                    "project_id": "789",
                    "status": "active"
                  }
                },
                "closed": {
                  "summary": "Project deactivated",
                  "value": {
                    "event": "project.status_changed",
                    "community_id": "1152727606572093543",
                    "project_id": "789",
                    "status": "inactive"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "project.deleted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Project deleted",
        "description": "Fires when a project is permanently deleted. No further events for this project ID will be delivered.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadProjectRef"
              },
              "example": {
                "event": "project.deleted",
                "community_id": "1152727606572093543",
                "project_id": "789"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "response.submitted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Response submitted",
        "description": "Fires when a real (non-test) participant completes a project. **Requires Premium tier or above** to subscribe. `session_number` distinguishes repeated completions when `max_completes_per_user > 1`. `user_id` + `platform_id` identify the respondent; call `GET /responses?user_id={user_id}&session_number={session_number}` after receiving this event to retrieve the full answer set for the exact submission.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadResponseSubmitted"
              },
              "example": {
                "event": "response.submitted",
                "community_id": "1152727606572093543",
                "project_id": "789",
                "session_number": 1,
                "user_id": "44821",
                "platform_id": "308994132968210433",
                "provider": "discord",
                "completed_at": "2026-05-01T14:32:00+00:00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    },
    "analysis.completed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "AI analysis completed",
        "description": "Fires when an AI summarization job finishes for a project. `blocks_updated` is the number of `open_text` blocks whose summary was successfully written. Call `GET /analysis` after receiving this event to retrieve the updated summaries.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayloadAnalysisCompleted"
              },
              "example": {
                "event": "analysis.completed",
                "community_id": "1152727606572093543",
                "project_id": "789",
                "blocks_updated": 2,
                "completed_at": "2026-05-01T15:00:00+00:00"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook processed successfully"
          }
        }
      }
    }
  }
}