> 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/event-lifecycle.md).

# Event Lifecycle

Endpoints for moving an event through its lifecycle (status changes, pricing model lock-in, readiness checks).

## List the editions linked to this event

> Editions are the recurring years of the same race ("The Mid South" 2023,\
> 2024, 2025). They are a link between separate events, not a field on one.\
> \
> An event that belongs to no group is returned as a single edition —\
> itself — which means it is not linked to anything.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"EditionGroup":{"type":"object","required":["group_name","current_edition_handle","editions"],"properties":{"group_name":{"type":"string","nullable":true,"description":"Null when the event belongs to no group yet."},"current_edition_handle":{"type":"string","nullable":true,"description":"The edition the admin surfaces as \"Current\"; null with no group."},"editions":{"type":"array","items":{"type":"object","required":["handle","name","status","start_date","end_date"],"properties":{"handle":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["draft","live","unlisted","landing_page","past_event"]},"start_date":{"type":"string","format":"date","nullable":true},"end_date":{"type":"string","format":"date","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}/editions":{"get":{"tags":["Event Lifecycle"],"summary":"List the editions linked to this event","description":"Editions are the recurring years of the same race (\"The Mid South\" 2023,\n2024, 2025). They are a link between separate events, not a field on one.\n\nAn event that belongs to no group is returned as a single edition —\nitself — which means it is not linked to anything.\n","operationId":"listEventEditions","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"responses":{"200":{"description":"The edition group and its members","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditionGroup"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## Link events into this event's edition group

> Links one or more existing events into this event's edition group,\
> creating the group on first use and naming it after this event.\
> Idempotent — an event already in the group is left alone.\
> \
> Because linking mutates the \*target\* events, the caller must administer\
> every event named, not just the one in the path. A handle the caller\
> cannot administer returns \`403\` and nothing is linked.\
> \
> To create a new historical event and link it in a single request, use\
> \`edition\_of\` on \`POST /api/v1/events\` instead.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"EditionGroup":{"type":"object","required":["group_name","current_edition_handle","editions"],"properties":{"group_name":{"type":"string","nullable":true,"description":"Null when the event belongs to no group yet."},"current_edition_handle":{"type":"string","nullable":true,"description":"The edition the admin surfaces as \"Current\"; null with no group."},"editions":{"type":"array","items":{"type":"object","required":["handle","name","status","start_date","end_date"],"properties":{"handle":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["draft","live","unlisted","landing_page","past_event"]},"start_date":{"type":"string","format":"date","nullable":true},"end_date":{"type":"string","format":"date","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"}}}},"responses":{"Unauthorized":{"description":"Unauthorized - invalid or missing token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}},"paths":{"/api/v1/events/{handle}/editions":{"post":{"tags":["Event Lifecycle"],"summary":"Link events into this event's edition group","description":"Links one or more existing events into this event's edition group,\ncreating the group on first use and naming it after this event.\nIdempotent — an event already in the group is left alone.\n\nBecause linking mutates the *target* events, the caller must administer\nevery event named, not just the one in the path. A handle the caller\ncannot administer returns `403` and nothing is linked.\n\nTo create a new historical event and link it in a single request, use\n`edition_of` on `POST /api/v1/events` instead.\n","operationId":"linkEventEditions","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["handles"],"properties":{"handles":{"type":"array","items":{"type":"string"},"description":"Handles of the events to link into this group."}}}}}},"responses":{"200":{"description":"The edition group after linking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditionGroup"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The caller does not administer one of the events named"},"404":{"description":"No event exists for one of the handles given"},"422":{"description":"handles was empty, or an event already belongs to a different group"}}}}}}
````

## Unlink one edition from this event's group

> Removes the link only — the event itself is untouched. If this leaves the\
> group with a single edition, the group is dissolved, since a group of one\
> carries no meaning.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"EditionGroup":{"type":"object","required":["group_name","current_edition_handle","editions"],"properties":{"group_name":{"type":"string","nullable":true,"description":"Null when the event belongs to no group yet."},"current_edition_handle":{"type":"string","nullable":true,"description":"The edition the admin surfaces as \"Current\"; null with no group."},"editions":{"type":"array","items":{"type":"object","required":["handle","name","status","start_date","end_date"],"properties":{"handle":{"type":"string"},"name":{"type":"string"},"status":{"type":"string","enum":["draft","live","unlisted","landing_page","past_event"]},"start_date":{"type":"string","format":"date","nullable":true},"end_date":{"type":"string","format":"date","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"}}}}}},"paths":{"/api/v1/events/{handle}/editions/{edition_handle}":{"delete":{"tags":["Event Lifecycle"],"summary":"Unlink one edition from this event's group","description":"Removes the link only — the event itself is untouched. If this leaves the\ngroup with a single edition, the group is dissolved, since a group of one\ncarries no meaning.\n","operationId":"unlinkEventEdition","parameters":[{"$ref":"#/components/parameters/EventHandle"},{"name":"edition_handle","in":"path","required":true,"schema":{"type":"string"},"description":"Handle of the edition to unlink."}],"responses":{"200":{"description":"The edition group after unlinking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EditionGroup"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"That event is not an edition of this group"}}}}}}
````

## Duplicate an event

> Creates a new event copied from this one, the same way the Create New\
> Event drawer in the event admin does. The copy carries over the source\
> event's details, sub-events and their price tiers, sponsors,\
> registration questions, waivers and marketing photos; each of those can\
> be left behind with its \`copy\_\*\` flag.\
> \
> Two things this endpoint deliberately does not do:\
> \
> \- \*\*The copy is always a draft.\*\* Duplicating never publishes. Going\
> &#x20; live still runs the readiness check on\
> &#x20; \`PATCH /api/v1/events/{handle}/status\`.\
> \- \*\*The source event's admins are not copied.\*\* The authenticated\
> &#x20; caller becomes the new event's owner; everyone else is added\
> &#x20; separately. Copying the roster would grant other people access to a\
> &#x20; resource that did not exist a moment ago.\
> \
> Price tiers whose \`close\_date\` has already passed are dropped, so the\
> copy is not left with an unpriced sub-event. If that drops every tier\
> on a sub-event, the most recently closed one is carried across as\
> indefinite.\
> \
> Marketing photos are copied as independent records — deleting one from\
> either event leaves the other's gallery alone. A photo whose stored\
> image cannot be read is skipped rather than failing the request.\
> \
> Requires the \`duplication\` scope on the source event.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"DuplicateEventRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"Name for the new event."},"start_date":{"type":"string","format":"date","description":"Start date for the copy. Omitted keeps the source event's dates.\n"},"end_date":{"type":"string","format":"date","description":"End date for the copy. Omitted keeps the source event's dates.\n"},"as_edition":{"type":"boolean","default":false,"description":"Link the copy into the source event's edition group, creating the\ngroup on first use — for another year of the same race.\n"},"copy_sub_events":{"type":"boolean","default":true,"description":"Copy the source's sub-events and their price tiers."},"copy_sponsors":{"type":"boolean","default":true,"description":"Copy the source's sponsors."},"copy_questions":{"type":"boolean","default":true,"description":"Copy the source's registration questions."},"copy_waivers":{"type":"boolean","default":true,"description":"Copy the source's active waivers."},"copy_marketing_photos":{"type":"boolean","default":true,"description":"Copy the source's marketing photos as independent photos."}}},"DuplicateEventResponse":{"allOf":[{"$ref":"#/components/schemas/Event"},{"type":"object","properties":{"duplicated_from":{"type":"string","description":"Handle of the event this copy was made from."},"edition_group_name":{"type":"string","nullable":true,"description":"Name of the edition group the copy was linked into. Only\npresent when `as_edition` was true.\n"}}}]},"Event":{"type":"object","description":"An event on the Movemint platform. All properties are always present in the response; nullable properties may have a null value.","required":["id","name","latitude","longitude","start_date","end_date","sport_type","registration_open_date","registration_close_date","phone_number","email","website","description","max_participants","address","city","state","zip","presented_by","handle","created_at","updated_at","blurb","custom_waiver","status","collect_gender_identity","option_nonbinary","option_prefer_not_to_say","instagram_url","strava_club_url","x_url","facebook_url","donation_goal_cents","registration_included_in_donation_goal","currency","volunteer_message","allow_participant_transfer","participant_transfer_close_date","country","tax_address","collect_phone_number","collect_dob","collect_address","collect_emergency_contact","peer_to_peer_fundraiser","merchandise_tax_rate","service_fee_tax_rate","registration_tax_rate","stripe_account_id","onboarding_step","new_participant_owner_email_setting","show_signup_count","protecht_enabled","send_participant_confirmation_emails","teams_enabled","show_teams_on_start_list","show_teams_on_results","show_relay_teams_on_start_list","timezone","email_owner_on_merchandise_order","email_check_in_confirmation","team_password_setting","sms_enabled","is_free","show_start_list","event_cancellation_insurance_enabled","last_transaction_date","allow_event_variant_transfers","partner_program_eligible","show_event_variant_price_schedule","require_one_sub_event","frictionless_protecht","buy_now_pay_later","show_location","merchandise_store_close_date_time_utc","presale_registration_open_date","presale_registration_code","write_to_strava_activity_on_finish","tax_exempt","name_with_start_date","formatted_start_date","name_with_year"],"properties":{"id":{"type":"integer","format":"int64","description":"Unique identifier"},"name":{"type":"string","description":"Event name"},"latitude":{"type":"string","description":"Venue latitude (decimal as string for precision)"},"longitude":{"type":"string","description":"Venue longitude (decimal as string for precision)"},"start_date":{"type":"string","format":"date-time","description":"Event start date and time (UTC)"},"end_date":{"type":"string","format":"date-time","nullable":true,"description":"Event end date and time (UTC), if applicable"},"sport_type":{"type":"string","description":"Type of sport"},"registration_open_date":{"type":"string","format":"date-time","description":"When registration opens (UTC)"},"registration_close_date":{"type":"string","format":"date-time","nullable":true,"description":"When registration closes (UTC)"},"phone_number":{"type":"string","nullable":true,"description":"Contact phone number"},"email":{"type":"string","format":"email","description":"Contact email for the event"},"website":{"type":"string","format":"uri","description":"Event website URL"},"description":{"type":"string","description":"Full event description (may contain HTML)"},"max_participants":{"type":"integer","nullable":true,"description":"Maximum number of participants allowed; null if unlimited"},"address":{"type":"string","description":"Venue address or venue name"},"city":{"type":"string","description":"City"},"state":{"type":"integer","description":"State enum value (e.g. 21 for Massachusetts)"},"zip":{"type":"string","description":"ZIP or postal code"},"presented_by":{"type":"string","nullable":true,"description":"Presenting sponsor or organization name"},"handle":{"type":"string","description":"Unique URL-safe handle used in paths"},"created_at":{"type":"string","format":"date-time","description":"Record creation timestamp (UTC)"},"updated_at":{"type":"string","format":"date-time","description":"Last update timestamp (UTC)"},"blurb":{"type":"string","description":"Short event summary"},"custom_waiver":{"type":"string","nullable":true,"description":"Custom waiver text shown during registration"},"status":{"type":"string","enum":["draft","live","unlisted","landing_page","past_event"],"description":"Event visibility/status"},"collect_gender_identity":{"type":"boolean","description":"Whether gender identity is collected during registration"},"option_nonbinary":{"type":"boolean","description":"Whether non-binary is offered as a gender identity option"},"option_prefer_not_to_say":{"type":"boolean","description":"Whether \"prefer not to say\" is offered for gender identity"},"instagram_url":{"type":"string","format":"uri","nullable":true,"description":"Instagram profile URL"},"strava_club_url":{"type":"string","format":"uri","nullable":true,"description":"Strava club URL"},"x_url":{"type":"string","format":"uri","nullable":true,"description":"X (Twitter) profile URL"},"facebook_url":{"type":"string","format":"uri","nullable":true,"description":"Facebook page URL"},"donation_goal_cents":{"type":"integer","format":"int64","nullable":true,"description":"Donation goal in cents, if fundraising is enabled"},"registration_included_in_donation_goal":{"type":"boolean","description":"Whether registration revenue counts toward donation goal"},"currency":{"type":"string","description":"ISO 4217 currency code"},"volunteer_message":{"type":"string","nullable":true,"description":"Message or instructions for volunteers"},"allow_participant_transfer":{"type":"boolean","description":"Whether participant transfers are allowed"},"participant_transfer_close_date":{"type":"string","format":"date-time","nullable":true,"description":"Deadline for participant transfers (UTC)"},"country":{"type":"integer","description":"Country enum value (e.g. 187 for United States)"},"tax_address":{"type":"string","nullable":true,"description":"Address used for tax purposes"},"collect_phone_number":{"type":"boolean","description":"Whether phone number is collected during registration"},"collect_dob":{"type":"boolean","description":"Whether date of birth is collected during registration"},"collect_address":{"type":"boolean","description":"Whether address is collected during registration"},"collect_emergency_contact":{"type":"boolean","description":"Whether emergency contact is collected during registration"},"peer_to_peer_fundraiser":{"type":"boolean","description":"Whether peer-to-peer fundraising is enabled"},"merchandise_tax_rate":{"type":"string","nullable":true,"description":"Tax rate for merchandise (decimal as string, e.g. 0.0)"},"service_fee_tax_rate":{"type":"string","nullable":true,"description":"Tax rate for service fees (decimal as string, e.g. 0.0625)"},"registration_tax_rate":{"type":"string","nullable":true,"description":"Tax rate for registration (decimal as string)"},"stripe_account_id":{"type":"integer","format":"int64","nullable":true,"description":"Stripe Connect account ID when connected"},"onboarding_step":{"type":"integer","nullable":true,"description":"Current step in event onboarding flow"},"new_participant_owner_email_setting":{"type":"string","enum":["never_email_owner","email_owner_every_participant"],"description":"When the event owner is emailed about new participants"},"show_signup_count":{"type":"boolean","description":"Whether the signup count is publicly visible"},"protecht_enabled":{"type":"boolean","description":"Whether event cancellation insurance (Protecht) is offered"},"send_participant_confirmation_emails":{"type":"boolean","description":"Whether confirmation emails are sent to participants"},"teams_enabled":{"type":"boolean","description":"Whether team registration is enabled"},"show_teams_on_start_list":{"type":"boolean","description":"Whether teams are shown on the start list"},"show_teams_on_results":{"type":"boolean","description":"Whether teams are shown on results"},"show_relay_teams_on_start_list":{"type":"boolean","description":"Whether relay teams are shown on the start list"},"timezone":{"type":"string","description":"IANA timezone string"},"email_owner_on_merchandise_order":{"type":"boolean","description":"Whether the owner is emailed on merchandise orders"},"email_check_in_confirmation":{"type":"boolean","description":"Whether check-in confirmation emails are sent"},"team_password_setting":{"type":"string","enum":["no_password","optional","required"],"description":"Whether team join requires a password"},"sms_enabled":{"type":"boolean","description":"Whether SMS notifications are enabled"},"is_free":{"type":"boolean","description":"Whether the event is free to register"},"show_start_list":{"type":"boolean","description":"Whether the start list is publicly visible"},"event_cancellation_insurance_enabled":{"type":"boolean","description":"Whether event cancellation insurance is enabled"},"last_transaction_date":{"type":"string","format":"date-time","nullable":true,"description":"Timestamp of last transaction (UTC)"},"allow_event_variant_transfers":{"type":"boolean","description":"Whether transfers between event variants are allowed"},"partner_program_eligible":{"type":"boolean","description":"Whether the event is eligible for partner program"},"show_event_variant_price_schedule":{"type":"boolean","description":"Whether event variant price schedule is shown"},"require_one_sub_event":{"type":"boolean","description":"Whether participant must choose at least one sub-event"},"frictionless_protecht":{"type":"boolean","description":"Whether frictionless Protecht signup is enabled"},"buy_now_pay_later":{"type":"boolean","description":"Whether buy-now-pay-later is offered"},"show_location":{"type":"boolean","description":"Whether the event location is shown publicly"},"merchandise_store_close_date_time_utc":{"type":"string","format":"date-time","nullable":true,"description":"When the merchandise store closes (UTC)"},"presale_registration_open_date":{"type":"string","format":"date-time","nullable":true,"description":"When presale registration opens (UTC)"},"presale_registration_code":{"type":"string","nullable":true,"description":"Code required for presale registration"},"write_to_strava_activity_on_finish":{"type":"boolean","description":"Whether finishing writes to Strava activity"},"tax_exempt":{"type":"boolean","description":"Whether the event is tax exempt"},"name_with_start_date":{"type":"string","description":"Event name combined with formatted start date"},"formatted_start_date":{"type":"string","description":"Human-readable formatted start date"},"name_with_year":{"type":"string","description":"Event name with year"}}},"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}/duplication":{"post":{"tags":["Event Lifecycle"],"summary":"Duplicate an event","description":"Creates a new event copied from this one, the same way the Create New\nEvent drawer in the event admin does. The copy carries over the source\nevent's details, sub-events and their price tiers, sponsors,\nregistration questions, waivers and marketing photos; each of those can\nbe left behind with its `copy_*` flag.\n\nTwo things this endpoint deliberately does not do:\n\n- **The copy is always a draft.** Duplicating never publishes. Going\n  live still runs the readiness check on\n  `PATCH /api/v1/events/{handle}/status`.\n- **The source event's admins are not copied.** The authenticated\n  caller becomes the new event's owner; everyone else is added\n  separately. Copying the roster would grant other people access to a\n  resource that did not exist a moment ago.\n\nPrice tiers whose `close_date` has already passed are dropped, so the\ncopy is not left with an unpriced sub-event. If that drops every tier\non a sub-event, the most recently closed one is carried across as\nindefinite.\n\nMarketing photos are copied as independent records — deleting one from\neither event leaves the other's gallery alone. A photo whose stored\nimage cannot be read is skipped rather than failing the request.\n\nRequires the `duplication` scope on the source event.\n","operationId":"duplicateEvent","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateEventRequest"}}}},"responses":{"201":{"description":"The newly created copy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateEventResponse"}}}},"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 the readiness report for an event

> Returns the list of blocking issues and warnings that determine\
> whether an event can transition to \`live\` or \`unlisted\`. An event is\
> ready when \`blocking\` is an empty array; warnings are surfaced for\
> organizer attention but do not gate publishing.\
> \
> The blocking set depends on the event's current status. A\
> \`past\_event\` event is a record of a race that already happened, so\
> only its name and event date are checked; every other event is checked for pricing\
> model, name, dates, location, registration open date, at least one\
> priced sub-event, and (when paid) a connected Stripe account.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"ReadinessReport":{"type":"object","required":["ready","blocking","warnings"],"properties":{"ready":{"type":"boolean","description":"True when there are no blocking issues."},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/ReadinessIssue"}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ReadinessIssue"}}}},"ReadinessIssue":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable code for the issue."},"message":{"type":"string","description":"Human-readable explanation of what to fix."},"sub_event_id":{"type":"integer","format":"int64","description":"Present when the issue is scoped to a specific sub-event."}}},"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}/readiness":{"get":{"tags":["Event Lifecycle"],"summary":"Get the readiness report for an event","description":"Returns the list of blocking issues and warnings that determine\nwhether an event can transition to `live` or `unlisted`. An event is\nready when `blocking` is an empty array; warnings are surfaced for\norganizer attention but do not gate publishing.\n\nThe blocking set depends on the event's current status. A\n`past_event` event is a record of a race that already happened, so\nonly its name and event date are checked; every other event is checked for pricing\nmodel, name, dates, location, registration open date, at least one\npriced sub-event, and (when paid) a connected Stripe account.\n","operationId":"getEventReadiness","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"responses":{"200":{"description":"Readiness report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessReport"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"}}}}}}
````

## Update an event's status

> Transition an event between statuses (\`draft\`, \`live\`, \`unlisted\`,\
> \`landing\_page\`, \`past\_event\`). Transitions to \`live\`, \`unlisted\` or\
> \`past\_event\` are gated by the readiness check — if blocking issues\
> remain, the response is \`422\` with the same \`blocking\` / \`warnings\`\
> shape returned by \`GET /readiness\`.\
> \
> \`past\_event\` marks a race that already happened, kept as a record of\
> its participants, results and photos and closed to registration. It\
> needs just a name and an event date, so entering the status is cheap;\
> leaving it for \`live\` or \`unlisted\` runs the full readiness check and\
> returns \`422\` until the event has pricing, a registration open date,\
> at least one priced sub-event and (when paid) a connected Stripe\
> account.\
> \
> It can be set at creation, on a \`draft\`, or on any event whose end\
> date has already passed. An event that has not finished yet cannot be\
> turned into a past event — that would strand anyone mid-registration.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"EventStatusUpdate":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["draft","live","unlisted","landing_page","past_event"]}}},"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"}}},"ReadinessGatedError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"blocking":{"type":"array","items":{"$ref":"#/components/schemas/ReadinessIssue"}},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/ReadinessIssue"}}}},"ReadinessIssue":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable code for the issue."},"message":{"type":"string","description":"Human-readable explanation of what to fix."},"sub_event_id":{"type":"integer","format":"int64","description":"Present when the issue is scoped to a specific sub-event."}}}},"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}/status":{"patch":{"tags":["Event Lifecycle"],"summary":"Update an event's status","description":"Transition an event between statuses (`draft`, `live`, `unlisted`,\n`landing_page`, `past_event`). Transitions to `live`, `unlisted` or\n`past_event` are gated by the readiness check — if blocking issues\nremain, the response is `422` with the same `blocking` / `warnings`\nshape returned by `GET /readiness`.\n\n`past_event` marks a race that already happened, kept as a record of\nits participants, results and photos and closed to registration. It\nneeds just a name and an event date, so entering the status is cheap;\nleaving it for `live` or `unlisted` runs the full readiness check and\nreturns `422` until the event has pricing, a registration open date,\nat least one priced sub-event and (when paid) a connected Stripe\naccount.\n\nIt can be set at creation, on a `draft`, or on any event whose end\ndate has already passed. An event that has not finished yet cannot be\nturned into a past event — that would strand anyone mid-registration.\n","operationId":"updateEventStatus","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventStatusUpdate"}}}},"responses":{"200":{"description":"Status updated","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"The new event status."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Status invalid or readiness check failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessGatedError"}}}}}}}}}
````

## Set or change the event's pricing model (free vs paid)

> Sets whether the event is \`is\_free\`. Restricted to the event owner.\
> \
> On the first call it records the decision and stamps\
> \`pricing\_model\_confirmed\_at\`. It can also be called afterwards to\
> switch an already-configured event: switching to paid\
> (\`is\_free: false\`) requires a connected Stripe account that can accept\
> payments, and switching to free (\`is\_free: true\`) requires a\
> currently-paid event; either returns \`422\` otherwise. Switching to free\
> does not refund or change existing paid registrations.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"PricingModelUpdate":{"type":"object","required":["is_free"],"properties":{"is_free":{"type":"boolean","description":"True for a free event, false for a paid event."}}},"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}/pricing_model":{"patch":{"tags":["Event Lifecycle"],"summary":"Set or change the event's pricing model (free vs paid)","description":"Sets whether the event is `is_free`. Restricted to the event owner.\n\nOn the first call it records the decision and stamps\n`pricing_model_confirmed_at`. It can also be called afterwards to\nswitch an already-configured event: switching to paid\n(`is_free: false`) requires a connected Stripe account that can accept\npayments, and switching to free (`is_free: true`) requires a\ncurrently-paid event; either returns `422` otherwise. Switching to free\ndoes not refund or change existing paid registrations.\n","operationId":"updateEventPricingModel","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingModelUpdate"}}}},"responses":{"200":{"description":"Pricing model locked in","content":{"application/json":{"schema":{"type":"object","required":["id","is_free","pricing_model_confirmed_at"],"properties":{"id":{"type":"integer","format":"int64"},"is_free":{"type":"boolean"},"pricing_model_confirmed_at":{"type":"string","format":"date-time"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Already in the requested state, paid requires Stripe, or `is_free` missing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
````

## Promote a marketing photo to the event cover photo

> Copies a photo from the event's marketing album into the event's\
> ActiveStorage \`cover\_photo\` slot. The source photo must already be\
> \`ready\` (i.e., processed). The marketing photo itself is not\
> removed from the album.<br>

````json
{"openapi":"3.1.0","info":{"title":"Movemint API","version":"1.0"},"tags":[{"name":"Event Lifecycle","description":"Endpoints for moving an event through its lifecycle (status changes,\npricing model lock-in, readiness checks).\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":{"CoverPhotoUpdate":{"type":"object","required":["marketing_photo_uuid"],"properties":{"marketing_photo_uuid":{"type":"string","description":"UUID of a `ready` photo in the event's marketing album."}}},"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}/cover_photo":{"patch":{"tags":["Event Lifecycle"],"summary":"Promote a marketing photo to the event cover photo","description":"Copies a photo from the event's marketing album into the event's\nActiveStorage `cover_photo` slot. The source photo must already be\n`ready` (i.e., processed). The marketing photo itself is not\nremoved from the album.\n","operationId":"updateEventCoverPhoto","parameters":[{"$ref":"#/components/parameters/EventHandle"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverPhotoUpdate"}}}},"responses":{"200":{"description":"Cover photo attached","content":{"application/json":{"schema":{"type":"object","required":["event_handle","cover_photo_attached"],"properties":{"event_handle":{"type":"string"},"cover_photo_attached":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/EventNotFound"},"422":{"description":"Source photo missing or not ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}
````
