> For the complete documentation index, see [llms.txt](https://movemint.gitbook.io/movemint-developer-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://movemint.gitbook.io/movemint-developer-docs/open-api-specification/core-resources/waves.md).

# Waves

Manage waves (start corrals / heats), assign and unassign participants, run bulk auto-assignment, and configure wave-number settings. A wave with no linked sub-events applies to every sub-event ("shared"); linking sub-events restricts it to those. Waves can be capped, gender-restricted, and gated on a custom question's answers — a TIME gate (bounds on a time-based question's answer) and an ANSWER gate (an ordered set of a multiple-choice question's options). The two gates are independent and AND: a wave may carry neither, either or both, and a participant must clear every gate that is configured. Read `GET /api/v1/events/{handle}/wave_gate_questions` for the questions available to gate on and the live count behind each answer option before sizing waves.

## GET /api/v1/events/{handle}/waves

> List waves

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"Wave":{"type":"object","required":["id","name","position","public","start_time","max_participants","gender_identity","gender_name","assigned_count","remaining_capacity","event_variant_ids","transactable_custom_question_id","min_time_seconds","max_time_seconds","answer_transactable_custom_question_id","question_multiple_choice_option_ids","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"position":{"type":"integer"},"public":{"type":"boolean","description":"Public waves are the ones bulk/auto-assignment fills."},"start_time":{"type":"string","format":"date-time","nullable":true},"max_participants":{"type":"integer","nullable":true,"description":"Capacity cap; null means unlimited."},"gender_identity":{"type":"integer","nullable":true,"description":"Gender restriction (0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say). Null = no restriction."},"gender_name":{"type":"string","nullable":true},"assigned_count":{"type":"integer"},"remaining_capacity":{"type":"integer","nullable":true},"event_variant_ids":{"type":"array","description":"Linked sub-event ids. Empty means the wave applies to every sub-event (shared).","items":{"type":"integer","format":"int64"}},"transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the time-based question whose answer gates eligibility, when set — not the underlying Question id. Independent of answer_transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"min_time_seconds":{"type":"integer","nullable":true},"max_time_seconds":{"type":"integer","nullable":true},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the multiple-choice question whose selected answers gate eligibility, when set — not the underlying Question id, and specific to the sub-event the question is attached to. Independent of transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"question_multiple_choice_option_ids":{"type":"array","description":"QuestionMultipleChoiceOption ids this wave admits, all belonging to answer_transactable_custom_question_id's question. THE ORDER IS THE WAVE'S FILL ORDER — the wave takes everyone matching its first option, then its second, and so on until capacity — and it is this wave's own order, independent of the question's option order. An option may appear on several waves deliberately: a participant lands in the first wave, by position, that admits them and has room, so a repeated option spills an oversized answer group across waves.","items":{"type":"integer","format":"int64"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/waves":{"get":{"tags":["Waves"],"summary":"List waves","operationId":"listWaves","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"responses":{"200":{"description":"Waves ordered by position","content":{"application/json":{"schema":{"type":"object","required":["waves"],"properties":{"waves":{"type":"array","items":{"$ref":"#/components/schemas/Wave"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## Create a wave

> Omit \`event\_variant\_ids\` (or send \`\[]\`) for a wave shared across every\
> sub-event; send ids to restrict it to those. Only \`public\` waves are\
> filled by bulk auto-assignment.\
> \
> A wave can carry a TIME gate (\`transactable\_custom\_question\_id\` with\
> \`min\_time\_seconds\` and/or \`max\_time\_seconds\`) and an ANSWER gate\
> (\`answer\_transactable\_custom\_question\_id\` with\
> \`question\_multiple\_choice\_option\_ids\`). The two are independent and AND\
> together: a wave may carry neither, either or both, and a participant\
> must clear every gate that is configured.\
> \
> Each gate's two fields are joint. A question id with no bounds, or with\
> no option ids, leaves that gate unconfigured — send both halves\
> together. For the answer gate the id array's ORDER IS THE WAVE'S FILL\
> ORDER: the wave takes everyone matching its first option, then its\
> second, and so on until capacity. That order is this wave's own and is\
> unrelated to the question's option order.\
> \
> Both gate id fields take a \`TransactableCustomQuestion\` id — the\
> question as attached to this event or one of its sub-events — never the\
> underlying \`Question\` id. Read\
> \`GET /api/v1/events/{handle}/wave\_gate\_questions\` for those ids, the\
> option ids, and the live count behind each option, which is what sizes\
> the wave you are about to create.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"CreateWaveRequest":{"allOf":[{"$ref":"#/components/schemas/WaveWritableFields"},{"type":"object","required":["name"],"properties":{"name":{"type":"string"}}}]},"WaveWritableFields":{"type":"object","properties":{"public":{"type":"boolean"},"start_time":{"type":"string","format":"date-time"},"max_participants":{"type":"integer","minimum":1},"gender_identity":{"type":"integer","enum":[0,1,2,3],"description":"0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say."},"position":{"type":"integer"},"transactable_custom_question_id":{"type":"integer","format":"int64","description":"TransactableCustomQuestion id of the time-based question to gate on — the question as attached to this event or one of its sub-events, NOT the underlying Question id, and not interchangeable with it. Get it from `GET /api/v1/events/{handle}/wave_gate_questions`, whose rows carry it as `id` (with the Question id alongside as `question_id`, which no wave endpoint accepts). One question attached to several sub-events is several rows sharing a `question_id`; pick the row for the sub-event you mean, or the gate reads a different sub-event's answers while looking correct. Required if you set min/max time bounds. Clearing it clears the bounds."},"min_time_seconds":{"type":"integer","minimum":0},"max_time_seconds":{"type":"integer","minimum":0},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","description":"TransactableCustomQuestion id of the multiple-choice question to gate on — same id space as `transactable_custom_question_id` above, and the same warnings apply: not the Question id, and specific to the sub-event the question is attached to. Required alongside `question_multiple_choice_option_ids`; sending this alone leaves the gate unconfigured, and clearing it clears the mapped options."},"question_multiple_choice_option_ids":{"type":"array","description":"Option ids to admit, in the order this wave should fill from them — the array order is significant and is independent of the question's own option order. Replace-not-merge: the ids sent become the wave's entire mapping. Ids outside answer_transactable_custom_question_id's question are ignored. Listing an option that another wave also lists is legitimate — a participant lands in the first wave, by position, that admits them and has room.","items":{"type":"integer","format":"int64"}},"event_variant_ids":{"type":"array","description":"Sub-event ids to link. Replace-not-merge. Pass [] for a shared wave.","items":{"type":"integer","format":"int64"}}}},"Wave":{"type":"object","required":["id","name","position","public","start_time","max_participants","gender_identity","gender_name","assigned_count","remaining_capacity","event_variant_ids","transactable_custom_question_id","min_time_seconds","max_time_seconds","answer_transactable_custom_question_id","question_multiple_choice_option_ids","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"position":{"type":"integer"},"public":{"type":"boolean","description":"Public waves are the ones bulk/auto-assignment fills."},"start_time":{"type":"string","format":"date-time","nullable":true},"max_participants":{"type":"integer","nullable":true,"description":"Capacity cap; null means unlimited."},"gender_identity":{"type":"integer","nullable":true,"description":"Gender restriction (0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say). Null = no restriction."},"gender_name":{"type":"string","nullable":true},"assigned_count":{"type":"integer"},"remaining_capacity":{"type":"integer","nullable":true},"event_variant_ids":{"type":"array","description":"Linked sub-event ids. Empty means the wave applies to every sub-event (shared).","items":{"type":"integer","format":"int64"}},"transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the time-based question whose answer gates eligibility, when set — not the underlying Question id. Independent of answer_transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"min_time_seconds":{"type":"integer","nullable":true},"max_time_seconds":{"type":"integer","nullable":true},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the multiple-choice question whose selected answers gate eligibility, when set — not the underlying Question id, and specific to the sub-event the question is attached to. Independent of transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"question_multiple_choice_option_ids":{"type":"array","description":"QuestionMultipleChoiceOption ids this wave admits, all belonging to answer_transactable_custom_question_id's question. THE ORDER IS THE WAVE'S FILL ORDER — the wave takes everyone matching its first option, then its second, and so on until capacity — and it is this wave's own order, independent of the question's option order. An option may appear on several waves deliberately: a participant lands in the first wave, by position, that admits them and has room, so a repeated option spills an oversized answer group across waves.","items":{"type":"integer","format":"int64"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}},"ValidationError":{"type":"object","description":"A validation error response with a list of error messages","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"string"},"description":"List of validation error messages"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/waves":{"post":{"tags":["Waves"],"summary":"Create a wave","operationId":"createWave","description":"Omit `event_variant_ids` (or send `[]`) for a wave shared across every\nsub-event; send ids to restrict it to those. Only `public` waves are\nfilled by bulk auto-assignment.\n\nA wave can carry a TIME gate (`transactable_custom_question_id` with\n`min_time_seconds` and/or `max_time_seconds`) and an ANSWER gate\n(`answer_transactable_custom_question_id` with\n`question_multiple_choice_option_ids`). The two are independent and AND\ntogether: a wave may carry neither, either or both, and a participant\nmust clear every gate that is configured.\n\nEach gate's two fields are joint. A question id with no bounds, or with\nno option ids, leaves that gate unconfigured — send both halves\ntogether. For the answer gate the id array's ORDER IS THE WAVE'S FILL\nORDER: the wave takes everyone matching its first option, then its\nsecond, and so on until capacity. That order is this wave's own and is\nunrelated to the question's option order.\n\nBoth gate id fields take a `TransactableCustomQuestion` id — the\nquestion as attached to this event or one of its sub-events — never the\nunderlying `Question` id. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for those ids, the\noption ids, and the live count behind each option, which is what sizes\nthe wave you are about to create.\n","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWaveRequest"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["wave"],"properties":{"wave":{"$ref":"#/components/schemas/Wave"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}}}}
````

## GET /api/v1/events/{handle}/waves/{id}

> Get a wave

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}},"ResourceId":{"name":"id","in":"path","required":true,"description":"The ID of the resource","schema":{"type":"integer","format":"int64"}}},"schemas":{"Wave":{"type":"object","required":["id","name","position","public","start_time","max_participants","gender_identity","gender_name","assigned_count","remaining_capacity","event_variant_ids","transactable_custom_question_id","min_time_seconds","max_time_seconds","answer_transactable_custom_question_id","question_multiple_choice_option_ids","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"position":{"type":"integer"},"public":{"type":"boolean","description":"Public waves are the ones bulk/auto-assignment fills."},"start_time":{"type":"string","format":"date-time","nullable":true},"max_participants":{"type":"integer","nullable":true,"description":"Capacity cap; null means unlimited."},"gender_identity":{"type":"integer","nullable":true,"description":"Gender restriction (0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say). Null = no restriction."},"gender_name":{"type":"string","nullable":true},"assigned_count":{"type":"integer"},"remaining_capacity":{"type":"integer","nullable":true},"event_variant_ids":{"type":"array","description":"Linked sub-event ids. Empty means the wave applies to every sub-event (shared).","items":{"type":"integer","format":"int64"}},"transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the time-based question whose answer gates eligibility, when set — not the underlying Question id. Independent of answer_transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"min_time_seconds":{"type":"integer","nullable":true},"max_time_seconds":{"type":"integer","nullable":true},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the multiple-choice question whose selected answers gate eligibility, when set — not the underlying Question id, and specific to the sub-event the question is attached to. Independent of transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"question_multiple_choice_option_ids":{"type":"array","description":"QuestionMultipleChoiceOption ids this wave admits, all belonging to answer_transactable_custom_question_id's question. THE ORDER IS THE WAVE'S FILL ORDER — the wave takes everyone matching its first option, then its second, and so on until capacity — and it is this wave's own order, independent of the question's option order. An option may appear on several waves deliberately: a participant lands in the first wave, by position, that admits them and has room, so a repeated option spills an oversized answer group across waves.","items":{"type":"integer","format":"int64"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/waves/{id}":{"get":{"tags":["Waves"],"summary":"Get a wave","operationId":"getWave","parameters":[{"$ref":"#/components/parameters/EventHandle"},{"$ref":"#/components/parameters/ResourceId"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["wave"],"properties":{"wave":{"$ref":"#/components/schemas/Wave"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## DELETE /api/v1/events/{handle}/waves/{id}

> Delete a wave

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}},"ResourceId":{"name":"id","in":"path","required":true,"description":"The ID of the resource","schema":{"type":"integer","format":"int64"}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}}},"paths":{"/api/v1/events/{handle}/waves/{id}":{"delete":{"tags":["Waves"],"summary":"Delete a wave","operationId":"deleteWave","parameters":[{"$ref":"#/components/parameters/EventHandle"},{"$ref":"#/components/parameters/ResourceId"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## Update a wave

> Only the fields you send are changed. Sending \`event\_variant\_ids\`\
> REPLACES the full set of linked sub-events (pass \`\[]\` to make the wave\
> shared). Clearing \`transactable\_custom\_question\_id\` also clears the time\
> bounds.\
> \
> The ANSWER gate's two fields are joint: send\
> \`answer\_transactable\_custom\_question\_id\` and\
> \`question\_multiple\_choice\_option\_ids\` together. Sending the question id\
> alone — to set it OR to clear it — drops the wave's existing option\
> mapping, because an option only means anything relative to the question\
> it belongs to. The option ids REPLACE the mapping in the order sent, and\
> that order is the wave's fill order.\
> \
> The TIME gate and the ANSWER gate are independent and AND together: a\
> wave may carry neither, either or both, and a participant must clear\
> every gate that is configured. Both id fields take a\
> \`TransactableCustomQuestion\` id (the question as attached to this event\
> or one of its sub-events), never the underlying \`Question\` id — read\
> \`GET /api/v1/events/{handle}/wave\_gate\_questions\` for those ids, the\
> option ids, and the live count behind each option.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}},"ResourceId":{"name":"id","in":"path","required":true,"description":"The ID of the resource","schema":{"type":"integer","format":"int64"}}},"schemas":{"UpdateWaveRequest":{"allOf":[{"$ref":"#/components/schemas/WaveWritableFields"},{"type":"object","properties":{"name":{"type":"string"}}}]},"WaveWritableFields":{"type":"object","properties":{"public":{"type":"boolean"},"start_time":{"type":"string","format":"date-time"},"max_participants":{"type":"integer","minimum":1},"gender_identity":{"type":"integer","enum":[0,1,2,3],"description":"0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say."},"position":{"type":"integer"},"transactable_custom_question_id":{"type":"integer","format":"int64","description":"TransactableCustomQuestion id of the time-based question to gate on — the question as attached to this event or one of its sub-events, NOT the underlying Question id, and not interchangeable with it. Get it from `GET /api/v1/events/{handle}/wave_gate_questions`, whose rows carry it as `id` (with the Question id alongside as `question_id`, which no wave endpoint accepts). One question attached to several sub-events is several rows sharing a `question_id`; pick the row for the sub-event you mean, or the gate reads a different sub-event's answers while looking correct. Required if you set min/max time bounds. Clearing it clears the bounds."},"min_time_seconds":{"type":"integer","minimum":0},"max_time_seconds":{"type":"integer","minimum":0},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","description":"TransactableCustomQuestion id of the multiple-choice question to gate on — same id space as `transactable_custom_question_id` above, and the same warnings apply: not the Question id, and specific to the sub-event the question is attached to. Required alongside `question_multiple_choice_option_ids`; sending this alone leaves the gate unconfigured, and clearing it clears the mapped options."},"question_multiple_choice_option_ids":{"type":"array","description":"Option ids to admit, in the order this wave should fill from them — the array order is significant and is independent of the question's own option order. Replace-not-merge: the ids sent become the wave's entire mapping. Ids outside answer_transactable_custom_question_id's question are ignored. Listing an option that another wave also lists is legitimate — a participant lands in the first wave, by position, that admits them and has room.","items":{"type":"integer","format":"int64"}},"event_variant_ids":{"type":"array","description":"Sub-event ids to link. Replace-not-merge. Pass [] for a shared wave.","items":{"type":"integer","format":"int64"}}}},"Wave":{"type":"object","required":["id","name","position","public","start_time","max_participants","gender_identity","gender_name","assigned_count","remaining_capacity","event_variant_ids","transactable_custom_question_id","min_time_seconds","max_time_seconds","answer_transactable_custom_question_id","question_multiple_choice_option_ids","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"position":{"type":"integer"},"public":{"type":"boolean","description":"Public waves are the ones bulk/auto-assignment fills."},"start_time":{"type":"string","format":"date-time","nullable":true},"max_participants":{"type":"integer","nullable":true,"description":"Capacity cap; null means unlimited."},"gender_identity":{"type":"integer","nullable":true,"description":"Gender restriction (0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say). Null = no restriction."},"gender_name":{"type":"string","nullable":true},"assigned_count":{"type":"integer"},"remaining_capacity":{"type":"integer","nullable":true},"event_variant_ids":{"type":"array","description":"Linked sub-event ids. Empty means the wave applies to every sub-event (shared).","items":{"type":"integer","format":"int64"}},"transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the time-based question whose answer gates eligibility, when set — not the underlying Question id. Independent of answer_transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"min_time_seconds":{"type":"integer","nullable":true},"max_time_seconds":{"type":"integer","nullable":true},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the multiple-choice question whose selected answers gate eligibility, when set — not the underlying Question id, and specific to the sub-event the question is attached to. Independent of transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"question_multiple_choice_option_ids":{"type":"array","description":"QuestionMultipleChoiceOption ids this wave admits, all belonging to answer_transactable_custom_question_id's question. THE ORDER IS THE WAVE'S FILL ORDER — the wave takes everyone matching its first option, then its second, and so on until capacity — and it is this wave's own order, independent of the question's option order. An option may appear on several waves deliberately: a participant lands in the first wave, by position, that admits them and has room, so a repeated option spills an oversized answer group across waves.","items":{"type":"integer","format":"int64"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}},"ValidationError":{"type":"object","description":"A validation error response with a list of error messages","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"string"},"description":"List of validation error messages"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/waves/{id}":{"patch":{"tags":["Waves"],"summary":"Update a wave","operationId":"updateWave","description":"Only the fields you send are changed. Sending `event_variant_ids`\nREPLACES the full set of linked sub-events (pass `[]` to make the wave\nshared). Clearing `transactable_custom_question_id` also clears the time\nbounds.\n\nThe ANSWER gate's two fields are joint: send\n`answer_transactable_custom_question_id` and\n`question_multiple_choice_option_ids` together. Sending the question id\nalone — to set it OR to clear it — drops the wave's existing option\nmapping, because an option only means anything relative to the question\nit belongs to. The option ids REPLACE the mapping in the order sent, and\nthat order is the wave's fill order.\n\nThe TIME gate and the ANSWER gate are independent and AND together: a\nwave may carry neither, either or both, and a participant must clear\nevery gate that is configured. Both id fields take a\n`TransactableCustomQuestion` id (the question as attached to this event\nor one of its sub-events), never the underlying `Question` id — read\n`GET /api/v1/events/{handle}/wave_gate_questions` for those ids, the\noption ids, and the live count behind each option.\n","parameters":[{"$ref":"#/components/parameters/EventHandle"},{"$ref":"#/components/parameters/ResourceId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWaveRequest"}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","required":["wave"],"properties":{"wave":{"$ref":"#/components/schemas/Wave"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}}}}
````

## Reorder waves

> Sets wave positions from the order of the given ids. Foreign ids are ignored.

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"Wave":{"type":"object","required":["id","name","position","public","start_time","max_participants","gender_identity","gender_name","assigned_count","remaining_capacity","event_variant_ids","transactable_custom_question_id","min_time_seconds","max_time_seconds","answer_transactable_custom_question_id","question_multiple_choice_option_ids","created_at","updated_at"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"position":{"type":"integer"},"public":{"type":"boolean","description":"Public waves are the ones bulk/auto-assignment fills."},"start_time":{"type":"string","format":"date-time","nullable":true},"max_participants":{"type":"integer","nullable":true,"description":"Capacity cap; null means unlimited."},"gender_identity":{"type":"integer","nullable":true,"description":"Gender restriction (0=Male, 1=Female, 2=Nonbinary, 3=Prefer Not to Say). Null = no restriction."},"gender_name":{"type":"string","nullable":true},"assigned_count":{"type":"integer"},"remaining_capacity":{"type":"integer","nullable":true},"event_variant_ids":{"type":"array","description":"Linked sub-event ids. Empty means the wave applies to every sub-event (shared).","items":{"type":"integer","format":"int64"}},"transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the time-based question whose answer gates eligibility, when set — not the underlying Question id. Independent of answer_transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"min_time_seconds":{"type":"integer","nullable":true},"max_time_seconds":{"type":"integer","nullable":true},"answer_transactable_custom_question_id":{"type":"integer","format":"int64","nullable":true,"description":"TransactableCustomQuestion id of the multiple-choice question whose selected answers gate eligibility, when set — not the underlying Question id, and specific to the sub-event the question is attached to. Independent of transactable_custom_question_id: a wave carrying both gates admits only participants who clear both."},"question_multiple_choice_option_ids":{"type":"array","description":"QuestionMultipleChoiceOption ids this wave admits, all belonging to answer_transactable_custom_question_id's question. THE ORDER IS THE WAVE'S FILL ORDER — the wave takes everyone matching its first option, then its second, and so on until capacity — and it is this wave's own order, independent of the question's option order. An option may appear on several waves deliberately: a participant lands in the first wave, by position, that admits them and has room, so a repeated option spills an oversized answer group across waves.","items":{"type":"integer","format":"int64"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/waves/reorder":{"patch":{"tags":["Waves"],"summary":"Reorder waves","operationId":"reorderWaves","description":"Sets wave positions from the order of the given ids. Foreign ids are ignored.","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["wave_ids"],"properties":{"wave_ids":{"type":"array","items":{"type":"integer","format":"int64"}}}}}}},"responses":{"200":{"description":"Reordered","content":{"application/json":{"schema":{"type":"object","required":["waves"],"properties":{"waves":{"type":"array","items":{"$ref":"#/components/schemas/Wave"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
````

## GET /api/v1/events/{handle}/waves/participants

> List participants with wave-assignment status

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"WaveParticipantList":{"type":"object","required":["participants","total_count","page","per_page","total_pages"],"properties":{"participants":{"type":"array","items":{"type":"object","required":["id","first_name","last_name","email","confirmation_number","event_variant_id","wave_assignment"],"properties":{"id":{"type":"integer","format":"int64"},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"confirmation_number":{"type":"string","nullable":true},"event_variant_id":{"type":"integer","format":"int64"},"wave_assignment":{"type":"object","nullable":true,"required":["wave_id","wave_name","wave_number"],"properties":{"wave_id":{"type":"integer","format":"int64"},"wave_name":{"type":"string","nullable":true},"wave_number":{"type":"string","nullable":true}}}}}},"total_count":{"type":"integer"},"page":{"type":"integer"},"per_page":{"type":"integer"},"total_pages":{"type":"integer"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/waves/participants":{"get":{"tags":["Waves"],"summary":"List participants with wave-assignment status","operationId":"listWaveParticipants","parameters":[{"$ref":"#/components/parameters/EventHandle"},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["assigned","unassigned"]}},{"name":"wave_id","in":"query","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"event_variant_id","in":"query","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"search","in":"query","required":false,"schema":{"type":"string"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"sort","in":"query","required":false,"description":"Sort order: `wave` (by assigned wave), `time:<question_id>` (by a time-based question's answer), or `choice:<question_id>` (by a multiple-choice question's answer, ordered by the organizer's option order rather than the answer text). Defaults to name.","schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"Paginated participants","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveParticipantList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## Assign a participant to a wave

> Replaces any existing wave assignment for the participant.

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"AssignParticipantRequest":{"type":"object","required":["participant_id","wave_id"],"properties":{"participant_id":{"type":"integer","format":"int64"},"wave_id":{"type":"integer","format":"int64"},"wave_number":{"type":"string","description":"Optional number within the wave. Auto-assigned when the event uses auto wave numbers and none is given."}}},"WaveAssignment":{"type":"object","required":["participant_id","wave_id","wave_number"],"properties":{"participant_id":{"type":"integer","format":"int64"},"wave_id":{"type":"integer","format":"int64"},"wave_number":{"type":"string","nullable":true}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}},"ValidationError":{"type":"object","description":"A validation error response with a list of error messages","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"string"},"description":"List of validation error messages"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/wave_assignments":{"post":{"tags":["Waves"],"summary":"Assign a participant to a wave","operationId":"assignParticipantToWave","description":"Replaces any existing wave assignment for the participant.","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignParticipantRequest"}}}},"responses":{"201":{"description":"Assigned","content":{"application/json":{"schema":{"type":"object","required":["wave_assignment"],"properties":{"wave_assignment":{"$ref":"#/components/schemas/WaveAssignment"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}}}}
````

## DELETE /api/v1/events/{handle}/wave\_assignments

> Unassign every participant

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}}},"paths":{"/api/v1/events/{handle}/wave_assignments":{"delete":{"tags":["Waves"],"summary":"Unassign every participant","operationId":"unassignAllFromWaves","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"responses":{"200":{"description":"All assignments removed","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## DELETE /api/v1/events/{handle}/wave\_assignments/{participant\_id}

> Unassign a participant from their wave

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}}},"paths":{"/api/v1/events/{handle}/wave_assignments/{participant_id}":{"delete":{"tags":["Waves"],"summary":"Unassign a participant from their wave","operationId":"unassignParticipantFromWave","parameters":[{"$ref":"#/components/parameters/EventHandle"},{"name":"participant_id","in":"path","required":true,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Unassigned","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","enum":[true]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## Bulk-assign participants by list

> Assigns many participants in one transaction (any error rolls back the\
> whole batch). Each entry identifies a participant by \`participant\_id\` or\
> \`confirmation\_number\`, and a wave by \`wave\_id\` or \`wave\_name\` (names are\
> created on the fly if they don't exist).<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"BulkWaveAssignmentRequest":{"type":"object","required":["assignments"],"properties":{"assignments":{"type":"array","minItems":1,"items":{"type":"object","description":"Identify the participant by id OR confirmation_number, and the wave by id OR name.","properties":{"participant_id":{"type":"integer","format":"int64"},"confirmation_number":{"type":"string"},"wave_id":{"type":"integer","format":"int64"},"wave_name":{"type":"string"},"wave_number":{"type":"string"}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/wave_assignments/bulk":{"post":{"tags":["Waves"],"summary":"Bulk-assign participants by list","operationId":"bulkAssignWaveParticipants","description":"Assigns many participants in one transaction (any error rolls back the\nwhole batch). Each entry identifies a participant by `participant_id` or\n`confirmation_number`, and a wave by `wave_id` or `wave_name` (names are\ncreated on the fly if they don't exist).\n","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkWaveAssignmentRequest"}}}},"responses":{"200":{"description":"Import summary","content":{"application/json":{"schema":{"type":"object","required":["created","moved","created_waves"],"properties":{"created":{"type":"integer"},"moved":{"type":"integer"},"created_waves":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
````

## Bulk auto-assign participants into waves

> Orders candidate participants and drops each into the first eligible\
> public wave (walked top-to-bottom by position), honoring each wave's\
> sub-event links, gender restriction, time gate, answer gate, and\
> capacity. Use \`dry\_run\` to preview without persisting.\
> \
> This is what backfills registrants who signed up before a gate existed,\
> so it is step 3 of gating waves on a question's answers: read the option\
> counts from \`GET /wave\_gate\_questions\`, size and set each wave's gate\
> with \`PATCH /waves/{id}\`, run this, then set \`PATCH /wave\_settings\` so\
> future registrations are placed on arrival.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"WaveGenerationRequest":{"type":"object","properties":{"ordering":{"type":"string","enum":["registration_date","custom_question","name","age","random"],"description":"How candidates are ordered before filling waves. Defaults to registration_date."},"direction":{"type":"string","enum":["asc","desc"],"description":"asc fills front waves with earliest/fastest first. Defaults to asc."},"gender_mode":{"type":"string","enum":["none","separate_waves","order_within"]},"transactable_custom_question_id":{"type":"integer","format":"int64","description":"Required when ordering=custom_question. Orders by the answer to this time-based question."},"assign_wave_numbers":{"type":"boolean","description":"Defaults to the event's auto_assign_wave_numbers setting."},"overwrite":{"type":"boolean","description":"Re-assign everyone (clears existing public-wave assignments first). Defaults to false (fill unassigned only)."},"dry_run":{"type":"boolean","description":"Preview the result without persisting."}}},"WaveGenerationResult":{"type":"object","required":["assigned_count","overflow_count","dry_run","wave_summaries"],"properties":{"assigned_count":{"type":"integer"},"overflow_count":{"type":"integer"},"dry_run":{"type":"boolean"},"wave_summaries":{"type":"array","items":{"type":"object","required":["id","name","newly_assigned","existing_assigned","capacity"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"newly_assigned":{"type":"integer"},"existing_assigned":{"type":"integer"},"capacity":{"type":"integer","nullable":true}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/wave_generation":{"post":{"tags":["Waves"],"summary":"Bulk auto-assign participants into waves","operationId":"generateWaveAssignments","description":"Orders candidate participants and drops each into the first eligible\npublic wave (walked top-to-bottom by position), honoring each wave's\nsub-event links, gender restriction, time gate, answer gate, and\ncapacity. Use `dry_run` to preview without persisting.\n\nThis is what backfills registrants who signed up before a gate existed,\nso it is step 3 of gating waves on a question's answers: read the option\ncounts from `GET /wave_gate_questions`, size and set each wave's gate\nwith `PATCH /waves/{id}`, run this, then set `PATCH /wave_settings` so\nfuture registrations are placed on arrival.\n","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveGenerationRequest"}}}},"responses":{"200":{"description":"Assignment result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveGenerationResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
````

## GET /api/v1/events/{handle}/wave\_settings

> Get wave settings

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"WaveSettings":{"type":"object","required":["uses_wave_numbers","auto_assign_wave_numbers","sub_events"],"properties":{"uses_wave_numbers":{"type":"boolean"},"auto_assign_wave_numbers":{"type":"boolean"},"sub_events":{"type":"array","items":{"type":"object","required":["id","name","auto_assign_waves"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"auto_assign_waves":{"type":"boolean"}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/wave_settings":{"get":{"tags":["Waves"],"summary":"Get wave settings","operationId":"getWaveSettings","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"responses":{"200":{"description":"Settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveSettings"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## PATCH /api/v1/events/{handle}/wave\_settings

> Update wave settings

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"WaveSettingsUpdateRequest":{"type":"object","properties":{"uses_wave_numbers":{"type":"boolean"},"auto_assign_wave_numbers":{"type":"boolean"},"variant_settings":{"type":"object","description":"Map of sub-event id (string) to a boolean auto_assign_waves flag.","additionalProperties":{"type":"boolean"}}}},"WaveSettings":{"type":"object","required":["uses_wave_numbers","auto_assign_wave_numbers","sub_events"],"properties":{"uses_wave_numbers":{"type":"boolean"},"auto_assign_wave_numbers":{"type":"boolean"},"sub_events":{"type":"array","items":{"type":"object","required":["id","name","auto_assign_waves"],"properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"auto_assign_waves":{"type":"boolean"}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}},"ValidationError":{"type":"object","description":"A validation error response with a list of error messages","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"string"},"description":"List of validation error messages"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/wave_settings":{"patch":{"tags":["Waves"],"summary":"Update wave settings","operationId":"updateWaveSettings","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveSettingsUpdateRequest"}}}},"responses":{"200":{"description":"Updated settings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveSettings"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}}}}
````

## List the custom questions waves can gate on, with live answer counts

> The sizing data behind a wave split. Returns every custom question\
> attached to this event or one of its sub-events that a wave can gate\
> on — time-based questions (which drive a wave's TIME gate) and\
> multiple-choice questions (which drive its ANSWER gate) — and, for each\
> multiple-choice option, how many of \*\*this event's\*\* registrants\
> currently picked it.\
> \
> \*\*The counts are live.\*\* They are computed per request from the answers\
> on record right now and scoped to this event's participants, so they\
> move as registrations arrive and a foreign event's answers never leak\
> in. Read them immediately before sizing waves rather than caching them.\
> \
> \*\*Every count on a row shares that row's scope.\*\* A question's\
> \`eligible\_participant\_count\` is the population it is actually put to —\
> the whole event for an event-level row, one sub-event's registrants for\
> a sub-event row — so on a multiple-choice row unanswered is that minus\
> \`answered\_participant\_count\`, and never needs an event-wide total. There\
> deliberately is no event-wide figure in this payload: measuring a\
> sub-event question's answers against the whole field invents an\
> unanswered population that can never shrink. Where the audience is\
> narrower still than its sub-event — a relay captain or teammate row —\
> the denominator is null rather than approximated, for the same reason.\
> \
> \*\*Only questions an event participant can answer are returned.\*\*\
> Volunteer and waitlist questions are excluded: their answers belong to\
> \`Volunteer\` / \`Waitlister\` records rather than participants, so a wave\
> gated on one would admit nobody while displaying a plausible-looking\
> row of zeroes. Relay captain and relay teammate questions ARE answered\
> by participants and are included.\
> \
> \*\*Mind the two id spaces.\*\* Each row's \`id\` is a\
> \`TransactableCustomQuestion\` id — the question \*as attached to\* this\
> event or one specific sub-event — and that is the id a wave's gate\
> points at. \`question\_id\` is the underlying \`Question\`, which a wave\
> never references. One question attached to N sub-events is N rows\
> sharing one \`question\_id\`, one title and one option set, and an answer\
> is keyed to the row, so gating on the wrong sibling reads a different\
> sub-event's answers while looking correct.\
> \
> Typical flow: read the counts here, size and set each wave's gate with\
> \`PATCH /waves/{id}\`, backfill existing registrations with\
> \`POST /wave\_generation\`, then set \`PATCH /wave\_settings\` so future\
> registrations are placed on arrival.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Waves","description":"Manage waves (start corrals / heats), assign and unassign participants,\nrun bulk auto-assignment, and configure wave-number settings. A wave with\nno linked sub-events applies to every sub-event (\"shared\"); linking\nsub-events restricts it to those. Waves can be capped, gender-restricted,\nand gated on a custom question's answers — a TIME gate (bounds on a\ntime-based question's answer) and an ANSWER gate (an ordered set of a\nmultiple-choice question's options). The two gates are independent and\nAND: a wave may carry neither, either or both, and a participant must\nclear every gate that is configured. Read\n`GET /api/v1/events/{handle}/wave_gate_questions` for the questions\navailable to gate on and the live count behind each answer option before\nsizing waves.\n"}],"servers":[{"url":"https://www.movemint.cc","description":"Production"}],"security":[{"bearerAuth":[]},{"oauth2":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Pass the access token in the `Authorization` header:\n```\nAuthorization: Bearer YOUR_ACCESS_TOKEN\n```\n"},"oauth2":{"type":"oauth2","description":"OAuth 2.0 authentication using the Authorization Code or Client\nCredentials grant flow.\n","flows":{"authorizationCode":{"authorizationUrl":"https://www.movemint.cc/oauth/authorize","tokenUrl":"https://www.movemint.cc/oauth/token","refreshUrl":"https://www.movemint.cc/oauth/token","scopes":{}},"clientCredentials":{"tokenUrl":"https://www.movemint.cc/oauth/token","scopes":{}}}}},"parameters":{"EventHandle":{"name":"handle","in":"path","required":true,"description":"The unique handle (slug) of the event","schema":{"type":"string"}}},"schemas":{"WaveGateQuestionList":{"type":"object","required":["questions"],"properties":{"questions":{"type":"array","description":"Every question a wave on this event can gate on, in the organizer's display order. Empty when the event asks no time-based or multiple-choice question its registrants can answer. Each row carries its own denominator (`eligible_participant_count`), so \"N of M answered\" reads off a single row without combining scopes.","items":{"$ref":"#/components/schemas/WaveGateQuestion"}}}},"WaveGateQuestion":{"type":"object","required":["id","question_id","title","label","sub_event_id","sub_event_name","sub_transactable_type","time_based","multiple_choice","eligible_participant_count","answered_participant_count","options"],"properties":{"id":{"type":"integer","format":"int64","description":"The TransactableCustomQuestion id — the question as attached to this event or one of its sub-events. THIS is the id a wave's gate points at: send it as `answer_transactable_custom_question_id` when `multiple_choice` is true, or as `transactable_custom_question_id` when `time_based` is true. Do not send `question_id` in its place; the two id spaces are unrelated and a Question id here matches a different question or none."},"question_id":{"type":"integer","format":"int64","description":"The underlying Question. Rows sharing this value are the same question attached to different sub-events (or to different sub-audiences of one), sharing a title and an option set while holding separate answers. Informational only — no wave endpoint accepts it."},"title":{"type":"string","description":"The question as the organizer wrote it. Not unique across rows."},"label":{"type":"string","description":"The title qualified by what this row alone covers — its sub-event and its sub-audience, when it has them. Use this, not `title`, to tell sibling rows apart when presenting a choice."},"sub_event_id":{"type":"integer","format":"int64","nullable":true,"description":"The sub-event this row hangs off, or null when it is attached to the event and therefore covers every registrant."},"sub_event_name":{"type":"string","nullable":true},"sub_transactable_type":{"type":"string","nullable":true,"enum":["relay_captain","relay_teammate"],"description":"The sub-audience within the transactable that answers this row, or null when every registrant does. Only participant-answered audiences appear here; volunteer and waitlist rows are not returned at all."},"time_based":{"type":"boolean","description":"The answer is an elapsed time, so this question can drive a wave's TIME gate via `transactable_custom_question_id` plus `min_time_seconds` / `max_time_seconds`."},"multiple_choice":{"type":"boolean","description":"The answer is one of `options`, so this question can drive a wave's ANSWER gate via `answer_transactable_custom_question_id` plus `question_multiple_choice_option_ids`."},"eligible_participant_count":{"type":"integer","nullable":true,"description":"Registrants who could answer this question — the population the question is actually put to, and the denominator its option counts sit under.\n**On a multiple-choice row, unanswered = this minus `answered_participant_count`**: how many people an answer-gated wave built from these options would not admit, and therefore whether the split needs an ungated catch-all wave. The subtraction is meaningless on a time-based row, which has no options and so always reports `answered_participant_count: 0` — that zero means \"not counted here\", not \"nobody answered\".\nScoped the way the question is: a row attached to the event counts every registrant, while a row attached to a sub-event counts only that sub-event's registrants. Do not compare a sub-event question's option counts against the event's field size — the people in other sub-events were never asked.\n**Null when `sub_transactable_type` is set.** A `relay_captain` / `relay_teammate` row is put to a slice of its sub-event — one captain per team, or the teammates around them — and that slice is not expressible as a single grouped count, so it is not computed. Skip the subtraction on those rows rather than substituting the sub-event's field size: a 100-registrant relay across 25 teams would read as 75 captains yet to answer when in fact every captain has."},"answered_participant_count":{"type":"integer","description":"Registrants whose answer matches one of `options` — the sum of the per-option counts. Always 0 for a time-based question, which has no options to count against, so do not read that zero as an unanswered field. An answer left over from an option the organizer has since renamed or deleted matches nothing and is counted nowhere, so this can sit below the number of people who actually answered."},"options":{"type":"array","description":"The selectable answers, in the organizer's option order. Empty for a time-based question. This order is the QUESTION's; it is not the order a wave fills from, which each wave sets for itself.","items":{"$ref":"#/components/schemas/WaveGateQuestionOption"}}}},"WaveGateQuestionOption":{"type":"object","required":["id","option_text","position","participant_count"],"properties":{"id":{"type":"integer","format":"int64","description":"The QuestionMultipleChoiceOption id. Goes into a wave's `question_multiple_choice_option_ids`, where THE ARRAY ORDER IS THE WAVE'S FILL ORDER — the wave takes everyone matching its first option, then its second, and so on until it reaches capacity. That order is the wave's own and is independent of `position` below. The same option id may be listed on several waves on purpose: a participant lands in the first wave, by wave position, that both admits their answer and has room, so repeating an option on a later wave is how an oversized answer group spills across waves."},"option_text":{"type":"string","description":"What the participant sees, and what an answer physically stores — an option's count is its text's count, which is why renaming an option orphans the answers already given under the old text."},"position":{"type":"integer","description":"The option's index within the question, as the organizer ordered them. Presentation order only; a wave's fill order is set per wave."},"participant_count":{"type":"integer","description":"Registrants of THIS event who currently picked this option. Live at request time — it moves as registrations arrive — and the number to size a wave's capacity against."}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Machine-readable error code"},"error_description":{"type":"string","description":"Human-readable description of the error"}}},"Error":{"type":"object","description":"A generic error response","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"Forbidden":{"description":"Forbidden - user is not an admin of this event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"EventNotFound":{"description":"Event not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/v1/events/{handle}/wave_gate_questions":{"get":{"tags":["Waves"],"summary":"List the custom questions waves can gate on, with live answer counts","operationId":"listWaveGateQuestions","description":"The sizing data behind a wave split. Returns every custom question\nattached to this event or one of its sub-events that a wave can gate\non — time-based questions (which drive a wave's TIME gate) and\nmultiple-choice questions (which drive its ANSWER gate) — and, for each\nmultiple-choice option, how many of **this event's** registrants\ncurrently picked it.\n\n**The counts are live.** They are computed per request from the answers\non record right now and scoped to this event's participants, so they\nmove as registrations arrive and a foreign event's answers never leak\nin. Read them immediately before sizing waves rather than caching them.\n\n**Every count on a row shares that row's scope.** A question's\n`eligible_participant_count` is the population it is actually put to —\nthe whole event for an event-level row, one sub-event's registrants for\na sub-event row — so on a multiple-choice row unanswered is that minus\n`answered_participant_count`, and never needs an event-wide total. There\ndeliberately is no event-wide figure in this payload: measuring a\nsub-event question's answers against the whole field invents an\nunanswered population that can never shrink. Where the audience is\nnarrower still than its sub-event — a relay captain or teammate row —\nthe denominator is null rather than approximated, for the same reason.\n\n**Only questions an event participant can answer are returned.**\nVolunteer and waitlist questions are excluded: their answers belong to\n`Volunteer` / `Waitlister` records rather than participants, so a wave\ngated on one would admit nobody while displaying a plausible-looking\nrow of zeroes. Relay captain and relay teammate questions ARE answered\nby participants and are included.\n\n**Mind the two id spaces.** Each row's `id` is a\n`TransactableCustomQuestion` id — the question *as attached to* this\nevent or one specific sub-event — and that is the id a wave's gate\npoints at. `question_id` is the underlying `Question`, which a wave\nnever references. One question attached to N sub-events is N rows\nsharing one `question_id`, one title and one option set, and an answer\nis keyed to the row, so gating on the wrong sibling reads a different\nsub-event's answers while looking correct.\n\nTypical flow: read the counts here, size and set each wave's gate with\n`PATCH /waves/{id}`, backfill existing registrations with\n`POST /wave_generation`, then set `PATCH /wave_settings` so future\nregistrations are placed on arrival.\n","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"responses":{"200":{"description":"Gateable questions with live per-option registrant counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaveGateQuestionList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````
