Developers

Race Schedule
API

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.

OpenAPI 3.1 spec ↓https://data.dragonboathub.de/v1

Quickstart

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.

Four things to know

Read these before you write anything. Each one will otherwise surprise you in production.

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.

No deletes

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.

Your ids, not ours

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.

Adoption

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.

Authentication

Send your key as a bearer token on every request.

Authorization: Bearer dbh_live_…
  • Keys 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.
  • Keys are shown once at creation and cannot be recovered. We store only a hash.
  • 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.
  • A key can be revoked at any time from the app, and revocation takes effect immediately.

Reference

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.

GET/events/{eventId}/schedule

Read the whole running order

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.

POST/events/{eventId}/schedule

Create the schedule if the event has none

Idempotent. 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.

GET/events/{eventId}/teams

List the crews at this event

PUT/events/{eventId}/teams

Create or update crews

Matches 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.

PUT/events/{eventId}/races

Create or update races

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.

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.

PUT/events/{eventId}/races/{externalId}/entries

Set the lane draw for one race

Send 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.

PUT/events/{eventId}/races/{externalId}/results

Post the finish for one race

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.

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.

Errors

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

Rate limits

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.

Building something?

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