Using the API

Preview / beta

Once your application has tokens (see Authenticating your application), it calls the Road API on the user's behalf with the access token.

Calling the API

Send the access token as a Bearer token:

GET https://api.road.io/1/...
Authorization: Bearer {access_token}

Each call acts as the user the token was issued for, and returns only what that user's scopes and data sharing allow.

Available data

The scoped data available today is the ERE charging data: the user's charging sessions and charging-station metadata. These are covered in full, including pagination and delta sync, on the ERE integration page.

Account and identity data is available from the userinfo endpoint (below). More scoped APIs are being added over time.

Token lifetime and expiry

Access tokens are short lived (expires_in seconds, typically one hour). Every rejected token carries a standard WWW-Authenticate challenge (RFC 6750) alongside the status code, so you can tell the scenarios apart without parsing error bodies:

ResponseChallengeMeaning
401error="invalid_token"The token is expired, revoked, or no longer matches what the user has granted. Refresh and retry.
403error="insufficient_scope"The token does not hold the scopes this endpoint requires. Re-authorise requesting the missing scopes.

A 401 on its own never means the user has disconnected your application: an expired token and a revoked one deliberately look identical at the resource. The refresh outcome tells you which it was.

Detecting revocation

A user can revoke your application at any time from Settings → Personal → Connected apps, and a provider administrator can remove or narrow the installation. Both take effect on the next API call, not at token expiry. What your application sees:

  1. A scoped call fails with 401 and error="invalid_token":
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="road-api", error="invalid_token", error_description="application installation or grant is no longer active"
  1. Attempt a refresh, exactly as for normal expiry. The outcome disambiguates:
Refresh outcomeMeaningWhat to do
SucceedsThe token had merely expired, or the granted scopes changed (the new token carries the currently granted scopes).Retry with the new access token.
400 with invalid_grantThe user revoked your application.Stop polling, discard the stored tokens, and offer the user a way to reconnect.
401 with invalid_clientThe provider administrator removed the installation.Same handling; reconnecting requires the installation to be restored first.

If a freshly refreshed token then fails with 403 and error="insufficient_scope", the granted scopes were narrowed below what the endpoint needs (for example, a provider administrator removed a data scope from the installation). Re-run the authorisation flow requesting the scopes; the user sees a consent screen for them.

A public (browser) client without a refresh token uses the silent prompt=none flow instead; an error redirect (login_required or consent_required) is the equivalent disconnection signal.

Distinguish all of the above from an empty 200: that means the grant is intact and the user has simply shared no data (see ERE integration). Treat 401 plus a failed refresh as "disconnected", never as an empty share.

Refreshing tokens

If the user granted offline_access, the token response includes a refresh token. Exchange it at the token endpoint for a new access token:

POST {token_endpoint}
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token={refresh_token}&
client_id={client_id}&
client_secret={client_secret}

Confidential clients include client_secret; public clients omit it.

Refresh tokens rotate

Silent refresh in the browser

A public (browser) client with no refresh token can obtain a fresh code without showing UI by re-running the authorisation request with prompt=none in a hidden iframe. If the user can be auto-approved a code is returned; otherwise you get a standard error redirect (login_required, consent_required, or interaction_required) and should fall back to an interactive flow.

UserInfo

GET {userinfo_endpoint}
Authorization: Bearer {access_token}

Returns scope-filtered claims. sub, provider_id, and account_id are always present. The email scope adds email and email_verified; the profile scope adds name, given_name, family_name, locale, and zoneinfo. The same identity claims are embedded in the ID token when those scopes are granted.

Common error cases

ScenarioSymptom
Unregistered redirect_uri400 from authorize/details with invalid_request.
Requested scope exceeds what the client may request400 with invalid_scope.
Bad PKCE verifier400 from /token with invalid_grant.
Replayed authorisation code400 from /token; codes are single use and short lived.
prompt=none but interaction neededRedirect with ?error=login_required, consent_required, or interaction_required.
User denies consentRedirect with ?error=access_denied.
Expired or revoked access token401 with WWW-Authenticate: Bearer error="invalid_token"; refresh to disambiguate (above).
User revoked the application401 with error="invalid_token" on calls; the refresh then fails with 400 invalid_grant.
Token lacks a scope the endpoint requires403 with WWW-Authenticate: Bearer error="insufficient_scope".
Application holds a data scope but the user shared nothingScoped data endpoints return an empty list. Not an error.
Application token bound to a user with no account403 from the scoped data endpoints.

Operational notes

  • Secrets: a confidential client's secret is shown once at registration and again only on rotation. Never expose it on a frontend.
  • Refresh token rotation: refresh tokens are single use; record the new one on every refresh.
  • ID token verification: verify iss, aud, exp, and the signature against the JWKS.
  • State: pass a CSRF-resistant state on the authorisation request and verify it on the callback.