Skip to content

Registration

Propose a registration ruleset for review

Propose a registration ruleset (any subset of sections; absent sections are left alone) and get back one reviewable plan with a labeled before and after for every setting that moves, in the organizer's words. Nothing is saved by this call: the organizer approves the plan in Endurance HQ (or ehq_changes_apply applies it when their autonomy mode allows). Dates are local to the event ("YYYY-MM-DD", "YYYY-MM-DDTHH:mm"), money is in cents, and list items match saved rows by label, title or name. Read ehq_registration_rules_get first. Before changing ticket rules or categories while applications are open, run lottery.simulateDrawing through ehq_capabilities_query (a read that saves nothing) and tell the organizer what moves. Send an Idempotency-Key header.

Method

POST

Path

/v1/events/{eventId}/registration-rules/proposals

Required scope

changes:propose

Request

curl -X POST "https://api.endurancehq.app/v1/events/{eventId}/registration-rules/proposals" \
  -H "Authorization: Bearer $EHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Path parameters

eventIdstring, required

JSON body schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "ruleset": {
      "type": "object",
      "properties": {
        "mode": {
          "type": "string",
          "enum": [
            "fcfs",
            "lottery"
          ],
          "description": "How entries are awarded: \"fcfs\" is first come, first served; \"lottery\" fills the field from a weighted drawing of applications."
        },
        "lottery": {
          "description": "Lottery settings. Only the fields present change; null is not allowed while the mode is lottery.",
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "opensAt": {
                  "description": "When applications open, local date-time (2026-12-01T06:00).",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T(?:[01]\\d|2[0-3]):[0-5]\\d$",
                      "description": "A local date and time in the event's timezone, \"YYYY-MM-DDTHH:mm\" (2026-12-01T06:00 is 6:00 AM on December 1). Never an ISO instant with Z or an offset; EHQ converts it."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "closesAt": {
                  "description": "When applications close, local date-time (2026-12-12T23:59).",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T(?:[01]\\d|2[0-3]):[0-5]\\d$",
                      "description": "A local date and time in the event's timezone, \"YYYY-MM-DDTHH:mm\" (2026-12-01T06:00 is 6:00 AM on December 1). Never an ISO instant with Z or an offset; EHQ converts it."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "drawingDate": {
                  "description": "The published drawing date (display only; the organizer runs the drawing).",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A calendar date in the event's timezone, \"YYYY-MM-DD\"."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "announceAt": {
                  "description": "Hold results and notices until this local date-time. Null: results go out when the drawing runs.",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T(?:[01]\\d|2[0-3]):[0-5]\\d$",
                      "description": "A local date and time in the event's timezone, \"YYYY-MM-DDTHH:mm\" (2026-12-01T06:00 is 6:00 AM on December 1). Never an ISO instant with Z or an offset; EHQ converts it."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "applicationFeeCents": {
                  "description": "Fee to apply, in cents, charged at application. 0 for a free application.",
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 9007199254740991
                },
                "capacity": {
                  "description": "Total field size including reserved entries (saved on the event as its capacity): 200.",
                  "anyOf": [
                    {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "spotsToDraw": {
                  "description": "Spots the drawing fills (180 when 20 are director picks). Null: capacity minus reserved entries. Never more than capacity minus reserved entries.",
                  "anyOf": [
                    {
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 9007199254740991
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "acceptanceDeadlineHours": {
                  "description": "Hours a drawn runner has to accept and pay (default 72).",
                  "type": "number",
                  "minimum": 1
                },
                "tickets": {
                  "description": "How many tickets each application carries.",
                  "type": "object",
                  "properties": {
                    "base": {
                      "description": "Tickets every application starts with (default 1).",
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 100
                    },
                    "perPriorFinish": {
                      "description": "Extra tickets per prior finish of this race (1 means one more ticket for each finish). 0 or null: finishes earn nothing.",
                      "anyOf": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 100
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "priorFinishMax": {
                      "description": "Cap on tickets from prior finishes. Null: no cap.",
                      "anyOf": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 9007199254740991
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "countStartsAsFinishes": {
                      "description": "Count a DNF or DQ start the same as a finish for the per-finish ticket.",
                      "type": "boolean"
                    },
                    "volunteerYears": {
                      "description": "Tickets for years of volunteering at this race. Null: volunteering earns no tickets.",
                      "anyOf": [
                        {
                          "type": "object",
                          "properties": {
                            "afterYears": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 50,
                              "description": "Years of volunteering for this race required before any volunteer-year ticket is earned (2 means a volunteer with 2+ years earns tickets)."
                            },
                            "perYear": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 100,
                              "description": "Extra tickets per qualifying volunteer year."
                            },
                            "max": {
                              "description": "Cap on tickets from volunteer years. Null: no cap.",
                              "anyOf": [
                                {
                                  "type": "integer",
                                  "minimum": 0,
                                  "maximum": 9007199254740991
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "countYearsBeyondMinimum": {
                              "description": "True: only the years past the minimum earn tickets. False (default): every year earns once the minimum is met.",
                              "type": "boolean"
                            }
                          },
                          "required": [
                            "afterYears",
                            "perYear"
                          ],
                          "additionalProperties": false
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "repeatApplications": {
                      "description": "Tickets that build up over unsuccessful applications in earlier years.",
                      "type": "object",
                      "properties": {
                        "model": {
                          "type": "string",
                          "enum": [
                            "none",
                            "linear",
                            "doubling"
                          ],
                          "description": "\"none\": no extra tickets for unsuccessful past applications. \"linear\": adds `increment` tickets per unsuccessful year. \"doubling\": doubles each unsuccessful year."
                        },
                        "increment": {
                          "description": "Linear model only: tickets added per unsuccessful year (default 1).",
                          "type": "number",
                          "minimum": 0
                        },
                        "cap": {
                          "description": "Hard cap on total tickets after every rule. Null: no cap.",
                          "anyOf": [
                            {
                              "type": "number",
                              "minimum": 1
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "resetOnDecline": {
                          "description": "A runner who is drawn and declines starts over (default true).",
                          "type": "boolean"
                        },
                        "streakBreaksOnSkippedYear": {
                          "description": "Skipping a year resets the streak (default false).",
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "model"
                      ],
                      "additionalProperties": false
                    },
                    "volunteerBonus": {
                      "description": "Bonus tickets from volunteer credits or hours (separate from volunteer-year tickets).",
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean",
                          "description": "Whether volunteer credits or hours earn bonus tickets."
                        },
                        "source": {
                          "description": "What earns the bonus: recorded credits, verified hours, or both.",
                          "type": "string",
                          "enum": [
                            "credits",
                            "hours",
                            "credits_and_hours"
                          ]
                        },
                        "scope": {
                          "description": "Whose volunteering counts: \"series\" is this race's editions (default), \"event\" this edition only, \"organization\" any of this organizer's events, \"all\" every event on the platform.",
                          "type": "string",
                          "enum": [
                            "all",
                            "organization",
                            "series",
                            "event"
                          ]
                        },
                        "creditsPerTicket": {
                          "description": "Credits required per bonus ticket (default 1).",
                          "type": "number",
                          "minimum": 1
                        },
                        "hoursPerTicket": {
                          "description": "Verified hours required per bonus ticket (default 4).",
                          "type": "number",
                          "minimum": 1
                        },
                        "max": {
                          "description": "Most bonus tickets from volunteering (default 3).",
                          "type": "number",
                          "minimum": 0
                        }
                      },
                      "required": [
                        "enabled"
                      ],
                      "additionalProperties": false
                    },
                    "showCountsBeforeDrawing": {
                      "description": "Show applicants their ticket count on the confirmation and status page (default true). Turn off while rules are still changing.",
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                },
                "categories": {
                  "description": "Applicant categories, matched in order; an applicant lands in the first whose rule fits. Replaces the saved list when present. An empty list means one category: everyone.",
                  "maxItems": 20,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 80,
                        "description": "Category name as runners see it: \"Veteran\", \"Never\", \"Women\"."
                      },
                      "key": {
                        "type": "string",
                        "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$",
                        "description": "Stable lowercase key (letters, digits, - and _). Defaults to a slug of the name; keep it when renaming so saved history stays attached."
                      },
                      "who": {
                        "description": "The plain audience this category holds. Leave out when `rule` says who belongs.",
                        "type": "string",
                        "enum": [
                          "everyone",
                          "veterans",
                          "first_timers",
                          "women",
                          "men"
                        ]
                      },
                      "rule": {
                        "description": "A custom rule instead of `who`.",
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "everyone",
                              "finishes",
                              "starts",
                              "applications",
                              "selections",
                              "volunteer_years",
                              "gender",
                              "state",
                              "age"
                            ],
                            "description": "What the rule counts: finishes or starts of this race, prior applications, prior selections (drawn in an earlier year), volunteer_years of service, the applicant's gender, home state, or age on race day."
                          },
                          "min": {
                            "description": "Lowest count or age that qualifies (inclusive). Null or absent: no lower bound.",
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "max": {
                            "description": "Highest count or age that qualifies (inclusive). \"max 0\" on finishes means never finished.",
                            "anyOf": [
                              {
                                "type": "integer",
                                "minimum": 0,
                                "maximum": 9007199254740991
                              },
                              {
                                "type": "null"
                              }
                            ]
                          },
                          "values": {
                            "description": "For gender: \"female\", \"male\", \"other\", \"prefer_not_to_say\". For state: state codes or names.",
                            "maxItems": 80,
                            "type": "array",
                            "items": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 60
                            }
                          }
                        },
                        "required": [
                          "kind"
                        ],
                        "additionalProperties": false
                      },
                      "also": {
                        "description": "Extra conditions every member must also meet (AND), e.g. a \"Never\" category that also requires no prior selection.",
                        "maxItems": 5,
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string",
                              "enum": [
                                "everyone",
                                "finishes",
                                "starts",
                                "applications",
                                "selections",
                                "volunteer_years",
                                "gender",
                                "state",
                                "age"
                              ],
                              "description": "What the rule counts: finishes or starts of this race, prior applications, prior selections (drawn in an earlier year), volunteer_years of service, the applicant's gender, home state, or age on race day."
                            },
                            "min": {
                              "description": "Lowest count or age that qualifies (inclusive). Null or absent: no lower bound.",
                              "anyOf": [
                                {
                                  "type": "integer",
                                  "minimum": 0,
                                  "maximum": 9007199254740991
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "max": {
                              "description": "Highest count or age that qualifies (inclusive). \"max 0\" on finishes means never finished.",
                              "anyOf": [
                                {
                                  "type": "integer",
                                  "minimum": 0,
                                  "maximum": 9007199254740991
                                },
                                {
                                  "type": "null"
                                }
                              ]
                            },
                            "values": {
                              "description": "For gender: \"female\", \"male\", \"other\", \"prefer_not_to_say\". For state: state codes or names.",
                              "maxItems": 80,
                              "type": "array",
                              "items": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 60
                              }
                            }
                          },
                          "required": [
                            "kind"
                          ],
                          "additionalProperties": false,
                          "description": "A membership rule for applicants, used when the plain audience words do not fit."
                        }
                      },
                      "spots": {
                        "description": "Spots this category's own drawing fills before the general drawing (a guaranteed minimum). Null or absent: none guaranteed; its applicants are drawn in the general pool.",
                        "anyOf": [
                          {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 9007199254740991
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "share": {
                        "description": "True: draw the category at no less than its share of applicants, worked out at draw time (\"women get the same share they applied in\"). Ignores `spots`.",
                        "type": "boolean"
                      },
                      "cap": {
                        "description": "The most entries this category may hold after the general drawing. Null: no limit.",
                        "anyOf": [
                          {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 9007199254740991
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "rolloverUnfilled": {
                        "description": "Unfilled guaranteed spots roll into the general drawing (default true).",
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "name"
                    ],
                    "additionalProperties": false
                  }
                },
                "reservedEntries": {
                  "description": "Entries held out of the drawing (director picks, tribal entries, a charity raffle). Replaces the saved list when present.",
                  "maxItems": 20,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 80,
                        "description": "Who the entries are held for, in the organizer's words (e.g. \"Director picks\", \"Sponsor entries\", \"Charity raffle\")."
                      },
                      "key": {
                        "type": "string",
                        "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$",
                        "description": "Stable lowercase key (letters, digits, - and _). Defaults to a slug of the name; keep it when renaming so saved history stays attached."
                      },
                      "count": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10000,
                        "description": "How many entries are held out of the drawing for them. Reserved entries come off capacity before the drawing picks its spots."
                      },
                      "fill": {
                        "type": "string",
                        "enum": [
                          "manual",
                          "invite_code",
                          "separate_draw"
                        ],
                        "description": "How the spots are filled: \"manual\" (the organizer assigns each one), \"invite_code\" (an invitation code tied to this bucket admits them), \"separate_draw\" (a separate raffle or drawing picks them)."
                      },
                      "note": {
                        "description": "Who decides or why, shown to the organizer only.",
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 500
                          },
                          {
                            "type": "null"
                          }
                        ]
                      }
                    },
                    "required": [
                      "name",
                      "count",
                      "fill"
                    ],
                    "additionalProperties": false
                  }
                },
                "waitlist": {
                  "description": "Waitlist shape after the drawing.",
                  "type": "object",
                  "properties": {
                    "max": {
                      "description": "How many undrawn applicants are placed on the waitlist. Null (default): everyone not drawn, uncapped.",
                      "anyOf": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 9007199254740991
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "offerHours": {
                      "description": "Hours a waitlisted runner has to accept an offered spot, when different from the selection window.",
                      "anyOf": [
                        {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 720
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "offersCloseAt": {
                      "description": "After this local date-time no more waitlist offers go out.",
                      "anyOf": [
                        {
                          "type": "string",
                          "pattern": "^\\d{4}-\\d{2}-\\d{2}T(?:[01]\\d|2[0-3]):[0-5]\\d$",
                          "description": "A local date and time in the event's timezone, \"YYYY-MM-DDTHH:mm\" (2026-12-01T06:00 is 6:00 AM on December 1). Never an ISO instant with Z or an offset; EHQ converts it."
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "backfill": {
                      "description": "Who a released spot goes to first: \"gender\" (a woman's spot goes to the next woman waiting, a man's to the next man), \"pool\" (the next in the same category), \"order\" (the next on the waitlist, whoever it is).",
                      "type": "string",
                      "enum": [
                        "pool",
                        "gender",
                        "order"
                      ]
                    },
                    "publishApplicantList": {
                      "description": "Publish the applicant list (names and status) on the event website.",
                      "type": "boolean"
                    }
                  },
                  "additionalProperties": false
                },
                "selectionPayment": {
                  "description": "How a drawn runner pays.",
                  "type": "object",
                  "properties": {
                    "mode": {
                      "type": "string",
                      "enum": [
                        "respond",
                        "card_on_file"
                      ],
                      "description": "\"respond\": drawn runners pay through a deadline link. \"card_on_file\": a card saved at application is charged after the notice window."
                    },
                    "cardChargeNoticeHours": {
                      "description": "card_on_file only: hours between the selection notice and the charge (default 48).",
                      "type": "integer",
                      "minimum": 1,
                      "maximum": 336
                    }
                  },
                  "required": [
                    "mode"
                  ],
                  "additionalProperties": false
                }
              },
              "additionalProperties": false
            },
            {
              "type": "null"
            }
          ]
        },
        "qualifier": {
          "description": "The qualifying race runner requirement. Null removes it.",
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "name": {
                  "description": "Requirement name as runners see it (default \"Qualifying race\").",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 160
                },
                "description": {
                  "description": "Which races count, in prose, shown to applicants.",
                  "type": "string",
                  "maxLength": 8000
                },
                "windowStart": {
                  "description": "First date a qualifying race may fall on (inclusive). Null: open.",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A calendar date in the event's timezone, \"YYYY-MM-DD\"."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "windowEnd": {
                  "description": "Last date a qualifying race may fall on (inclusive). Null: open.",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A calendar date in the event's timezone, \"YYYY-MM-DD\"."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "verifiedOnly": {
                  "description": "True: only applications whose qualifier was verified (by an organizer or the automatic check) enter the drawing.",
                  "type": "boolean"
                },
                "autoVerify": {
                  "description": "True: an automatic \"meets\" finding from the standards check verifies the qualifier without review.",
                  "type": "boolean"
                },
                "standards": {
                  "description": "Checkable standards. Replaces the saved list when present.",
                  "maxItems": 12,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "distance",
                          "race",
                          "list"
                        ],
                        "description": "\"distance\": a course of at least a distance with gain and time bounds. \"race\": one named race. \"list\": a maintained list of races."
                      },
                      "minDistanceKm": {
                        "description": "distance: shortest course that counts, in kilometers (100K = 100, 100 miles = 160.9344).",
                        "type": "number",
                        "minimum": 0
                      },
                      "maxDistanceKm": {
                        "description": "distance: longest course that counts, in kilometers. Null: open.",
                        "anyOf": [
                          {
                            "type": "number",
                            "minimum": 0
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "minGainM": {
                        "description": "distance: least elevation gain, in meters (10,000 ft = 3048; 15,000 ft = 4572).",
                        "anyOf": [
                          {
                            "type": "number",
                            "minimum": 0
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "maxGainM": {
                        "description": "distance: most elevation gain, in meters. Null: open.",
                        "anyOf": [
                          {
                            "type": "number",
                            "minimum": 0
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "maxTimeSeconds": {
                        "description": "Finish time cap in seconds (20 hours = 72000). Null: finishing inside the race's own cutoff is enough.",
                        "anyOf": [
                          {
                            "type": "number",
                            "minimum": 0
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "raceName": {
                        "description": "race: the race's name (\"Copper Kings 100\").",
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 160
                      },
                      "crewableSlug": {
                        "description": "race: Crewable directory slug when known.",
                        "anyOf": [
                          {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "ehqEventId": {
                        "description": "race: Endurance HQ event id when the race is on the platform.",
                        "anyOf": [
                          {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 64
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "listId": {
                        "description": "list: the list id (\"western-states-100\").",
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 80
                      },
                      "label": {
                        "description": "The organizer's own words for this line, shown instead of the generated one.",
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 200
                          },
                          {
                            "type": "null"
                          }
                        ]
                      }
                    },
                    "required": [
                      "kind"
                    ],
                    "additionalProperties": false,
                    "description": "One checkable standard a qualifying race must meet."
                  }
                },
                "askedAt": {
                  "description": "When the qualifier is asked: \"during_registration\" (on the application; default) or \"after_registration\".",
                  "type": "string",
                  "enum": [
                    "during_registration",
                    "after_registration"
                  ]
                },
                "raceOptionIds": {
                  "description": "Only these distances (race option ids) ask it. Empty or absent: every distance.",
                  "maxItems": 50,
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  }
                }
              },
              "additionalProperties": false
            },
            {
              "type": "null"
            }
          ]
        },
        "trailWork": {
          "description": "The trail work runner requirement. Null removes it.",
          "anyOf": [
            {
              "type": "object",
              "properties": {
                "name": {
                  "description": "Requirement name as runners see it (default \"Trail work\").",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 160
                },
                "description": {
                  "description": "What counts and how to log it, in prose, shown to entrants.",
                  "type": "string",
                  "maxLength": 8000
                },
                "hours": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 100,
                  "description": "Hours each entrant owes (8)."
                },
                "dueDate": {
                  "description": "Local calendar date the hours are due by (end of that day in the event's timezone). Null: no due date.",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A calendar date in the event's timezone, \"YYYY-MM-DD\"."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "buyoutCents": {
                  "description": "Donation that satisfies the requirement instead of hours, in cents ($250 = 25000). Null: no buyout.",
                  "anyOf": [
                    {
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 9007199254740991,
                      "description": "Money in cents: $250 is 25000, free is 0."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "beneficiary": {
                  "description": "Where buyout donations go. Null clears it.",
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "organizationId": {
                          "description": "Saved donation organization id when known.",
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64
                        },
                        "name": {
                          "description": "Trail group that receives buyout donations, by name. Created on the event when it does not exist yet.",
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 160
                        },
                        "donationUrl": {
                          "description": "The group's own donation page, when donations are paid there.",
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "additionalProperties": false
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "workWindowStart": {
                  "description": "Earliest work date that counts (inclusive). Null: any date.",
                  "anyOf": [
                    {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                      "description": "A calendar date in the event's timezone, \"YYYY-MM-DD\"."
                    },
                    {
                      "type": "null"
                    }
                  ]
                },
                "accepts": {
                  "description": "The kinds of work that count toward the hours (default: all three).",
                  "type": "object",
                  "properties": {
                    "organizationDays": {
                      "type": "boolean",
                      "description": "Work on trail days the organization runs counts."
                    },
                    "outsideTrailWork": {
                      "type": "boolean",
                      "description": "Trail work with another group, confirmed by that group's coordinator, counts."
                    },
                    "outsideVolunteering": {
                      "type": "boolean",
                      "description": "Volunteering at another race, confirmed by its director, counts."
                    }
                  },
                  "required": [
                    "organizationDays",
                    "outsideTrailWork",
                    "outsideVolunteering"
                  ],
                  "additionalProperties": false
                },
                "organizationDays": {
                  "description": "Trail days the organization runs. Replaces the saved list when present.",
                  "maxItems": 50,
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "description": "Stable id; defaults to a slug of the date and name.",
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 64
                      },
                      "date": {
                        "type": "string",
                        "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                        "description": "A calendar date in the event's timezone, \"YYYY-MM-DD\"."
                      },
                      "name": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 120,
                        "description": "What the day is called."
                      },
                      "location": {
                        "anyOf": [
                          {
                            "type": "string",
                            "maxLength": 160
                          },
                          {
                            "type": "null"
                          }
                        ]
                      },
                      "hours": {
                        "description": "Hours a full day counts for; the runner can log fewer.",
                        "anyOf": [
                          {
                            "type": "number",
                            "minimum": 0.25,
                            "maximum": 24
                          },
                          {
                            "type": "null"
                          }
                        ]
                      }
                    },
                    "required": [
                      "date",
                      "name"
                    ],
                    "additionalProperties": false
                  }
                },
                "directorReview": {
                  "description": "True: a coordinator's confirmation still waits for the director before the hours count (default false).",
                  "type": "boolean"
                },
                "deadline": {
                  "description": "What happens to entrants who have not done the hours by the due date.",
                  "type": "object",
                  "properties": {
                    "action": {
                      "type": "string",
                      "enum": [
                        "flag",
                        "release_spot"
                      ],
                      "description": "At the due date: \"flag\" only lists overdue entrants for the director; \"release_spot\" drops them from the field so the spot goes to the waitlist."
                    },
                    "graceDays": {
                      "description": "Days after the due date before the action runs (default 0).",
                      "type": "integer",
                      "minimum": 0,
                      "maximum": 60
                    },
                    "requires": {
                      "description": "What counts as done at the deadline: \"logged\" (enough hours logged, verification may still be pending) or \"verified\" (hours verified).",
                      "type": "string",
                      "enum": [
                        "logged",
                        "verified"
                      ]
                    },
                    "notifyRunner": {
                      "description": "Tell the runner when the action runs. False drops them silently; the scheduled reminders still go out beforehand.",
                      "type": "boolean"
                    },
                    "lateAdmitDays": {
                      "description": "Days an entrant admitted after the due date gets, counted from their admission, before the deadline action applies to them. 0 or null: the next run treats them like everyone else.",
                      "anyOf": [
                        {
                          "type": "integer",
                          "minimum": 0,
                          "maximum": 365
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "required": [
                    "action"
                  ],
                  "additionalProperties": false
                },
                "showVerifiedOnApplicantList": {
                  "description": "Show a \"Trail work verified\" check beside accepted entrants on the public list (default false).",
                  "type": "boolean"
                },
                "reminders": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean",
                      "description": "Send the shared requirement reminders."
                    },
                    "daysBefore": {
                      "description": "Days before the due date a reminder goes out (default 30, 14, 7, 1).",
                      "maxItems": 12,
                      "type": "array",
                      "items": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 365
                      }
                    }
                  },
                  "required": [
                    "enabled"
                  ],
                  "additionalProperties": false
                },
                "raceOptionIds": {
                  "description": "Only entrants in these distances owe it. Empty or absent: every distance.",
                  "maxItems": 50,
                  "type": "array",
                  "items": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  }
                }
              },
              "required": [
                "hours"
              ],
              "additionalProperties": false
            },
            {
              "type": "null"
            }
          ]
        },
        "questions": {
          "description": "Registration questions, matched to saved ones by label. Saved questions not listed are left alone.",
          "maxItems": 100,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "The question as the form asks it (\"Would you like a pacer?\"). Matches a saved question by label."
              },
              "type": {
                "description": "\"checkbox\" for a yes/no, \"select\" one of the options, \"multiselect\" several, \"number\", \"textarea\" for long text, \"text\" otherwise (default).",
                "type": "string",
                "enum": [
                  "text",
                  "select",
                  "multiselect",
                  "checkbox",
                  "number",
                  "textarea"
                ]
              },
              "options": {
                "description": "Answer choices for select and multiselect.",
                "anyOf": [
                  {
                    "maxItems": 50,
                    "type": "array",
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 200
                    }
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "required": {
                "description": "Whether the runner must answer (default false).",
                "type": "boolean"
              },
              "askedAt": {
                "description": "\"during_registration\" asks at checkout or on the application (default); \"after_registration\" asks in the event app afterwards.",
                "type": "string",
                "enum": [
                  "during_registration",
                  "after_registration"
                ]
              },
              "visibility": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "minAgeOnRaceDay": {
                        "description": "Ask only runners at least this old on race day (65 for a 65+ pacer request).",
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 130
                      },
                      "maxAgeOnRaceDay": {
                        "description": "Ask only runners at most this old on race day.",
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 130
                      },
                      "raceOptionIds": {
                        "description": "Ask only on these distances (race option ids).",
                        "maxItems": 100,
                        "type": "array",
                        "items": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 64
                        }
                      },
                      "answerEquals": {
                        "description": "Ask only when another question has this answer.",
                        "type": "object",
                        "properties": {
                          "questionLabel": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 200,
                            "description": "The other question's label."
                          },
                          "value": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 500,
                            "description": "The answer that reveals this question (\"true\" for a checked box)."
                          }
                        },
                        "required": [
                          "questionLabel",
                          "value"
                        ],
                        "additionalProperties": false
                      }
                    },
                    "additionalProperties": false,
                    "description": "Who is asked; every rule present must hold. Absent or null: everyone."
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "autoFillSource": {
                "description": "Prefill a number question from EHQ data (volunteer years of service). Answers are always organizer-only; nothing a runner answers here is published.",
                "anyOf": [
                  {
                    "type": "string",
                    "enum": [
                      "volunteer_years_of_service"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "remove": {
                "description": "True removes the saved question with this label.",
                "type": "boolean"
              }
            },
            "required": [
              "label"
            ],
            "additionalProperties": false
          }
        },
        "addOns": {
          "description": "Things for sale at registration, matched to saved add-ons by title.",
          "maxItems": 50,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "What is for sale at registration (\"Camping spot, church lot\"). Matches a saved add-on by title."
              },
              "description": {
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 2000
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "priceCents": {
                "anyOf": [
                  {
                    "type": "integer",
                    "minimum": 0,
                    "maximum": 9007199254740991,
                    "description": "Money in cents: $250 is 25000, free is 0."
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Price per unit in cents. Null when the price isn't decided yet: applying asks the organizer for it as a quick answer and puts the add-on on sale once they reply."
              },
              "kind": {
                "description": "\"service\" for a spot, reservation or seat (default); \"merch\" for a physical item.",
                "type": "string",
                "enum": [
                  "service",
                  "merch"
                ]
              },
              "inventory": {
                "description": "Stock for this add-on. Null: unlimited.",
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "total": {
                        "anyOf": [
                          {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 9007199254740991
                          },
                          {
                            "type": "null"
                          }
                        ],
                        "description": "Units for sale in total (7 camping spots). EHQ counts what is left as runners buy and shows when it is full. Null: not tracked."
                      }
                    },
                    "required": [
                      "total"
                    ],
                    "additionalProperties": false
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "namedSpots": {
                "description": "Each unit is a specific spot the organizer assigns (camping sites), with live availability on the form.",
                "type": "boolean"
              },
              "salesChannel": {
                "description": "Where it is offered: \"registration_only\" at registration (default), \"store_and_registration\" in the store as well, \"registered_runners_only\" in the event app shop to runners holding a confirmed entry (camping bought after a lottery acceptance), never on the public form.",
                "type": "string",
                "enum": [
                  "registration_only",
                  "store_and_registration",
                  "registered_runners_only"
                ]
              },
              "published": {
                "description": "Live for sale (default true). False keeps it a draft.",
                "type": "boolean"
              },
              "remove": {
                "description": "True archives the saved add-on with this title.",
                "type": "boolean"
              }
            },
            "required": [
              "title",
              "priceCents"
            ],
            "additionalProperties": false
          }
        },
        "volunteerPositions": {
          "description": "Volunteer roles, matched by name.",
          "maxItems": 100,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 160,
                "description": "Role name (\"Aid station captain\", \"Ham radio operator\"). Matches a saved role by name."
              },
              "description": {
                "description": "What the role does, shown to volunteers.",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 2000
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "category": {
                "description": "What the role is, independent of its name, so captain-only audiences and station cards find it.",
                "anyOf": [
                  {
                    "type": "string",
                    "enum": [
                      "pre_race_course_check",
                      "aid_station_captain",
                      "aid_station_team",
                      "course_monitor",
                      "check_in",
                      "video_content",
                      "finish_area",
                      "course_sweep",
                      "setup_teardown",
                      "parking",
                      "medical",
                      "radio",
                      "course_marking",
                      "other"
                    ]
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "active": {
                "description": "False hides the role from signups without deleting it.",
                "type": "boolean"
              },
              "communicationPolicy": {
                "description": "Whether people in this role receive event-wide volunteer messages.",
                "type": "string",
                "enum": [
                  "inherit",
                  "include",
                  "exclude"
                ]
              }
            },
            "required": [
              "name"
            ],
            "additionalProperties": false
          }
        },
        "automations": {
          "description": "Recurring or anchored messages, matched by name.",
          "maxItems": 50,
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 120,
                "description": "Automation name (\"Trail work reminder\"). Matches a saved custom automation by name."
              },
              "anchor": {
                "type": "string",
                "enum": [
                  "event_date",
                  "registration",
                  "calendar_date"
                ],
                "description": "What the send time counts from: \"event_date\" (the race date), \"registration\" (each runner's registration), or \"calendar_date\" (a fixed month and day every year)."
              },
              "offsetDays": {
                "description": "Days relative to the anchor; negative is before (event_date and -7 sends a week before the race). Registration sends at or after registration only.",
                "type": "integer",
                "minimum": -365,
                "maximum": 365
              },
              "offsetMinutes": {
                "description": "Minutes relative to the anchor when days are too coarse. Added to offsetDays.",
                "type": "integer",
                "minimum": -525600,
                "maximum": 525600
              },
              "calendarDate": {
                "description": "calendar_date only: the date each year (April 20 for an annual trail work reminder).",
                "type": "object",
                "properties": {
                  "month": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 12,
                    "description": "Month, 1 to 12."
                  },
                  "day": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 31,
                    "description": "Day of the month."
                  },
                  "time": {
                    "description": "Local send time \"HH:mm\" (default 09:00).",
                    "type": "string",
                    "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$"
                  }
                },
                "required": [
                  "month",
                  "day"
                ],
                "additionalProperties": false
              },
              "keepOnEachEdition": {
                "description": "Carry this automation to each new edition of the race so it is set once and lightly edited each year.",
                "type": "boolean"
              },
              "audience": {
                "description": "Who receives it (default participants).",
                "type": "string",
                "enum": [
                  "participants",
                  "volunteers"
                ]
              },
              "segment": {
                "description": "A saved smart segment key instead of the built-in audience.",
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              "list": {
                "description": "A saved contact list id instead of the built-in audience.",
                "type": "string",
                "minLength": 1,
                "maxLength": 120
              },
              "channels": {
                "description": "Delivery channels (default email).",
                "minItems": 1,
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "email",
                    "sms",
                    "push"
                  ]
                }
              },
              "subject": {
                "type": "string",
                "minLength": 1,
                "maxLength": 300,
                "description": "Subject line."
              },
              "body": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100000,
                "description": "Message body."
              },
              "reviewMode": {
                "description": "\"auto\" sends on schedule; \"notify\" tells the organizer first; \"hold\" waits for the organizer to release it (default hold for event-date sends).",
                "type": "string",
                "enum": [
                  "auto",
                  "notify",
                  "hold"
                ]
              },
              "reviewNoticeDays": {
                "description": "Days before the send the review notice goes out (default 3).",
                "type": "integer",
                "minimum": 1,
                "maximum": 30
              },
              "active": {
                "description": "Turned on (default false). Activating schedules real sends.",
                "type": "boolean"
              },
              "remove": {
                "description": "True turns the saved automation with this name off (sends stop; history is kept).",
                "type": "boolean"
              }
            },
            "required": [
              "name",
              "anchor",
              "subject",
              "body"
            ],
            "additionalProperties": false
          }
        },
        "notes": {
          "description": "Open decisions to flag for the organizer rather than guess (\"Veteran ticket per finish still undecided\").",
          "maxItems": 50,
          "type": "array",
          "items": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          }
        }
      },
      "additionalProperties": false,
      "description": "A race's registration rules in plain terms. Absent sections are left as they are."
    }
  },
  "required": [
    "ruleset"
  ],
  "additionalProperties": false
}

Response

Success status: 201

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "data": {
      "type": "object",
      "properties": {
        "planId": {
          "type": "string"
        },
        "title": {
          "type": "string"
        },
        "summary": {
          "type": "string"
        },
        "risk": {
          "type": "string",
          "enum": [
            "low",
            "medium",
            "high"
          ]
        },
        "fidelity": {
          "type": "string",
          "enum": [
            "kind_module",
            "raw_input"
          ]
        },
        "diff": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "field": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "label": {
                "type": "string"
              },
              "before": {},
              "after": {},
              "beforeText": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "afterText": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "field",
              "label",
              "before",
              "after",
              "beforeText",
              "afterText"
            ],
            "additionalProperties": false
          }
        },
        "effects": {
          "type": "array",
          "items": {
            "type": "string"
          }
        },
        "reviewHref": {
          "type": "string"
        },
        "expiresAt": {
          "type": "string",
          "format": "date-time",
          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
        }
      },
      "required": [
        "planId",
        "title",
        "summary",
        "risk",
        "fidelity",
        "diff",
        "effects",
        "reviewHref",
        "expiresAt"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "data"
  ],
  "additionalProperties": false
}

Other surfaces

CLI: ehq registration rules propose --event <event_id> --input-file <ruleset.json>

MCP tool: ehq_registration_rules_propose