Accounts, tiers and billing
Getting an account
Registration is at /register. While BETA_SIGNUP_CODE is set, a matching code
is required and requests without one are refused — which is why an open beta can
be closed without a deploy.
Existing users can bring others in directly: an invite creates a signed,
single-use link (/invite/{token}) that lands the recipient on a pre-filled
registration form. Password reset works the same way, through
/password-reset-request and a /password-reset/{token} link.
Once signed in you have two credentials, and they are not interchangeable:
- a session cookie for the web pages, and
- one or more API keys for the mobile app, the desktop client and anything you write yourself. Generate these from your profile page.
See the introduction for how the two differ in practice. A
missing key and a bad key both return 401.
Creating an account means accepting the Terms of Service and the Privacy Policy
(/guide/legal/terms, /guide/legal/privacy). GET /legal/current returns the live
version of both texts; a sign-up without terms_accepted is refused with 400
terms_not_accepted once TERMS_ENFORCE_FROM has passed, and the account records the
date and version it accepted.
Your account page: a copy of your data, and deleting your account
Every signed-in account has a page at /account
(on the public site: https://fastgis.eu/account). Signing in on the web
lands a rider there; accounts with the web console (managers and fleet
operators, and administrators) land on /dashboard, which carries the same
delete panel.
Download a copy of your data (GDPR Art. 15 and Art. 20). One click queues an archive of everything held about you: profile, garage, routes, library, surveys, rides, diagnostics, events, shares, emergency contacts, support conversation, subscription state, the files you uploaded (vehicle manuals and event files) and what the desktop client keeps. A README inside says what each file is. The archive is built in the background. When it is ready, it is emailed to you and listed on the account page. Either link downloads it once, within 6 hours, and only while you are signed in as that account. You can ask once an hour.
Delete my account (Art. 17). Type your username to confirm. Deleting starts a 30-day window before your data is destroyed:
- At once, and for good: public links and shares stop working, published stories and board listings come down, your emergency contacts are deleted (they are other people's details), friends, group memberships, push notifications and live location sharing end, and you are signed out everywhere.
- After 30 days: everything else — routes, library, rides, captures, surveys, leaderboard runs, vehicle reports, support conversation and workspace.
If you have shared tracks, the panel lists them, none ticked. A track you
tick stays published after your account is gone, edited first (the start and
end of the ride and the places you stopped are removed, and we do not claim the
result is anonymous). You then get a one-time code, on screen and by email.
That code is the only way to remove those tracks later, at /withdraw-tracks.
To cancel, use the link in the email we send you, or sign in again within the 30 days: you land on a screen with the date, Keep my account and Delete now instead of waiting. Keeping restores what was held; what went at once does not come back. You can still download a copy of your data from that screen.
From the apps (the API). The same two rights are open to an app holding the
account's X-API-Key, under /auth/account/* (the API reference, tag
auth-account). They do the same as the account page:
POST /auth/account/deletewith{"confirm": "<username>", "keep_tracks": [...]}is the same deletion, and it revokes the key that called it. The answer is{erase_after, receipt_code, kept}. The receipt code is in that body only (and in the email), never in a URL. A wrong username answers400and changes nothing.GET /auth/account/shared-trackslists the tracks that could be kept: id, name and length, no geometry.POST /auth/account/exportqueues the archive (202). The hour is shared with the account page, so a second ask from either answers429withretry_after_s. It answers503when the build queue is down, and that attempt does not count toward the hour.GET /auth/account/exportsays whether an archive is ready and until when. The download itself stays on the account page, signed in.
An account inside its 30-day window gets 401 from all four: its keys were
revoked, and signing in gives it no new one. Its way back is the web.
The desktop's Settings → Account uses these four (the desktop guide); deleting from there also clears that browser's own copies of the account. The rider app's Settings → Account (More → Connection, signed in) uses them too (the rider app); deleting there signs the phone out and clears the account from it, as a sign-out does, and a sign-in during the window shows the date and opens the web screen.
Your profile and garage
Your profile carries your display name, avatar and riding styles. The garage is your list of vehicles, drawn from a vehicle catalogue the platform maintains:
| Search the catalogue | GET /vehicles/search?q=&kind= |
| Browse by manufacturer | GET /vehicles/makes?kind=, GET /vehicles/models?make=&kind= |
| One model | GET /vehicles/models/{model_id} |
| Its OBD-II parameters | GET /vehicles/models/{model_id}/obd |
| Report a correction | POST /vehicles/models/{model_id}/obd/report |
| Where the data came from | GET /vehicles/sources |
kind is motorcycle (the default) or car, and the three list endpoints
answer with that kind alone. Leaving it out gives motorcycles, so a client
written before the car list existed is unaffected.
The OBD endpoints back the app's dongle support: knowing which parameters a given model exposes is what makes a live reading meaningful.
The 4x4 car list
kind=car is a named list of 4x4 model families — Land Cruiser, Defender,
Range Rover, Hilux, Jimny, Duster, G-Class and thirty-odd more — rather than
every car ever registered. It has to be a named list: no vehicle register says which
cars drive all four wheels. Every one of them is registered in the same EU
category as a hatchback, and the body code that would mark an off-roader is on
fewer than two thousand cars in the whole Dutch register. So the list is kept
in the open, in the repository, and each family is counted against the register
before it ships.
A car's detail (GET /vehicles/models/{model_id}) carries two extra fields:
| Field | What it says |
|---|---|
route_vehicle | "4x4" — the speed row a pick should set, which you can then change to Car |
four_wheel_drive | "all" when every version of that model drives four wheels, "some" when the family also has two-wheel-drive versions |
four_wheel_drive: "some" is the honest answer for a RAV4, a Duster or a
Hilux, where 4x2 versions are on the road under the same name: the apps say
4x4 versions exist rather than telling you your car is one.
A car's consumption comes from the register too, as the combined figure the
model was type-approved at — measurement on the spec reads
type_approval_wltp, or type_approval_nedc for cars older than the WLTP
cycle, and the two are never mixed. It is a laboratory figure and optimistic
off road, so it arrives as a starting point you can edit. The fuel tank is
still yours to type: no register records it.
Each garage vehicle also says how its routes are timed on unpaved ground:
route_vehicle on POST /auth/garage and PATCH /auth/garage/{vid} is one
row of the vehicle speed table — sport, touring, adventure or enduro
for a motorcycle, car or 4x4 for a car — and "" means the kind's default
(Adventure, Car). A row the kind cannot have is refused; a bicycle or walking
entry has none. The rider app sends the default bike's row as vehicle on its
route requests (see the rider app page, "How the ETA is estimated": rider app).
Fuel you carry beside the tank
On a 4x4 it is common to carry jerry cans, so a garage vehicle can say how
many and how big: extra_tank_count and extra_tank_size_l on
POST /auth/garage and PATCH /auth/garage/{vid}. You give the count and the
size — two cans of twenty litres — and the totals come back derived, never
typed:
| Field | |
|---|---|
extra_fuel_l | the litres you carry, count × size |
extra_range_km | what those litres are worth at your consumption figure |
They are a second range, not a bigger tank. range_km stays your
vehicle's own tank times its consumption, because that is the range the
vehicle can actually reach while moving: fuel in a can is not available until
you stop and pour it. So the apps show the extra beside the range ring rather
than inside it, and anything that plans fuel stops goes on using range_km.
Carried fuel is your kit, not the manual's figures, so picking a model from
the catalogue never fills it in and never locks it — unlike the tank and
consumption fields, which a pick or a manual read can supply. Set
extra_tank_count to 0 when the cans come off.
Your service schedule, in your own language
A garage vehicle carries its own service schedule: maintenance on
POST /auth/garage and PATCH /auth/garage/{vid} is a list of up to twenty
items, each with a name, an interval_km or an interval_months (at least
one — a brake fluid change is "every 24 months" with no distance at all) and
the last_done_km you have recorded.
Each item may also name which service it is:
| Field | What it says |
|---|---|
name | the words to show: the catalogue's published wording, or your own |
item | the catalogue's key for that service — oil_change, air_filter, valve_clearance, brake_fluid, chain_lube and the rest of the list GET /vehicles/models/{model_id} answers in |
item is optional in both directions, and that is the point. When you pick
a bike from the catalogue, the apps send the key along with the wording, and
the apps can then say the item in your language instead of showing the
catalogue's English sentence — the same words Read it uses below. When you
type an item in yourself there is no key, and your own words are shown; a key
outside the catalogue's list is refused rather than stored. A vehicle saved
before the key existed answers item: null, and its words are shown exactly as
they were.
Your own manual
A vehicle can keep your own owner's manual, so that it is always with the
bike rather than in a drawer. POST /auth/garage/{vid}/manual takes the PDF as
a multipart upload, up to 60 MB; the content has to really be a PDF, not
just a file called one.
The manual is stored as a private document in your Library and linked from the vehicle:
| Upload or replace it | POST /auth/garage/{vid}/manual (multipart file) |
| Open it | GET /grass/catalogue/items/{item_id}/file |
| Remove it | DELETE /auth/garage/{vid}/manual |
| Read what it says | POST /auth/garage/{vid}/manual/read, then GET the same path |
The vehicle's JSON then carries manual_item_id and a manual block with the
title, the size in bytes and the page count (pages is null when the PDF
could not be read — the file is kept either way).
It is yours alone. The file is served by one route, which reads the record
as you before it answers, so another account gets a 404 — the same answer as
for a record that does not exist. A manual cannot be shared, published or
re-exported to anyone else, and no document record is created by anything but
this upload. Uploading again replaces the manual: the previous one is removed
outright rather than put in the trash, and so is the file. Deleting the vehicle
takes its manual with it.
Your manuals travel with the rest of your data: they are in the documents/
folder of a download of your data (as PDFs, named after their Library
record), and they are erased with the account.
Reading your manual
Read it pulls the specs and the service intervals out of your own PDF and
keeps them against your bike. POST /auth/garage/{vid}/manual/read starts it
and answers 202 straight away — reading a 60 MB manual is not a request's
work — and GET /auth/garage/{vid}/manual/read tells you how it is going and,
when it is done, what the manual said.
Every figure carries where it came from: the source is your manual, the
page it was printed on, and, when your bike is a catalogue model, the
catalogue's own number beside it so you can see the two agree. A figure whose
line could not be found again on any page is shown with no page and marked
unverified — a page number nobody can check is worse than none.
It says plainly what it could not read. Most manuals in the world are not laid out like the ones we have patterns for. A brand whose service chart marks its cells with pictures (which no text extraction can see), or a manual that is a scan with no text in it, finishes with a plain answer rather than a silence:
code | What it means |
|---|---|
read | the specs and the schedule were read |
specs_only | could not read the schedule of this manual; the specs page was read |
nothing_read | nothing read — with a reason: it is a scan, or the brand is not one we can read yet |
A read that ends nothing_read is still a finished read (state: "done"):
failed is kept for the job itself going wrong. The brand is recorded either
way, including when it is a brand we have no patterns for — that list is what
decides which brand is taught next.
text_pages says how many pages of text came out of the PDF, which is 0
for a scan — it is not the manual's page count, which is pages on the
manual block beside it.
A read describes one PDF. Replace the manual and the read goes with the book it described, rather than showing the old manual's intervals under the new one.
Your riding pace
The rider app learns how fast you really ride against the plan — one factor on roads, one on slower stretches such as tracks — and remembers it per vehicle (see "How the ETA is estimated" on the rider app page). The account can hold a copy, so that a new phone starts from your pace rather than from nothing and the desktop planner can time a route at it (a fourth pace step, Mine). The routes below are live. The desktop planner reads them (only reads: it never writes your pace); the rider app starts writing and reading them in its next release.
| Every vehicle's pace, and the limits a client applies | GET /auth/pace |
| Store one vehicle's pace | PUT /auth/pace/{vehicle} |
| Forget it, every vehicle | DELETE /auth/pace |
{vehicle} is motorcycle, car, bicycle or walk. A record carries
fast_factor and slow_factor (actual ÷ planned time, kept between 0.25 and 3),
the km behind each, rides, updated_at and split_kmh, the road/track split
it was learned under (30–50 km/h). The newer updated_at wins: a write
older than what is stored changes nothing, and every answer returns what is
stored now with applied: true or false, so a phone that lost the race takes
the account's copy. A pace counts once 20 km are behind it, and is applied
damped halfway to the plan (limits.seed_min_km, limits.seed_damping).
The pace is in your data export (riding_pace.json) and is erased with the
account.
Performances
Rides can be scored against each other. POST /performances records an effort,
GET /performances lists yours, and GET /performances/leaderboard ranks them.
DELETE /performances/{effort_id} removes one.
Tiers
Plans are subtractive from the top: the app ships complete, the highest tier
holds every feature by construction, and lower tiers are what remains after
subtraction (app/services/features.py). The ladder is:
| Plan | Position |
|---|---|
| Free | Every signed-in user, including lapsed subscribers |
| Trial | Fourteen days with the Pegasus claim set |
| Centaur | Entry paid tier |
| Pegasus | Middle tier |
| Alicorn | Everything |
Each feature declares the lowest plan that includes it, so moving a feature between tiers is a one-value change rather than a scattered edit. Two kinds of feature are enforced differently, and the difference is deliberate:
- Server features gate an API route. A request without the claim is refused.
- Client features shape the app's own interface. They cost nothing at the margin, so they are enforced softly and circumvention is accepted rather than fought.
Per-plan grants can also be flipped at runtime without a deploy, through
GET /billing/admin/features and
PUT /billing/admin/features/{plan}/{slug}.
Billing
| What you are entitled to | GET /billing/me |
| Start a subscription | POST /billing/checkout |
| Stripe callbacks | POST /billing/stripe/webhook |
GET /billing/me returns the claims payload the clients read to decide what to
show. Checkout uses the price named by STRIPE_PRICE_ID, or the per-tier mapping
in STRIPE_PRICE_MAP. Webhooks are signature-verified with
STRIPE_WEBHOOK_SECRET, which is why that endpoint needs no API key.
:::tip Billing can be switched off entirely
While BILLING_ENABLED is unset, every entitlement check passes. A deployment
can run the full feature set without a payment provider configured, which is what
a self-hosted or pre-launch instance does.
:::