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.
Drive a regatta's running order, lane draw and results from your own timing software. Results you post reach the crews following that race within a minute.
Create a key in the DragonBoat Hub app under Account settings → API keys. You need permission to manage a race schedule in at least one club. The key is shown once and cannot be recovered — copy it before you close the screen.
# Read a regatta's running order
curl -H "Authorization: Bearer $DBH_KEY" \
https://data.dragonboathub.de/v1/events/1234/schedule
# Push a heat
curl -X PUT -H "Authorization: Bearer $DBH_KEY" \
-H "Content-Type: application/json" \
-d '{"races":[{
"externalId":"heat-12",
"raceNumber":12,
"title":"Mixed 200m Heat",
"scheduledAt":"2026-05-16T12:20:00Z"
}]}' \
https://data.dragonboathub.de/v1/events/1234/races
# Post the finish
curl -X PUT -H "Authorization: Bearer $DBH_KEY" \
-H "Content-Type: application/json" \
-d '{"results":[
{"position":1,"finishTimeMillis":51230},
{"position":2,"finishTimeMillis":52880}
]}' \
https://data.dragonboathub.de/v1/events/1234/races/heat-12/results The event id is in the app's event URL. If the event has no schedule yet, POST to the same path first.
Read these before you write anything. Each one will otherwise surprise you in production.
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.
The API can create and update; it cannot remove. Deleting a race, a crew or a schedule stays a human action in the app. A misconfigured client can corrupt data here, but it cannot destroy it.
Every race and crew is addressed by an externalId you choose. Our 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.
When an externalId is unknown, we look for an existing row with the same natural key — raceNumber for a race, name for a crew — and claim it, stamping your id onto it. That is what lets you push crews into an event whose teams were already seeded from club registrations.
Send your key as a bearer token on every request.
Authorization: Bearer dbh_live_… Generated from the OpenAPI document, so it cannot drift from what the server actually does. Two reads and five writes — there is no DELETE anywhere.
/events/{eventId}/scheduleReturns 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.
/events/{eventId}/scheduleIdempotent. Returns 201 with a fresh schedule, or 200 with the one that already exists — retrying after a timeout is safe.
All 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.
/events/{eventId}/teams/events/{eventId}/teamsMatches on externalId, then adopts an existing crew with the same name. Teams seeded from club registrations carry no externalId, so your first push claims them rather than failing on the unique name index.
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.
Each returned crew carries an outcome telling you which of the three paths it took.
/events/{eventId}/racesOne 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.
Adoption matches on raceNumber, the one identifier a schedule built by hand and a schedule pushed by software agree on.
Omitted 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.
/events/{eventId}/races/{externalId}/entriesSend the complete draw. A lane with no teamExternalId is emptied but kept, so the race keeps its start-position structure.
The crew must already exist — push to /teams first. positionLabel defaults to the race's lane scheme, then the schedule's, then the position number.
Seat 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.
/events/{eventId}/races/{externalId}/resultsSend 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.
In 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.
status defaults to finished. Use dns, dnf or dsq where they apply.
Posting 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.
Every non-2xx is an RFC 9457 application/problem+json body. Branch on code, which is stable; title and detail are prose and may be reworded. Validation failures also carry field, naming the offending property including its index in a list.
{
"type": "https://dragonboathub.de/developers/errors/validation-failed",
"title": "Request failed validation",
"status": 422,
"detail": "Must carry an explicit UTC offset.",
"code": "validation_failed",
"field": "races[0].scheduledAt"
}missing_credentialsinvalid_credentialsforbiddenevent_not_foundschedule_not_foundrace_not_foundroute_not_foundmethod_not_allowedpayload_too_largemalformed_jsonvalidation_failedconflictrate_limitedinternal_error Per key, as a sliding window: 600 reads and 120 writes per minute. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 adds Retry-After.
A 429 counts against your own window, so a client that ignores Retry-After keeps itself locked out. Back off rather than retrying tighter.
The API covers the race schedule today. If you need something it does not do yet — or you hit a case the docs do not answer — write, and it will get looked at properly.
flechtner@robocitrus.com