Appearance
API Reference
RunClub HQ is a server-rendered Laravel + Inertia application. There is one open, unauthenticated endpoint anyone may call — age grading — and beyond that, integrations today are built around webhooks from payment providers and the mobile app API used by the Capacitor client. A general REST API for third-party integrations is on the roadmap but is not yet available.
Status
This page describes what is currently callable from outside the app. It is not a stable public API contract — endpoint paths, payloads, and auth mechanisms may change between releases. Do not build external integrations against undocumented endpoints.
Age grading API
GET /api/age-grade is a free, public, unauthenticated endpoint. It is the same calculation that powers the age grade calculator, over the 2025 WMA/USATF road standards and the 2005/2006 WMA track standards, for single ages 5 to 100 and 22 distances from a mile to 200 km.
You do not need an account, a key or a club to call it.
Request
GET https://runclubhq.com/api/age-grade?gender=female&age=42&distance=10k&time=44:10| Parameter | Values | Notes |
|---|---|---|
gender | male, female | The standards are published as two tables; there is no third. |
age | 5–100 | Age at the race, not today. |
dob | a date | Alternative to age; age is worked out from it. |
distance | a distance slug | mile, 5k, 10k, half-marathon, marathon, 50k… |
distance_value + distance_unit | a number, then km or mi | For a distance the tables do not tabulate. |
surface | road (default), track | Track uses the 2005/2006 table. Off-road is not offered — there are no standards for it. |
time | 44:10, 1:28:15, 19:02.4 | Read as the only sensible reading for that distance. |
mode | grade (default), target | target answers "what time do I need for X%?" |
target | 1–200 | The percentage sought, in target mode. |
Every parameter is optional: an empty request is a valid empty calculator, not an error. What is rejected is a shape that could never be a performance.
Response
json
{
"input": { "gender": "female", "age": 42, "distance": "10k", "time": "44:10" },
"result": {
"percentage": 78.42,
"band": { "label": "Regional class", "…": "…" },
"next_band": { "…": "…" },
"standard_time": "34:38",
"age_graded_time": "41:55",
"finish_time": "44:10",
"distance_label": "10K",
"interpolated": false,
"approximate": false,
"table_version": "2025-07-27",
"table_label": "2025 WMA/USATF road standards",
"equivalents": [],
"championship": null
},
"source": {
"name": "RunClub HQ",
"tool": "Age grade calculator",
"url": "https://runclubhq.com/tools/age-grade-calculator",
"result_url": "https://runclubhq.com/tools/age-grade-calculator/10k/female/42/44-10",
"tables": "2025 WMA/USATF road standards",
"attribution": "Age-grade standards compiled by Alan L. Jones and Rex Harvey, approved by USATF Masters LDR and sanctioned by World Masters Athletics."
}
}result is null when the inputs do not describe a gradeable performance — the request was fine, there is simply no standard for it. result_url appears when the answer has a page of its own, which is the page to link a human to.
Rules of use
- Rate limit: 120 requests per minute per IP address. Generous, because a debounced keystroke on the calculator page is a request and a whole club can share one address.
- Please attribute. If you show an age grade from this endpoint, link back to the
source.urlorsource.result_urlin the response. That link is the whole reason this is free and open. - Quote the table. An age grade means nothing without the edition it came from.
source.tablesis there to be shown alongside the number. - Not versioned. This is the same endpoint our own page calls, so the payload can gain fields at any time. We will not remove or repurpose the fields documented above without notice, but treat unknown fields as additive and do not depend on key order.
- No caching requirement, but please be reasonable. The standards change at most once a year; there is no need to recompute the same query repeatedly.
The standards themselves are not ours: they are compiled by Alan L. Jones and Rex Harvey and published openly at github.com/AlanLyttonJones/Age-Grade-Tables. If you want the raw tables rather than a calculation, go there.
What exists today
Provider webhooks (inbound)
RunClub HQ receives webhooks from payment providers. These endpoints are intended for the providers themselves, not for third parties, and they verify request signatures.
| Provider | Path | Purpose |
|---|---|---|
| GoCardless | /webhooks/gocardless | Payments, mandates, refunds, chargebacks |
| Stripe | /webhooks/stripe | Card payments and refunds |
| AWS SES | /webhooks/ses | Inbound email for support tickets and bounce / complaint handling |
RunClub HQ validates signatures on every request and rejects anything that doesn't match. Nothing here is for a club to configure — email is run by us, and a club connecting GoCardless authorises it from the panel rather than setting a URL by hand. See Payments → Webhooks for what a club does see.
Mobile application API
The Capacitor mobile app talks to the same Laravel backend as the web UI, but uses Laravel Sanctum tokens instead of session cookies. Mobile requests are identified by the X-Capacitor-App header and exempted from the web CSRF middleware by VerifyMobileCsrfToken.
The mobile API is considered internal — it is not versioned, not documented for external consumers, and is allowed to change in lockstep with the mobile app. Do not build integrations against it.
Public event check-in
The EventCheckIn controllers expose a limited set of endpoints used by the in-venue check-in kiosk / admin UI. These are scoped to authenticated check-in managers and are not intended for external use.
What does not exist yet
The following were described in earlier drafts of this page but are not implemented in the codebase today:
- A public
/api/v1/*REST API for members, subscriptions, events, and payments (the age-grading endpoint above is a standalone public tool, not the start of one) - API token management from Settings → API
- Outbound webhooks you can subscribe to from your own systems
- A public OpenAPI / Swagger spec
If you need any of these, please open an issue describing your use case — it helps us prioritise.
Integrating with RunClub HQ today
While there is no public REST API, several integration paths are supported:
Data export
Club Admins can take a copy of the club's data from Settings → Download your data: a zip of CSVs covering members, households, addresses, emergency contacts, consents, seasons and fees, subscriptions, invoices, payments, events, participations and club records. Download links last seven days, after which the file is deleted and you can prepare another.
The same archive can be read back in, which is how a club moves between RunClub HQ instances — we run that import, and it reports any file it could not handle rather than dropping it silently. Not every file is read back yet. See Moving your club to RunClub HQ → From a club archive.
The Finances screens have their own Export to CSV actions for the statement, transactions, budget and treasurer's report, plus a year-end pack. Poll responses export from the poll itself. The Members screen exports the active roster for whichever age categories and genders you have filtered to, which is what a team manager needs to enter a squad. The Export button asks what the file should contain before it downloads: name, date of birth, age category, gender, England Athletics URN, phone, email, membership and status, all included unless you untick them. Other admin screens do not currently have per-screen CSV exports.
Email ingestion
Every club has an email address that turns an incoming message into a support ticket, and a reply to a ticket threads onto it. We run that infrastructure; a club either forwards its existing support address to it or shares it directly. See Support.
WordPress migration
If you are moving from a WordPress-based club site, there is a full migration path and we run it with you — see Moving from WordPress.
Reporting bugs and requesting features
If you have integration needs we should support, or you spot discrepancies between this page and the codebase, please open an issue on GitHub.