Login with Cyclemate is OAuth 2.1 with PKCE. A rider taps a button in your app, signs in to Cyclemate (or is already signed in), approves your app on our consent screen, and your app receives an access token for that rider. Requests made with the token count against the rider’s own rate limit and are attributed to your app.An access token works on these endpoints, and nothing else:
A token never reads the rider’s ride history, saved routes, profile or settings, and it cannot reach any other part of the platform. Choose this over an API key when your app acts for a signed-in rider. A key is for your own server calling the API as itself.
The name of your app, as riders should see it on the consent screen.
Every redirect URI your app will use, in full. Matching is exact and there are no wildcards; see Redirect URIs.
Whether the app is public (a mobile app or a single-page web app, which cannot keep a secret) or confidential (a server-side app that can).
We reply with your client ID. A confidential app also receives a client secret, shown once; we can issue a new one if it is lost. Use separate registrations for development and production.
A redirect URI is the page or screen in your app that receives the rider after consent. We only ever send riders to a URI you registered, and the one you send in the authorize request must match a registered one exactly: scheme, host, port and path, character for character. There are no wildcards, patterns or prefix matches.
Register
Not
Why
https://your-app.example/auth/callback
https://your-app.example/auth/callback/
A trailing slash is a different path
https://your-app.example/auth/callback
https://www.your-app.example/auth/callback
A different host
https://your-app.example:8443/auth/callback
https://your-app.example/auth/callback
The port is part of the match
http://localhost:3000/auth/callback
http://localhost/auth/callback
Local development, with its port
com.yourcompany.rides://oauth/callback
com.yourcompany.rides://
A custom scheme still needs the full path
What to register:
Web apps:https:// URIs only. The one exception is http://localhost:<port>/… for local development.
Mobile apps: either your app’s custom scheme (com.yourcompany.rides://oauth/callback) or an https:// URL your app claims as a universal link or app link. Open the authorize URL in the system browser or an in-app browser tab; a web view will not hand the redirect back to your app.
Every environment you run: register one client per environment (development, staging, production) rather than one client with every URI. Each gets its own client ID and secret, so a leaked development secret never reaches production.
Several callbacks in one app (a silent-refresh page, a native and a web callback): list them all. Each authorize request names the one it wants.
What we send to it:
On approval, ?code=…&state=…. Exchange the code within 10 minutes; it works once.
On a cancel, ?error=access_denied&error_description=…&state=….
Nothing, if the request was malformed: an unregistered redirect_uri, an unknown client_id or a missing PKCE challenge answers the rider’s browser with an error straight from the authorize endpoint and never redirects anywhere, so a typo in the URI surfaces as a rider stuck on that error rather than as a request to your app.
Two URLs you do not register: the consent screen itself (https://cyclemate.club/oauth/consent) is ours, and the authorize and token endpoints are fixed (see Endpoints). To add, remove or change a redirect URI after registration, email [email protected]; the change applies to new authorize requests immediately.
One look, three titles. Use Sign in where a rider has an account, Sign up where they are creating one, and Continue where you do not know. Keep the mark, the title and the colours as they are; size the button to fit your layout. It sits on light and dark pages alike.
Property
Value
Typeface
Fustat, 16px, bold (700), 0.6px letter spacing. Fustat is a free Google Font; the snippet loads it
Size
50px tall, 24px side padding, 8px corner radius
Colours
White background, black title, 1px border at 15% black. Hover #F0F0F0 with the border at 20% black. Pressed #E0E0E0 at 97% scale
Mark
The Cyclemate icon at 20px with 4px rounded corners, 8px before the title. Load it from https://docs.cyclemate.club/images/brand/cyclemate-icon.png or ship a copy
Build the authorize URL when the rider taps, so each attempt gets a fresh code_challenge and state.
If you would rather not build the button, download one. Each file is the button above with the title already drawn, so there is nothing to load. The SVG scales to any size; the PNGs are 50px tall at 1x.
Scale the image proportionally, keep at least 8px clear around it, and do not recolour, stretch or add a shadow. Wrap it in a link or button that opens the authorize URL, as in the snippets above.
On a phone, open it in the system browser or an in-app browser tab, not a web view. A rider with the Cyclemate app installed may be taken into the app for the consent step; the web page handles it otherwise.
2
The rider signs in and approves your app
We show a consent screen naming your app and what it will be able to do. A rider who is not signed in signs in first (Google, Apple or email), then returns to the same consent screen.
The consent screen, here for an app registered as Commute Planner
The request is valid for 3 minutes. If the rider takes longer, the screen says the request expired and they go back to your app to try again.
3
Receive the code
On approval the rider lands on your redirect_uri with code and state:
Check that state matches what you stored. The code is single-use and valid for 10 minutes.If the rider cancels, the same URI receives error=access_denied and an error_description, plus your state.
4
Exchange the code for tokens
POST https://api.cyclemate.club/auth/v1/oauth/token, form-encoded.
A confidential app authenticates with -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" instead of sending client_id. Refresh tokens rotate: store the new one whenever the response carries it.
Access tokens are JWTs signed with ES256. We verify every token on our side, so your app does not need to validate them; the JWKS endpoint is there if you want to check an id_token yourself.
Problems with the request itself (an unknown client_id, a redirect URI that is not registered, a missing PKCE challenge) are shown to the rider as an error page and never reach your redirect URI. A rider who cancels returns to your redirect URI with error=access_denied.
{ "error": "invalid_grant", "error_description": "The authorization code has expired or was already used"}
invalid_grant also covers a code_verifier that does not match the challenge, and a refresh token that was revoked or already rotated. invalid_client means the client ID or secret was wrong.
A valid access token sent to an endpoint outside the five above answers 401 with { "error": "OAuth access tokens are not accepted by this endpoint" }, and the rest of the platform answers 403. An expired token answers 401 on every endpoint: refresh and retry.
Rate limits apply per rider, in a bucket of their own for your app, so one busy integration never slows a rider’s own use of Cyclemate.
A report filed with a token is the rider’s report. It appears in their app under My reports, and GET /reports/{id}/ answers only for reports that rider filed.
To revoke an app’s access for every rider, or to rotate a secret, email [email protected]. Refresh tokens stop working at once; access tokens expire within the hour.