{"openapi":"3.1.0","info":{"title":"DragonBoat Hub Race Schedule API","version":"1.0.0","summary":"Read and write a regatta's running order, lane draw and results.","description":"Lets timing and regatta-management software drive a race schedule hosted on DragonBoat Hub.\n\n**Concurrency.** Writes are last-write-wins. There is no version check, no `If-Match`, and no `409` on a concurrent edit — if a manager edits the schedule in the app while your client pushes a stale copy, your copy wins silently. Push what changed, not what you last read.\n\n**Deletes are narrow.** The one destructive operation is `DELETE` on a single race, and it reaches only races addressed by an `externalId` — that is, ones your sync created or adopted. A race a manager built by hand in the app carries no `externalId` and cannot be reached from here, and neither can a team or a schedule. A misconfigured client can tidy up after itself; it cannot destroy their work.\n\n**Your ids, not ours.** Every race and team is addressed by an `externalId` you choose. Internal identifiers appear in no request body. A repeated push is a no-op rather than a duplicate, so retrying after a timeout is always safe.\n\n**Adoption.** When an `externalId` is unknown, the API looks for an existing row with the same natural key — `raceNumber` for a race, `name` for a team — and claims it, stamping your id onto it. This is what lets you push crews into an event whose teams were already seeded from club registrations, instead of colliding with them.\n\n**Adoption claims only unclaimed rows.** A row already carrying a *different* `externalId` is one of your other records, and taking it over would silently merge the two, so it is never adopted. Your row is treated as new instead, and then collides: a crew whose name is already held under another `externalId` fails the unique name index, and a race whose number is already held fails the race-number rule. Both come back as `409`. To rename or renumber, push the new value under the `externalId` that already owns the row.\n\n**Start at `GET /v1/me` and `GET /v1/events`.** The first says whose key you are holding and is the connection test — it is the only call that tells a bad credential apart from a wrong event id. The second lists the regattas that key may write to, so nobody has to read an event id out of a browser URL. Both spend the read budget like any other call.\n\n**No personal data.** Race structure, teams, lanes, times, places and statuses only. Paddler names and seat plans are not exposed and will not be added to this version.","contact":{"name":"DragonBoat Hub","url":"https://dragonboathub.de/developers","email":"flechtner@robocitrus.com"}},"servers":[{"url":"https://data.dragonboathub.de/v1","description":"DragonBoat Hub public API"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Discovery","description":"Whose key this is, and what it may write to."},{"name":"Schedule","description":"The regatta's running order."},{"name":"Teams","description":"The crews competing at the event."},{"name":"Races","description":"Heats, finals and their timing."},{"name":"Results","description":"Lane draw and finish."}],"paths":{"/me":{"get":{"tags":["Discovery"],"operationId":"getIdentity","summary":"Who this key belongs to","description":"Confirms the key works and names the person behind it, without touching an event. Use it for a connection test: every other route answers `404` for an event you may not see, so a bad key and a wrong event id are otherwise indistinguishable.\n\nNo e-mail address is returned, here or anywhere else. `displayName` is the name its owner set in the app, and is meant for showing which account a desktop client is connected as.","responses":{"200":{"description":"The key and its owner.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Identity"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events":{"get":{"tags":["Discovery"],"operationId":"listEvents","summary":"The events this key may manage","description":"Every event whose race schedule the key's owner may manage — the ones their club role covers, plus any they created themselves. Ordered by `startDate`, newest first, and capped at 200 with no pagination.\n\nPast events are included on purpose: results get typed up days later and a running order gets corrected after the fact.\n\nAn owner who may manage nothing gets an empty list and a `200`, never a `403`. Permissions are re-checked on every request, so an event can leave this list without your key changing.","responses":{"200":{"description":"The manageable events, newest first. Possibly empty.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/schedule":{"parameters":[{"$ref":"#/components/parameters/EventId"}],"get":{"tags":["Schedule"],"operationId":"getSchedule","summary":"Read the whole running order","description":"Returns the schedule with every race and lane, including draft schedules and unmasked finish times. `resultVisibility` reports what the club configured for public display — respect it if you render a spectator-facing scoreboard.","responses":{"200":{"description":"The schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Schedule"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Schedule"],"operationId":"createSchedule","summary":"Create the schedule if the event has none","description":"Idempotent. Returns `201` with a fresh schedule, or `200` with the one that already exists — retrying after a timeout is safe.\n\nAll three settings default, because the app asks about them deliberately and a machine caller answers none of them: no lane scheme, `timed` results, `full` visibility. Send them explicitly if the venue is not a lane course.","requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleCreate"},"example":{"laneScheme":["1","2","3","4","5","6"],"defaultResultMode":"timed","defaultResultVisibility":"full"}}}},"responses":{"200":{"description":"The schedule already existed and is returned unchanged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleSummary"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/schedule/publish":{"parameters":[{"$ref":"#/components/parameters/EventId"}],"post":{"tags":["Schedule"],"operationId":"publishSchedule","summary":"Make the running order visible to the crews","description":"`POST /schedule` creates a **draft**, which only the event's managers can see. Until you publish, a complete regatta — every race, every lane, every result — reaches nobody following the event.\n\nIdempotent: publishing an already-published schedule is a `200`, not an error. `404` if the event has no schedule yet; create one first.\n\nThere is no un-publish. Taking a running order back off the crews' phones stays a human action in the app.","responses":{"200":{"description":"The published schedule.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/schedule/delay":{"parameters":[{"$ref":"#/components/parameters/EventId"}],"put":{"tags":["Schedule"],"operationId":"setRaceDayDelay","summary":"Report that a race day is running late","description":"Sets the offset for **one race day**, so every race of that day which has not been scored is re-estimated at `scheduledAt + delayMinutes`.\n\nPer day and not per schedule: a regatta spans several days and each slips on its own. `venueDay` is `YYYY-MM-DD` in the venue's zone — the same key the app groups races by, and deliberately not derived from a UTC date, which lands on the wrong day across a DST boundary.\n\nSigned, because a day can also run early. **Zero clears the offset** rather than storing a zero.\n\nThis is the one write here that reaches people within the minute: the schedule-delayed push goes to the crews following that day. Setting it on a regatta that has already finished is silent all the same — the notification task only looks at a 30-hour window.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DelayWrite"}}}},"responses":{"200":{"description":"Every day of the schedule, with its offset.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RaceDayList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/teams":{"parameters":[{"$ref":"#/components/parameters/EventId"}],"get":{"tags":["Teams"],"operationId":"listTeams","summary":"List the crews at this event","responses":{"200":{"description":"The crew list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"put":{"tags":["Teams"],"operationId":"upsertTeams","summary":"Create or update crews","description":"Matches on `externalId`, then adopts an existing crew with the same `name` — but only one that carries no `externalId` of its own. Teams seeded from club registrations carry none, so your first push claims them rather than failing on the unique name index. A crew already held under a different `externalId` is never adopted: your row is treated as new, hits `UNIQUE (eventId, name)`, and comes back as a `409`. Renaming means pushing the new name under the `externalId` that already owns it.\n\n`clubId`, `boatIndex` and `sortOrder` are preserved when omitted: they tie a crew to a club on this platform and to the registration it came from, and a caller that has never heard of either should not erase both by pushing a name.\n\nEach returned crew carries an `outcome` telling you which of the three paths it took.\n\nTwo failures to tell apart: the same `externalId` twice in one payload is a `422` with `field` set to `teams[i].externalId`, and a name already held under a different `externalId` is a `409` with no `field`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/TeamWrite"}},{"type":"object","properties":{"teams":{"type":"array","items":{"$ref":"#/components/schemas/TeamWrite"}}},"required":["teams"]}]},"example":{"teams":[{"externalId":"crew-101","name":"RC Hansa I"},{"externalId":"crew-102","name":"RC Hansa II","boatIndex":2}]}}}},"responses":{"200":{"description":"The crew list after the write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamListWithOutcome"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/races":{"parameters":[{"$ref":"#/components/parameters/EventId"}],"put":{"tags":["Races"],"operationId":"upsertRaces","summary":"Create or update races","description":"One request, one transaction. Race numbers must be unique across the whole schedule once your payload is applied — not just within the payload — and a violation rejects the entire request rather than leaving a half-written running order.\n\nAdoption matches on `raceNumber`, the one identifier a schedule built by hand and a schedule pushed by software agree on — but only for a race that carries no `externalId` yet. A race already held under a different `externalId` is never adopted; your row is treated as new and then collides on the race number, which is a `409`. To renumber, push the new number under the `externalId` that already owns the race.\n\nOmitted fields are preserved on an existing race. `scheduledAt` is required when creating one and must carry an explicit UTC offset — a naive timestamp is rejected rather than guessed at.\n\nA race number repeated *within* your payload is a `422` naming `races[i].raceNumber`. One that collides with a race already in the schedule but absent from your payload is a `409` with no `field` — `detail` names the number, and you will have to find it yourself.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/RaceWrite"}},{"type":"object","properties":{"races":{"type":"array","items":{"$ref":"#/components/schemas/RaceWrite"}}},"required":["races"]}]},"example":{"races":[{"externalId":"heat-12","raceNumber":12,"title":"Mixed 200m Heat","scheduledAt":"2026-05-16T12:20:00Z","distanceMeters":200,"boatClass":"Mixed","round":"Heat"}]}}}},"responses":{"200":{"description":"The running order after the write.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RaceListWithOutcome"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/races/{externalId}":{"parameters":[{"$ref":"#/components/parameters/EventId"},{"$ref":"#/components/parameters/RaceExternalId"}],"delete":{"tags":["Races"],"operationId":"deleteRace","summary":"Remove one race","description":"Deletes a race **your own sync created**, together with its lanes.\n\nA race is addressable here only by an `externalId`, and a race a manager built by hand in the app carries none. So this reaches exactly what you pushed and cannot touch their running order — which is why a `DELETE` can exist here at all when nothing else in this API destroys a row.\n\n**Idempotent.** An `externalId` this schedule does not carry answers `204`, not `404`: a delete retried after a timeout must not report a different outcome from the one that succeeded.\n\nAny lane elsewhere fed by a progression rule pointing at this race is emptied rather than left dangling, and the races that lost a crew that way are re-ranked.","responses":{"204":{"description":"The race is gone, or was never here."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/races/{externalId}/entries":{"parameters":[{"$ref":"#/components/parameters/EventId"},{"$ref":"#/components/parameters/RaceExternalId"}],"put":{"tags":["Results"],"operationId":"setEntries","summary":"Set the lane draw for one race","description":"Send the complete draw. A lane with no `teamExternalId` is emptied but kept, so the race keeps its start-position structure.\n\nThe crew must already exist — push to `/teams` first. `positionLabel` defaults to the race's lane scheme, then the schedule's, then the position number.\n\nSeat plans and progression rules cannot be set here. Both belong to the app, and a caller that forgot to echo them back would otherwise erase a lineup by re-pushing a draw.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/LaneWrite"}},{"type":"object","properties":{"lanes":{"type":"array","items":{"$ref":"#/components/schemas/LaneWrite"}}},"required":["lanes"]}]},"example":{"lanes":[{"position":1,"teamExternalId":"crew-101"},{"position":2,"teamExternalId":"crew-102"},{"position":3}]}}}},"responses":{"200":{"description":"The race with its lanes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Race"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/events/{eventId}/races/{externalId}/results":{"parameters":[{"$ref":"#/components/parameters/EventId"},{"$ref":"#/components/parameters/RaceExternalId"}],"put":{"tags":["Results"],"operationId":"setResults","summary":"Post the finish for one race","description":"Send every lane you have a result for in a single call. A place is a statement about the whole field — a time in lane 5 can demote lanes 1 through 4 — so posting lanes one at a time makes every intermediate state a ranking nobody entered, visible to everyone reading the live schedule.\n\nIn a `timed` race, places are derived from the times: send `finishTimeMillis` and leave `finishPlace` alone. In a `placesOnly` race, send `finishPlace` and no time. Mixing them is rejected rather than silently dropped.\n\n`status` defaults to `finished`. Use `dns`, `dnf` or `dsq` where they apply.\n\nA result contradicting the race's mode is a `422` with `field` set to `results` — the whole list, not the offending lane. The rule belongs to the race, so there is no one element to blame.\n\nPosting a result for a race scheduled near the current time notifies the crews following it, within about a minute. Backfilling a finished regatta does not.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ResultWrite"}},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ResultWrite"}}},"required":["results"]}]},"example":{"results":[{"position":1,"finishTimeMillis":51230},{"position":2,"finishTimeMillis":52880},{"position":3,"status":"dnf"}]}}}},"responses":{"200":{"description":"The race with its results and derived places.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Race"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key minted in the DragonBoat Hub app under Account settings → API keys. Send it as `Authorization: Bearer dbh_live_…`.\n\nKeys belong to a person, not to a club or a machine, and carry exactly that person's permissions — re-checked on every request. If they lose the race-schedule role or leave the club, the key stops working for it with no revocation step.\n\nKeys are shown once at creation and cannot be recovered. There is no CORS: this API is not callable from a browser, and a key placed in front-end JavaScript is a key you have published."}},"parameters":{"EventId":{"name":"eventId","in":"path","required":true,"description":"The DragonBoat Hub event id, as returned by `GET /v1/events`. It is also the number in the app's event URL, if you would rather read it off by hand.","schema":{"type":"integer","format":"int64"}},"RaceExternalId":{"name":"externalId","in":"path","required":true,"description":"Your own identifier for the race, as sent to `PUT /races`. Percent-encode it if it contains slashes or spaces.","schema":{"type":"string"}}},"responses":{"Unauthorized":{"description":"Missing, malformed, unknown or revoked key.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"NotFound":{"description":"The event, schedule or race does not exist — or is not visible to this key's owner. The two are deliberately indistinguishable: an id you may not see should not be confirmable as existing.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Conflict":{"description":"The request is well-formed but contradicts stored state — a race number or crew name already held under another `externalId`, a crew already drawn into another lane of the same race.\n\nA `409` never carries `field`. `detail` is prose naming the offending value, so a client can quote it to a human but cannot mark one element of the payload as the culprit. Nothing was written: these are rejected whole.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"ValidationFailed":{"description":"The payload is well-formed JSON but wrong. `field` names the offending property, including its index in a list.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"RateLimited":{"description":"600 reads or 120 writes per minute per key, as a sliding window. `Retry-After` and the `X-RateLimit-*` headers say when to come back. Note that a 429 counts against your own window: a client ignoring `Retry-After` keeps itself locked out.","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"schemas":{"Identity":{"type":"object","description":"Whose key this is. Carries no e-mail address.","properties":{"userId":{"type":"string","description":"Stable identifier of the key's owner. Use it to tell two accounts apart, not to address anything else in this API."},"displayName":{"type":"string","description":"The name its owner set in the app. Prose, meant for showing which account you are connected as — it is not unique and may change."},"keyLabel":{"type":"string","description":"The label given when the key was created, e.g. «Photo-Finish Laptop». Matches what its owner sees in the app's key list."},"keyPrefix":{"type":"string","description":"The first characters of the key, in the same `dbh_live_a1b2c3d4` form the app shows. Enough to tell five keys apart; useless for authenticating."}},"required":["userId","displayName","keyLabel","keyPrefix"]},"EventSummary":{"type":"object","description":"Event identity and venue. No registrations, no clubs, no crews.","properties":{"id":{"type":"integer","format":"int64","description":"The `eventId` every other route takes."},"name":{"type":"string"},"startDate":{"type":"string","format":"date","description":"Calendar day at the **venue**, not in UTC. A 01:30 start in Berlin falls on the previous UTC day."},"endDate":{"type":"string","format":"date","description":"Venue calendar day of the event's end. Equal to `startDate` for a single-day regatta."},"timezone":{"type":["string","null"],"description":"IANA zone of the venue, e.g. `Europe/Berlin`. Null when it was never derived; treat the dates as already local in that case."},"city":{"type":["string","null"],"description":"Read back out of the venue's free-text address, because there is no column for it. Best-effort and null when the address yields nothing — show it, never group or filter on it."},"countryCode":{"type":["string","null"],"description":"ISO 3166-1 alpha-2, derived from the same address, with the same caveat."},"hasSchedule":{"type":"boolean","description":"False means `POST /events/{eventId}/schedule` first; everything else 404s until it exists."},"scheduleStatus":{"type":["string","null"],"enum":["draft","published",null],"description":"Null when there is no schedule. `draft` means the crews cannot see it yet — finish with `POST /events/{eventId}/schedule/publish`."}},"required":["id","name","startDate","endDate","hasSchedule"]},"EventList":{"type":"object","properties":{"events":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/EventSummary"}}},"required":["events"]},"Problem":{"type":"object","description":"RFC 9457 problem detail. Branch on `code`, which is stable; `title` and `detail` are prose and may be reworded.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"code":{"type":"string","enum":["missing_credentials","invalid_credentials","forbidden","event_not_found","schedule_not_found","race_not_found","route_not_found","method_not_allowed","payload_too_large","malformed_json","validation_failed","conflict","rate_limited","internal_error"]},"field":{"type":["string","null"],"description":"Set on `validation_failed` only."}},"required":["type","title","status","code"]},"ResultMode":{"type":"string","enum":["timed","placesOnly"],"description":"`timed` derives places from finish times. `placesOnly` means the organiser numbers the field by hand and no times are stored."},"ResultVisibility":{"type":"string","enum":["hidden","placesOnly","full"],"description":"What the club wants shown publicly. The API always returns full data regardless; honour this if you render a spectator-facing view."},"EntryStatus":{"type":"string","enum":["assigned","finished","dns","dnf","dsq"]},"ScheduleCreate":{"type":"object","properties":{"laneScheme":{"type":["array","null"],"items":{"type":"string"},"description":"Ordered start-position labels, e.g. `[\"1\",\"2\",\"3\"]` or `[\"Window\",\"Wall\"]`. Null or absent means the venue has no lane structure."},"defaultResultMode":{"$ref":"#/components/schemas/ResultMode"},"defaultResultVisibility":{"$ref":"#/components/schemas/ResultVisibility"}}},"ScheduleSummary":{"type":"object","properties":{"eventId":{"type":"integer"},"status":{"type":"string","enum":["draft","published"]},"laneScheme":{"type":["array","null"],"items":{"type":"string"}},"defaultResultMode":{"$ref":"#/components/schemas/ResultMode"},"defaultResultVisibility":{"$ref":"#/components/schemas/ResultVisibility"},"updatedAt":{"type":"string","format":"date-time"}}},"Schedule":{"allOf":[{"$ref":"#/components/schemas/ScheduleSummary"},{"type":"object","properties":{"eventName":{"type":"string"},"timezone":{"type":["string","null"],"description":"IANA zone of the venue. All timestamps are UTC; this is how the app renders local start times and groups races into days."},"days":{"type":"array","description":"The race days carrying a delay, so a client can render what a crew actually sees. Only days with an offset appear; an absent day runs to plan.","items":{"$ref":"#/components/schemas/RaceDay"}},"races":{"type":"array","items":{"$ref":"#/components/schemas/Race"}}}}]},"Race":{"type":"object","properties":{"externalId":{"type":["string","null"]},"raceNumber":{"type":"integer"},"title":{"type":["string","null"]},"scheduledAt":{"type":"string","format":"date-time"},"distanceMeters":{"type":["integer","null"]},"boatClass":{"type":["string","null"]},"round":{"type":["string","null"]},"divisionLabel":{"type":["string","null"]},"resultMode":{"$ref":"#/components/schemas/ResultMode"},"resultVisibility":{"oneOf":[{"$ref":"#/components/schemas/ResultVisibility"},{"type":"null"}],"description":"Null inherits the schedule default."},"laneScheme":{"type":["array","null"],"items":{"type":"string"}},"notes":{"type":["string","null"]},"lanes":{"type":"array","items":{"$ref":"#/components/schemas/Lane"}}}},"Lane":{"type":"object","properties":{"position":{"type":"integer"},"positionLabel":{"type":"string"},"teamExternalId":{"type":["string","null"]},"teamName":{"type":["string","null"]},"status":{"$ref":"#/components/schemas/EntryStatus"},"finishTimeMillis":{"type":["integer","null"]},"finishPlace":{"type":["integer","null"]},"penaltyMillis":{"type":["integer","null"]}}},"DelayWrite":{"type":"object","required":["venueDay","delayMinutes"],"properties":{"venueDay":{"type":"string","description":"`YYYY-MM-DD` in the venue's zone. Must be a day the schedule actually has races on.","maxLength":10},"delayMinutes":{"type":"integer","minimum":-720,"maximum":1440,"description":"Signed. Zero clears the offset for that day."}}},"RaceDay":{"type":"object","properties":{"venueDay":{"type":"string"},"delayMinutes":{"type":"integer"},"anchorRaceNumber":{"type":["integer","null"]},"anchorSource":{"type":"string"}}},"RaceDayList":{"type":"object","properties":{"days":{"type":"array","items":{"$ref":"#/components/schemas/RaceDay"}}}},"RaceWrite":{"type":"object","required":["externalId","raceNumber"],"properties":{"externalId":{"type":"string","maxLength":128},"raceNumber":{"type":"integer","minimum":1},"title":{"type":["string","null"],"maxLength":200,"description":"The race's name. **Send `null` to clear a stored one**; leave the property out to keep it. Every other optional property here preserves on omission and has no way to be unset — this one does, because a generated or mistyped name would otherwise stay on the crews' phones for the life of the event."},"scheduledAt":{"type":"string","format":"date-time","description":"Required when creating. Must carry an explicit UTC offset."},"distanceMeters":{"type":"integer","minimum":1},"boatClass":{"type":"string","maxLength":60},"round":{"type":"string","maxLength":60},"divisionLabel":{"type":"string","maxLength":60},"resultMode":{"$ref":"#/components/schemas/ResultMode"},"resultVisibility":{"$ref":"#/components/schemas/ResultVisibility"},"laneScheme":{"type":"array","items":{"type":"string"}},"notes":{"type":"string","maxLength":500}}},"Team":{"type":"object","properties":{"externalId":{"type":["string","null"]},"name":{"type":"string"},"clubId":{"type":["string","null"]},"boatIndex":{"type":["integer","null"]},"sortOrder":{"type":"integer"},"adopted":{"type":"boolean","description":"True when this crew existed before your sync and was claimed by it."}}},"TeamWrite":{"type":"object","required":["externalId","name"],"properties":{"externalId":{"type":"string","maxLength":128},"name":{"type":"string","maxLength":120},"clubId":{"type":"string","maxLength":64},"boatIndex":{"type":"integer","minimum":1},"sortOrder":{"type":"integer"}}},"TeamList":{"type":"object","properties":{"teams":{"type":"array","items":{"$ref":"#/components/schemas/Team"}}}},"TeamListWithOutcome":{"type":"object","properties":{"teams":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Team"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/UpsertOutcome"}}}]}}}},"RaceListWithOutcome":{"type":"object","properties":{"races":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Race"},{"type":"object","properties":{"outcome":{"$ref":"#/components/schemas/UpsertOutcome"}}}]}}}},"UpsertOutcome":{"type":"string","enum":["matched","adopted","inserted"],"description":"`matched` — found by your externalId. `adopted` — an existing row with the same natural key was claimed and now carries your id. `inserted` — newly created."},"LaneWrite":{"type":"object","required":["position"],"properties":{"position":{"type":"integer","minimum":1},"positionLabel":{"type":"string","maxLength":40},"teamExternalId":{"type":["string","null"],"description":"Omit or null to empty the lane while keeping it."}}},"ResultWrite":{"type":"object","required":["position"],"properties":{"position":{"type":"integer","minimum":1},"status":{"$ref":"#/components/schemas/EntryStatus"},"finishTimeMillis":{"type":"integer","minimum":0,"description":"Timed races only."},"finishPlace":{"type":"integer","minimum":1,"description":"Places-only races. In a timed race, places are derived."},"penaltyMillis":{"type":"integer","minimum":0}}}}}}