Building a website with a publishable key
If you are building a dealer's website yourself, the browser can talk to the Vehiso API directly with a publishable key (vh_live_pk_...). It is designed to be visible in page source: it can only read what the dealer's website already shows, and it can only send enquiries and valuation requests.
Never put a secret key (vh_live_sk_...) in a website. The API refuses a secret key used from a browser with 403 secret_key_in_browser.
What a publishable key can do
| Endpoint | Returns or does |
|---|---|
GET /v1/vehicles, GET /v1/vehicles/{id} | Website-visible stock only, public fields only |
GET /v1/vehicles/{id}/images | The vehicle's photos |
GET /v1/branches, GET /v1/branches/{id} | Branch details |
POST /v1/public/enquiries | Creates an enquiry in the dealer's inbox |
POST /v1/public/valuation-requests | Creates a valuation request |
Only public fields are returned to a publishable key, and stock hidden from the website is left out. Publishable keys work on every Vehiso plan, including Free.
Set up the key
- In the DMS, go to Administration > Developers and create a publishable key.
- Add every origin the site is served from to its allowed origins, as
https://hostorhttps://host:port. Include bothhttps://www.example-motors.co.ukandhttps://example-motors.co.ukif both are used. Use a separate test key (vh_test_pk_) with its own allowed origins while you develop. - Copy the key into your site's front-end configuration.
A browser request from an origin that is not on the list is refused with 403 origin_not_allowed. The allowlist is what stops another website from embedding your key and spending your rate limit from its own visitors' browsers.
Requests with no Origin header (from your own server, for example during a static site build) are accepted with a publishable key, since they can only read public data.
Show stock
const VEHISO_KEY = 'vh_live_pk_...';
async function loadStock(cursor) {
const params = new URLSearchParams({limit: '24'});
if (cursor) params.set('cursor', cursor);
const res = await fetch(`https://api.vehiso.com/v1/vehicles?${params}`, {
headers: {Authorization: `Bearer ${VEHISO_KEY}`},
});
if (!res.ok) {
const error = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
return res.json(); // {data: [...vehicles], meta: {next_cursor, has_more}}
}
const {data: vehicles, meta} = await loadStock();
// Render vehicles. To show more, call loadStock(meta.next_cursor) while meta.has_more is true.
Prices are money objects in minor units, so format them before display:
const formatPrice = ({amount, currency}) =>
new Intl.NumberFormat('en-GB', {style: 'currency', currency, maximumFractionDigits: 0}).format(
amount / 100,
);
formatPrice({amount: 1499500, currency: 'GBP'}); // "£14,995"
price is null when the vehicle is price on application, so check price_on_application before formatting it. Each vehicle also carries its images, so a listing page needs no second request. The public vehicle fields are listed under List vehicles.
Send an enquiry
POST /v1/public/enquiries puts an enquiry straight into the dealer's Vehiso inbox, handled exactly as the dealer's own Vehiso website handles one: the same validation, the same notifications, and the source WEBSITE. It takes first_name and last_name (required), at least one of email and phone, and optionally message and the vehicle_id the enquiry is about.
async function sendEnquiry(form, vehicleId) {
const fields = new FormData(form);
const res = await fetch('https://api.vehiso.com/v1/public/enquiries', {
method: 'POST',
headers: {
Authorization: `Bearer ${VEHISO_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({
first_name: fields.get('first_name'),
last_name: fields.get('last_name'),
email: fields.get('email') || null,
phone: fields.get('phone') || null,
message: fields.get('message') || null,
vehicle_id: vehicleId ?? null,
}),
});
const body = await res.json();
if (res.status === 422) {
return {ok: false, fieldErrors: body.errors}; // {field: ["message", ...]}
}
if (!res.ok) {
return {ok: false, message: body.message};
}
return {ok: true};
}
Show errors beside the matching inputs on a 422. Generate the Idempotency-Key once per submission, not once per attempt, if you add retries: see Idempotency. The answer carries only the new enquiry's id.
The form can also describe the visitor's part exchange (registration_number, which then makes mileage, condition and service_history required), marketing opt-ins, and photos of the part exchange, which need a multipart/form-data body. Leave the opt-ins out when the visitor was not asked. The full body is under Send an enquiry from a website.
These public writes work with publishable keys only. From your own server, use POST /v1/enquiries with a secret key.
Value My Car
POST /v1/public/valuation-requests is the Value My Car form, handled as the dealer's Vehiso website handles it: the vehicle is looked up and a part exchange and an enquiry are created. The answer's mode says what happens next:
instant: the dealer has instant valuations turned on, andvaluationholds the figure.callback: the dealer will be in touch.
A dealer who does not take valuation requests (their Value My Car page is not published) answers 404 valuation_requests_unavailable. See Send a valuation request from a website for the body.
Limits on public writes
On top of the key's overall rate limit, each visitor's IP address can make 10 public writes per minute. A visitor who submits a form repeatedly gets 429 rate_limited; show them a friendly "please try again in a minute" message.
Checklist before launch
- The site uses a live publishable key (
vh_live_pk_), not a test key. - Every production origin, with and without
www, is on the key's allowed origins. - No secret key appears anywhere in the site's JavaScript bundle or HTML.
- Enquiry and valuation forms handle
422validation errors and429gracefully.