Registration
Draft a ruleset from prose or a web page
Read-only: turn pasted prose (call notes, an email, a rules page copied from a website) or a public URL into a registration ruleset draft, with a summary, the open decisions the source leaves (notes) and anything it stated that could not be placed (warnings). Nothing is saved. Review the draft with the organizer, correct it, then send it to ehq_registration_rules_propose. Send an Idempotency-Key header.
Method
POST
Path
/v1/events/{eventId}/registration-rules/extract
Required scope
capabilities:readRequest
curl -X POST "https://api.endurancehq.app/v1/events/{eventId}/registration-rules/extract" \
-H "Authorization: Bearer $EHQ_API_KEY" \
-H "Idempotency-Key: $(uuidgen)"Path parameters
eventIdstring, requiredJSON body schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"text": {
"description": "Pasted prose: call notes, an email, a rules page copied from a website.",
"type": "string",
"minLength": 1,
"maxLength": 300000
},
"url": {
"description": "A public page to read the rules from (the race's lottery or registration page).",
"type": "string",
"format": "uri"
}
},
"additionalProperties": false
}Response
Success status: 200
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"data": {
"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."
},
"summary": {
"type": "string"
},
"warnings": {
"type": "array",
"items": {
"type": "string"
}
},
"sourceUrl": {
"type": [
"string",
"null"
]
}
},
"required": [
"ruleset",
"summary",
"warnings",
"sourceUrl"
],
"additionalProperties": false
}
},
"required": [
"data"
],
"additionalProperties": false
}Other surfaces
CLI: ehq registration rules extract --event <event_id> [--text-file <notes.txt>] [--url <page>]
MCP tool: ehq_registration_rules_extract