A strongly-typed, async .NET client library for the RaceResult Web API. It wraps the entire HTTP surface — authentication, event management, participants, timing, results, and more — behind a clean, fluent C# API.
- .NET 10.0 or later
- A running RaceResult Web server (on-premise or cloud)
RaceResultClient.NET/
├── Apiclient.cs # Core HTTP client (auth, request building, error handling)
├── EventApiClient.cs # Per-event facade; entry point for all event-scoped endpoints
├── EventEndpoints.cs # Core event-scoped endpoint groups (participants, timing, etc.)
├── EventEndpointsDesign.cs # Additional groups (archives, certificates, lists, labels, …)
├── PublicAndGeneral.cs # Server-level endpoints (login, event lifecycle, user rights)
├── Models.cs # Core request/response record types and enums
├── DesignModels.cs # Design-object models (certificate, label, list, kiosk, …)
├── Supporting.cs # QueryParams builder, ApiException, Identifier, JSON config
└── RaceResultClient.csproj
This client mirrors the official Go libraries go-webapi
and go-model: endpoint paths, query parameters and
model fields track those repositories. Enums serialize numerically (as in Go's encoding/json),
except the certificate page-size/format enums which serialize as strings.
using RaceResultClient;
var client = new ApiClient(server: "my-raceresult-server.com", useHttps: true);Three authentication methods are supported:
// Username + password
await client.Public.LoginAsync(new LoginOptions { User = "admin", Password = "secret" });
// API key
await client.Public.LoginAsync(new LoginOptions { ApiKey = "your-api-key" });
// RaceResult user token
await client.Public.LoginAsync(new LoginOptions { RrUserToken = "token" });After login, the session token is stored internally and sent with every subsequent request as a Bearer token.
// By known event ID
var ev = client.ForEvent("my-event-id");
// Or create a new event and get a client for it
var ev = await client.Public.CreateEventAsync("My Race 2025", new DateTime(2025, 6, 1));
// Or pick from the event list
var events = await client.Public.GetEventListAsync(year: 2025);
var nextEvents = await client.Public.GetNextEventListAsync();
var pastEvents = await client.Public.GetPastEventListAsync();
var ev = client.ForEvent(events[0].Id);All endpoint groups are accessed as properties on EventApiClient:
// Count registered participants
int count = await ev.Data.CountAsync();
// Get all contests
Contest[] contests = await ev.Contests.GetAsync();
// Create a new participant
var result = await ev.Participants.NewAsync(bib: 42, contest: 1);
// Submit a timing passing
await ev.Times.AddAsync(new[] {
new Passing {
Transponder = "ABC123",
UtcTime = DateTime.UtcNow
}
});ApiClient owns an HttpClient and implements IDisposable:
using var client = new ApiClient("my-server.com");
// ...| Endpoint | Description |
|---|---|
Public.LoginAsync |
Authenticate and store session |
Public.LogoutAsync |
Terminate the current session |
Public.GetEventListAsync |
List events for a given year |
Public.CreateEventAsync |
Create a new event |
Public.DeleteEventAsync |
Permanently delete an event |
Public.GetUserInfoAsync |
Get info about the authenticated user |
Public.TokenFromSessionAsync |
Exchange the session for an OAuth2 token |
Public.GetUserRightsAsync |
List user access rights for an event |
Public.SaveUserRightsAsync |
Grant/update user rights |
Public.DeleteUserRightsAsync |
Revoke user rights |
General.GetFontsAsync |
List supported font names |
General.GetAppVersionAsync |
Get server application version |
General.TranslateAsync |
Translate field names or expressions |
General.GetLangItemAsync |
Get a single translation string |
All groups below are properties on EventApiClient.
| Group | Description |
|---|---|
AgeGroups |
CRUD, generate and reassign age group definitions |
Archives |
Archive lookup, create/write/import the event archive |
Backup |
Start/stop and monitor the backup process |
BibRanges |
CRUD for bib number ranges |
Certificates |
CRUD for certificate designs; render PDF/JPG/thumbnail |
CertificateSets |
CRUD for certificate sets; render and count |
Chat |
Read messages, register users, post messages |
ChipFile |
Read, save and clear the transponder chip file |
Contests |
CRUD for contest/category definitions |
CustomFields |
CRUD for custom participant fields |
Data |
Query and transform participant data with filtering, sorting, pagination |
Dependencies |
Inspect the field dependency tree and circular references |
EmailTemplates |
CRUD for email/SMS templates; preview and send |
EntryFees |
CRUD for entry fee tiers |
Exporters |
CRUD and start/stop for data exporters |
File |
File access, activation, version, modjobid, ownership/rights |
Forwarding |
Manage and monitor live data forwarding |
GroupTimes |
Read and save group start times / finish time limits |
History |
Read, export and delete participant field history |
Information |
Frequent first names and gender lookup |
Kiosks |
CRUD for kiosk configurations |
Labels |
CRUD for label designs; render PDF |
Lists |
CRUD for list designs; render PDF/HTML/XML/JSON/CSV/XLSX/… |
OverwriteValues |
Save/delete/count result overwrite values |
Participants |
Full participant lifecycle: create, read, update, delete, import (incl. SES), bib management |
Pictures |
Manage participant/event pictures and thumbnails |
Rankings |
CRUD for ranking definitions |
RawData |
Read, filter, export, copy/swap and delete raw timing data |
RawDataRules |
CRUD for raw data routing rules |
Registrations |
Process submissions; CRUD for registration forms |
Results |
CRUD for result column definitions |
Settings |
Read, write and delete event settings by name |
SimpleApi |
CRUD for Simple API endpoint configurations |
Splits |
CRUD for split/checkpoint definitions |
Statistics |
CRUD for statistic definitions; render and compute pivots |
Synchronization |
Check-out/check-in status (online server) |
TeamScores |
CRUD for team scoring definitions |
TimingPoints |
CRUD for timing point definitions |
TimingPointRules |
CRUD for timing point decoder routing rules |
Times |
Submit passings, read/delete/copy/swap times, single-start and random times |
UserDefinedFields |
Read and overwrite user-defined formula fields |
Vouchers |
CRUD for discount vouchers and validation |
WebHooks |
CRUD for webhook configurations |
Many endpoints accept either a bib number, an internal participant ID (PID), or a generic internal ID. The Identifier struct encodes this choice:
// Address by bib number
await ev.Times.GetAsync(Identifier.Bib(101));
// Address by internal participant ID (PID)
await ev.Participants.GetFieldsAsync(Identifier.Pid(5432), new[] { "Firstname", "Lastname" });
// Address by internal ID
await ev.History.GetAsync(Identifier.Id(99));QueryParams is a fluent, type-safe builder used internally and exposed for raw calls:
var q = new QueryParams()
.Add("filter", "contest=1")
.Add("limitFrom", 0)
.Add("limitTo", 50)
.AddArray("fields", new[] { "Firstname", "Lastname", "Bib" });Any non-200 HTTP response throws an ApiException carrying the status code and server error message:
try
{
await ev.Participants.DeleteAsync(filter: "bib=999");
}
catch (ApiException ex)
{
Console.WriteLine($"API error {ex.StatusCode}: {ex.Message}");
}You can also inject a custom exception factory if your application uses its own exception hierarchy:
client.ErrorFactory = (message, statusCode) => new MyAppException(message, statusCode);If you need to call an endpoint not yet surfaced by the library:
byte[] raw = await ev.GetRawAsync("some/undocumented/endpoint");byte[] fileBytes = File.ReadAllBytes("participants.xlsx");
ImportResult result = await ev.Participants.ImportAsync(
file: fileBytes,
addParticipants: true,
updateParticipants: true
);
Console.WriteLine($"Added: {result.Added}, Updated: {result.Updated}");var rows = await ev.Data.ListAsync(
fields: new[] { "Bib", "Firstname", "Lastname", "Contest" },
filter: "status=0",
sort: new[] { "Lastname" },
limitFrom: 0,
limitTo: 100
);
foreach (var row in rows)
Console.WriteLine($"{row[0]} — {row[1]} {row[2]}");var responses = await ev.Times.AddAsync(new[]
{
new Passing
{
Transponder = "A1B2C3",
UtcTime = DateTime.UtcNow,
DeviceName = "Finish Line Reader"
}
});
foreach (var r in responses)
Console.WriteLine($"Bib {r.ResultId}: {r.Time} at {r.TimingPoint}");await ev.Participants.SaveFieldsAsync(
id: Identifier.Bib(42),
values: new Dictionary<string, object?>
{
["Status"] = 1,
["Comment"] = "DNS"
}
);var settings = await ev.Settings.GetAsync(ct, "EventName", "EventDate");
await ev.Settings.SaveAsync("EventName", "My Updated Race Name");ApiClientowns theHttpClientand session state. It is the root object and should be disposed when done.EventApiClientis a lightweight façade created viaclient.ForEvent(id). It holds only an event ID string and a reference toApiClient— no state of its own. Endpoint group objects (e.g.ev.Participants) are instantiated on each property access (no caching overhead, no shared state).- All models use C#
recordtypes withinit-only properties, making them immutable and copy-friendly. - Serialization uses
System.Text.Jsonthroughout, with case-insensitive deserialization andJsonStringEnumConverterregistered globally. CancellationTokenis a last optional parameter on every async method, defaulting todefault.
This project is licensed under the MIT License.