DIRTANIAN.
API v1

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.

Base URL  https://api.dirtanian.com/v1
🔑 Scoped API keys 📦 One response envelope 📄 Cursor pagination 🛡 PII never exposed ⚡ Warm instances — no cold starts

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"]}'
The plaintext key appears exactly once in that response — copy it then. Revoke any time with 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.

ScopeGrants
{resource}:readList + fetch that resource — e.g. events:read
{resource}:writeCreate + update — e.g. venues:write
{resource}:deleteDelete — granted sparingly, on purpose
*:read / *:write / *:deleteThe same, across every resource
keys:manageKey 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 — …" } }
HTTPcodeWhen
400invalid_requestThe request needs a tweak — the message says exactly what.
401unauthenticatedNo credential, or one we couldn't verify.
403forbiddenValid credential, missing scope — the message names the scope to ask for.
404not_foundNo resource at that address.
405method_not_allowedThe Allow header lists what the endpoint speaks.
500internalOur 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.

ParamMeaning
limitPage size, 1–100 (default 25).
cursorOpaque cursor — pass the previous page's meta.nextCursor.
archivedtrue to include archived records (hidden by default).
filtersEach 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.