The Dirtanian API
Everything that powers off-road sport on Dirtanian — events, venues, outrides, activities, competitors, challenges, routes, segments and orders — over clean, predictable REST. Grab a key, make your first call in under a minute, and build something great.
Getting started
Three steps and you're pulling live data.
1 · Get a key
A Dirtanian admin issues keys, scoped to exactly what your integration needs. Ask for the
scopes you want (say, events:read) — you'll receive a key that looks like
dirt_live_4f…. It's shown once at creation, so store it somewhere safe
(a secret manager, not your source code).
2 · Make your first call
curl "https://api.dirtanian.com/v1/events?limit=3" \
-H "x-api-key: dirt_live_YOUR_KEY"
3 · Read the envelope
{
"ok": true,
"data": [ { "uid": "abc123", "exploreItemName": "Desert 1000", … } ],
"meta": { "count": 3, "limit": 3, "hasMore": true, "nextCursor": "abc123" }
}
That's it — every endpoint works the same way. The unauthenticated index at
GET /api/v1 lists every resource, live.
Authentication & keys
Two credentials are accepted; pick the one that fits.
API keys (integrations & partners)
Send your key on every request, either way works:
x-api-key: dirt_live_YOUR_KEY
# or
Authorization: Bearer dirt_live_YOUR_KEY
- Keys are stored hashed (SHA-256) — Dirtanian can't read your key back, and neither can anyone else.
- Keys carry scopes (below), an optional expiry, and can be revoked in seconds.
- Lost a key? No problem — it's revoked and reissued in moments. Nothing else changes.
Firebase ID tokens (the Dirtanian apps)
Signed-in calls with Authorization: Bearer <idToken> work too:
admins get full access including key management; a profile holding the
api role gets read access across the board. Roles are granted in the back office.
Managing keys (admins)
Admins manage keys right here in the API — sign in with your admin ID token (API keys deliberately can't mint more keys):
curl -X POST "https://api.dirtanian.com/v1/keys" \
-H "Authorization: Bearer $ADMIN_ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Race-results partner", "scopes": ["events:read", "activities:read"]}'
DELETE /v1/keys/{keyId}; usage (lastUsedAt,
requestCount) is visible on GET /v1/keys.Scopes & the api role
Access is opt-in per integration — a key does exactly what its scopes say, nothing more.
| Scope | Grants |
|---|---|
| {resource}:read | List + fetch that resource — e.g. events:read |
| {resource}:write | Create + update — e.g. venues:write |
| {resource}:delete | Delete — granted sparingly, on purpose |
| *:read / *:write / *:delete | The same, across every resource |
| keys:manage | Key administration — admins only, via ID token |
The api user role complements keys: grant it to a teammate's profile in the
back office and their own app sign-in can read the API (handy for internal dashboards and
exploration) — no key to circulate at all.
Responses & errors
One envelope, everywhere. You always know where to look.
Success
{ "ok": true, "data": …, "meta": { … } } // meta on lists
Errors
{ "ok": false, "error": { "code": "forbidden", "message": "Your key doesn't cover this yet — …" } }
| HTTP | code | When |
|---|---|---|
| 400 | invalid_request | The request needs a tweak — the message says exactly what. |
| 401 | unauthenticated | No credential, or one we couldn't verify. |
| 403 | forbidden | Valid credential, missing scope — the message names the scope to ask for. |
| 404 | not_found | No resource at that address. |
| 405 | method_not_allowed | The Allow header lists what the endpoint speaks. |
| 500 | internal | Our side — logged, and worth a retry in a moment. |
Timestamps serialise to ISO-8601 strings; geo points to
{"latitude", "longitude"}. Money is ZAR, plain numbers.
Pagination & filtering
Cursor-based, stable, fast — the same on every list endpoint.
| Param | Meaning |
|---|---|
| limit | Page size, 1–100 (default 25). |
| cursor | Opaque cursor — pass the previous page's meta.nextCursor. |
| archived | true to include archived records (hidden by default). |
| filters | Each resource lists its exact-match filters below, e.g. ?discipline=enduro. |
let cursor;
do {
const url = new URL("https://api.dirtanian.com/v1/activities");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const page = await fetch(url, { headers: { "x-api-key": KEY } }).then(r => r.json());
handle(page.data);
cursor = page.meta.hasMore ? page.meta.nextCursor : null;
} while (cursor);
Data care & fair use
- Personal data never leaves the platform. Competitor endpoints return public fields only — email, phone, date of birth, address, medical information, dependents and payment details are never exposed, on any scope. Social reach comes back as counts, not follower lists. (POPIA-aligned by design.)
- The ledger stays authoritative. Orders are read-only; the payment engine owns every write, so what you read always matches the money that moved.
- Platform-managed fields (ids, timestamps, sold tickets/entries, ratings, commission rates, KOM standings, counters) are set by Dirtanian — writes that touch them are declined with a clear message.
- Fair use: paginate rather than hammer, cache what you can, and keep to the data you need. Every key's usage is visible to admins; keys can be scoped, expired and revoked.