Skip to main content

OAuth for partner apps

If your product serves many dealers (a finance house, a CRM, a marketing platform), use OAuth rather than asking each dealer to create an API key and paste it into your product. The dealer signs in to Vehiso, sees exactly what your app is asking for, and approves it. They can revoke it at any time.

Vehiso implements the OAuth 2.0 authorisation code flow (RFC 6749) with PKCE (RFC 7636), using the S256 method only.

For a single dealer's own integration, an API key is simpler.

Registering an app​

Apps are registered by Vehiso. To request one, email Vehiso developer support at support@vehiso.com with:

  • your app's name, website and logo, as dealers will see them on the consent screen
  • every redirect URI your app will use (exact matches only)
  • the scopes your app needs

You receive a client id and a client secret. Keep the secret on your server.

Endpoints​

EndpointPurpose
GET https://api.vehiso.com/v1/oauth/authorizeStart of the flow. Send the dealer's browser here.
POST https://api.vehiso.com/v1/oauth/tokenExchange a code, or a refresh token, for tokens.
POST https://api.vehiso.com/v1/oauth/revokeRevoke a token.

The flow​

1. Send the dealer to Vehiso​

Generate a PKCE code verifier (a high-entropy random string) and its code challenge (the base64url-encoded SHA-256 of the verifier), and a random state. Store the verifier and state in the user's session, then redirect the browser:

https://api.vehiso.com/v1/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fpartner.example%2Fvehiso%2Fcallback
&scope=vehicles%3Aread%20enquiries%3Awrite
&state=RANDOM_STATE
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256

(Shown on several lines for reading; it is one URL.) scope is a space-separated list, and every scope must be one your app is registered for.

An unknown client_id or a redirect_uri that is not registered is answered by Vehiso with a 400 error page and is never redirected, so a bad link cannot send the dealer anywhere unexpected. Any other problem with the request is redirected back to your redirect_uri with error, error_description and state.

2. The dealer approves​

Vehiso checks your client id and redirect URI, then sends the dealer to the DMS at myvehiso.com. They enter their dealer code, sign in if they need to, and see a consent screen with your app's name and the scopes it wants, including which ones reach customers' personal data. They can only approve scopes their own DMS role allows.

3. Vehiso redirects back with a code​

On approval, the browser returns to your redirect_uri:

https://partner.example/vehiso/callback?code=AUTHORISATION_CODE&state=RANDOM_STATE

Check that state matches the one you stored. If the dealer declined, you get ?error=access_denied&state=... instead.

The code is single-use and expires after 10 minutes.

4. Exchange the code for tokens​

From your server, authenticating your app with HTTP Basic (preferred) or with client_id and client_secret in the body. The body can be form-encoded or JSON.

curl https://api.vehiso.com/v1/oauth/token \
-u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
-d grant_type=authorization_code \
-d code=AUTHORISATION_CODE \
-d redirect_uri=https://partner.example/vehiso/callback \
-d code_verifier=CODE_VERIFIER

redirect_uri must be the one the code was issued for, and code_verifier is the PKCE verifier (43 to 128 characters). The response:

{
"access_token": "vh_oat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "vh_ort_...",
"scope": "vehicles:read enquiries:write"
}

The tokens:

TokenPrefixLifetime
Access tokenvh_oat_1 hour
Refresh tokenvh_ort_90 days, replaced every time it is used

5. Call the API​

Use the access token exactly like an API key:

curl https://api.vehiso.com/v1/vehicles \
-H "Authorization: Bearer vh_oat_..."

Requests act as the dealer user who approved your app, limited to the scopes they approved and to that user's current DMS permissions. GET /v1/me tells you which dealer the token belongs to, and its app field describes your app. OAuth access tokens have their own rate limit, and like secret keys they need the dealer to be on a paid plan.

Refreshing tokens​

When the access token expires, get a new pair with the refresh token:

curl https://api.vehiso.com/v1/oauth/token \
-u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
-d grant_type=refresh_token \
-d refresh_token=vh_ort_...

You can add scope with a space-separated subset of the granted scopes to get a narrower access token.

Refresh tokens rotate: each is accepted once, and the response carries a new one. Always store the new one straight away.

Reusing a refresh token disconnects your app

Presenting a refresh token (or an authorisation code) a second time revokes the whole authorisation and every token it issued, because a reused token means a copy is in someone else's hands. Make sure two workers can never refresh the same dealer's token at the same time: lock per dealer around the refresh.

Errors​

The OAuth endpoints answer with standard RFC 6749 error bodies rather than the API's usual format:

{"error": "invalid_grant", "error_description": "..."}
errorMeaning
invalid_requestA parameter is missing or malformed.
invalid_clientClient authentication failed (status 401).
invalid_grantThe code or refresh token is invalid, expired or already used.
invalid_scopeA requested scope is not allowed.
unsupported_grant_typegrant_type is not authorization_code or refresh_token.

The OAuth endpoints are limited to 60 requests a minute.

PKCE example​

import crypto from 'node:crypto';

const base64url = (buffer) => buffer.toString('base64url');

export function startVehisoAuthorisation(session) {
const verifier = base64url(crypto.randomBytes(32));
const challenge = base64url(crypto.createHash('sha256').update(verifier).digest());
const state = base64url(crypto.randomBytes(16));

session.vehisoVerifier = verifier;
session.vehisoState = state;

const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.VEHISO_CLIENT_ID,
redirect_uri: 'https://partner.example/vehiso/callback',
scope: 'vehicles:read enquiries:write',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
});
return `https://api.vehiso.com/v1/oauth/authorize?${params}`;
}

export async function finishVehisoAuthorisation(session, query) {
if (query.error) throw new Error(`Authorisation failed: ${query.error}`);
if (query.state !== session.vehisoState) throw new Error('State mismatch');

const basic = Buffer.from(
`${process.env.VEHISO_CLIENT_ID}:${process.env.VEHISO_CLIENT_SECRET}`,
).toString('base64');

const res = await fetch('https://api.vehiso.com/v1/oauth/token', {
method: 'POST',
headers: {Authorization: `Basic ${basic}`},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: query.code,
redirect_uri: 'https://partner.example/vehiso/callback',
code_verifier: session.vehisoVerifier,
}),
});
if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
return res.json(); // store the access and refresh tokens against this dealer
}

Revocation​

  • Your app can revoke a token (RFC 7009) when a dealer disconnects on your side. Revoking an access token ends just that token; revoking the refresh token disconnects your app from the dealer entirely, revoking the whole authorisation and every token it issued. The answer is 200 with an empty object whether or not the token existed.

    curl https://api.vehiso.com/v1/oauth/revoke \
    -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \
    -d token=vh_ort_...
  • The dealer can revoke your app at any time in the DMS under Administration > Developers > Connected apps. That revokes every token the grant issued. Your next request gets 401 api_key_revoked, and refreshing fails. Treat that as "disconnected" and ask the dealer to connect again.