Vehiso API
The Vehiso API lets a dealer, and the partner apps a dealer connects, read and write the dealer's own data from outside Vehiso: bespoke websites, DMS tooling, data warehouses, automation tools and AI assistants.
Authentication
Every request except the OAuth endpoints carries a bearer token:
Authorization: Bearer <key>. One host serves every dealer, so the key is
what names the dealer. Keys are created in the Vehiso DMS (Dealer
Management System) under Administration > Developers and are shown once.
| Prefix | Kind | Use |
|---|---|---|
vh_live_sk_ | secret key | servers, scripts, ETL, against the dealer's data |
vh_live_pk_ | publishable key | a visitor's browser on a bespoke website |
vh_test_sk_, vh_test_pk_ | sandbox keys | building and testing, against the shared sandbox dealer |
vh_oat_ | OAuth access token | partner apps, see OAuth below |
A secret key must never be sent from a browser: a request with an
Origin header made with one is refused (secret_key_in_browser), so
the key is noticed and rolled. A publishable key is checked against its
list of allowed origins instead.
A key acts as its owner, a user of the dealership. What a key can do
is its scopes AND its owner's current permissions: a scope whose
permission the owner has lost stops working (403
scope_not_permitted_for_owner), and a key whose owner is deleted or
banned stops working altogether (401 key_owner_inactive). A key may also
be restricted to some branches, in which case it sees and writes only
their data.
Scopes
Each operation names the scope it needs in x-required-scope and in its
description. GET /me and GET /reference/{list} work with any key.
Publishable keys always carry exactly vehicles:read (public fields of
website-visible stock only), branches:read, public:enquiries and
public:valuation_requests, and can be given nothing more. Secret keys,
OAuth and webhooks need a paid plan (403 plan_not_entitled).
Responses
JSON throughout. A single object is returned flat under data; a list
is data (an array) plus meta. Ids are UUIDs; numeric ids never
appear.
Pagination and syncing
Lists use cursor pagination: ?limit= (1 to 100, default 25) and
?cursor=, the meta.next_cursor of the previous page. meta.has_more
says whether another page follows. Lists are ordered by updated_at,
then id, ascending, so a sync job can page to the end and remember the
last updated_at it saw for next time. Every list takes
?updated_since= (an ISO 8601 date-time), and lists of records that can
be deleted take ?include_deleted=true, which returns deleted rows as
tombstones ({ "id": "...", "deleted": true, "deleted_at": "..." }) so a
mirror can apply deletes. GET /events is the exception: events never
change, so it is ordered by when they happened.
Idempotency
Send an Idempotency-Key header (up to 255 characters) on any POST. A
retry with the same key and the same body within 24 hours gets the stored
response, marked Idempotent-Replayed: true, instead of doing the work
twice. The same key with a different body is refused with 409
idempotency_key_reused, and a retry that arrives while the first
attempt is still running gets 409 idempotency_key_in_use. Server errors
are not stored, so they can always be retried.
Errors
Errors share one body:
{ "success": false, "message": "...", "code": "not_found", "request_id": "...", "errors": { "field": ["..."] } }.
code is a stable, machine-readable string to branch on; message is
for people and may change. errors is present on 422 only, keyed by
field. Status codes: 401 for a missing or bad key, 403 for a scope, plan
or key restriction, 404, 409 for a conflict with the record's state, 422
for validation, 429 for rate limiting and 5xx for our faults. The OAuth
endpoints are the exception: they answer RFC 6749
{ "error", "error_description" } bodies, which is what OAuth client
libraries expect.
Every response carries X-Request-Id. Quote it to support.
Rate limits
Limits are per key, per minute: 120 for secret keys and OAuth tokens, 300
for publishable keys. The two public writes are further limited to 10 per
minute per visitor address. Responses carry X-RateLimit-Limit and
X-RateLimit-Remaining; a 429 carries Retry-After in seconds.
Money, times and codes
Money is an object in minor units with its currency:
{ "amount": 1499500, "currency": "GBP" } is 14,995.00 pounds. Money in a
request body (a vehicle's price, say) is an integer in minor units of
the dealer's currency. Times are ISO 8601 in UTC
(2026-09-27T14:02:11Z); dates are YYYY-MM-DD. Statuses, types and
sources are codes; GET /reference/{list} lists them with the dealer's
names for them.
OAuth
A partner serving many dealers uses the authorisation code flow with PKCE
(S256) instead of asking dealers for keys: send the dealer to
GET /oauth/authorize, exchange the code at POST /oauth/token for an
access token (vh_oat_, one hour) and a refresh token (vh_ort_, 90
days, rotated on every use), and revoke at POST /oauth/revoke.
Versioning
/v1 is in the path. Changes within v1 are additive only: new endpoints,
new optional request fields and new response fields. Treat unknown
response fields as normal.
Authentication
- HTTP: Bearer Auth
- OAuth 2.0: oauth2
An API key or an OAuth access token, as Authorization: Bearer <token>.
Secret keys start vh_live_sk_ (sandbox vh_test_sk_), publishable
keys vh_live_pk_ (sandbox vh_test_pk_) and OAuth access tokens
vh_oat_.
Security Scheme Type: | http |
|---|---|
HTTP Authorization Scheme: | bearer |
Bearer format: | vh_live_sk_... / vh_oat_... |
For partner apps. Authorisation code flow with PKCE (S256); the access token is then sent as a bearer token.
Security Scheme Type: | oauth2 |
|---|---|
OAuth Flow (authorizationCode): | Token URL: https://api.vehiso.com/v1/oauth/token Authorization URL: https://api.vehiso.com/v1/oauth/authorize Refresh URL: https://api.vehiso.com/v1/oauth/token Scopes:
|