Web SDK
The SDK puts DriveCars search, cart, checkout and payment on your own site as HTML custom elements. Your page keeps its own layout, navigation and brand; the elements render inside it.
| sandbox | production | |
|---|---|---|
| SDK CDN | https://sdk.sandbox.drivecars.ai | https://sdk.drivecars.ai |
| API | your sandbox base URL | your production base URL |
widget.drivecars.ai also answers in production. It is a legacy alias kept
alive for embed snippets issued before the CDN existed. Use sdk.drivecars.ai
for new work.
Three things to know before you start
The install does not depend on your configuration. Same script tag, same element, same bundle for every partner. What you are enabled for is a commercial setting on your partner record, and the element reads it out of your session. There is nothing to switch on. See Components for what actually changes on screen.
There is no publishable key. Integrators look for one; there isn’t one, and there will not be one. No DriveCars secret ever reaches a browser. Your own backend mints a short-lived session token and hands the page only that.
If your users are already signed in on your own site, there is a shorter path that needs no backend route at all — your page trades your own end-user token for a DriveCars session in the browser. See Your own users. Everything else on this page still applies.
Elements go in the DOM before you call init(). This is the natural order —
the tags live in your HTML, init() runs once your backend returns a session —
and it is the order that works. An element created after init() misses the
sweep that publishes your products onto it and falls back to rentals, whatever
you were sold. No error is logged.
The two-line install
<script src="https://sdk.drivecars.ai/v1/loader.js"></script>
<drivecars-search
environment="sandbox"
token="SESSION_TOKEN_FROM_YOUR_BACKEND"
lang="en"
results-url="/cars">
</drivecars-search>That is a complete integration for one element. You never type a hostname of
ours — here or anywhere else: environment takes sandbox or production
and the element resolves the host itself, so going live is one word in one
place. See
the element attribute.
The moment you call init() (below), drop the attribute and pass
environment there instead: the SDK resolves the host once and writes it onto
every element, mounted or not. An attribute you wrote always beats what
init() would have set, so an environment left on the tag keeps overriding
the runtime you just configured — which is what makes a stale one hard to
spot.
loader.js is about 1 KB: it
watches the DOM for <drivecars-*> tags and injects only the bundle each one
needs, so a page with only search never downloads checkout.
Do not rearrange the paths. loader.js derives its base URL from its own
<script src> and fetches its siblings from that same directory. Copying
loader.js onto your own host without its siblings, or nesting the bundles
into subdirectories, breaks every lazy injection — the custom element simply
stays empty, with nothing in the console.
results-url is in that snippet on purpose. Without it a rental result card
renders as an anchor with no href, and clicking it does nothing at all. The
widget has no car-detail page of its own; you tell it where yours lives. See
Components.
The session token
Your server does two calls and gives the browser the result of the second.
POST /oauth/tokenwith your client credentials, for an API access token. It needs thesdk:sessionsscope. See Authentication.POST /v1/sdk/sessionswith that access token, for a browser session token.
POST https://{your-base-url}/v1/sdk/sessions
Authorization: Bearer <your API access token>
Content-Type: application/json
{
"channel": "web",
"origin": "https://www.yoursite.com"
}{
"session_token": "eyJhbGciOi...",
"expires_at": "2026-09-09T12:15:00.000Z",
"partner": {
"slug": "acme",
"name": "Acme Travel",
"mor_mode": "drivecars",
"enabled_components": ["search", "cart", "checkout"],
"theme_overrides": { "accent": "#ff6b00" },
"enabled_products": ["rentals"],
"default_product": null
}
}originis required whenchannelis"web", and must be on the origin allowlist we configured for you. A mismatch is403 origin_not_allowed; omitting it is400 origin_required. The minted token is bound to that origin.- The token lives at most 15 minutes.
ttl_secondsis accepted but hard capped — asking for 24 hours silently gets you 15 minutes, not an error. It is short precisely because it ends up in a browser. expires_atis ISO 8601, and is what the SDK’s proactive refresh timer reads.- Mint it per page load, server-side. Do not cache one across users, and do not put your client credentials anywhere a browser can read them.
Stand up an endpoint on your own site — say GET /api/drivecars-session — that
does both calls and returns that JSON verbatim. The SDK’s refresh callback calls
that. DriveCars never calls your server.
A working first page
For more than one element, or to configure them once instead of per element,
also load sdk.js. The loader does not inject it — it is not in the
tag-to-bundle map, and a missing sdk.js shows up as window.DriveCars being
undefined.
<script src="https://sdk.drivecars.ai/v1/loader.js"></script>
<script src="https://sdk.drivecars.ai/v1/sdk.js"></script>
<!-- Elements first. init() below sweeps the document for them. -->
<drivecars-search results-url="/cars"></drivecars-search>
<script>
// Wrapped in an async function: top-level `await` in a plain <script> is a
// syntax error, and the whole block would fail to parse.
(async () => {
const session = await fetch('/api/drivecars-session').then((r) => r.json());
DriveCars.init({
session, // the whole /v1/sdk/sessions response
environment: 'sandbox', // 'production' when you go live
lang: 'en',
onRefreshToken: async () =>
// Return the whole object, not just the token — see below.
fetch('/api/drivecars-session').then((r) => r.json()),
onEvent: (event, payload) => console.log(event, payload),
});
DriveCars.mount('search', document.querySelector('drivecars-search'));
})();
</script>init() applies your configuration as attributes on every element you mount(),
but only where the element does not already carry that attribute. A token=
or lang= you wrote in the HTML always wins over the init() value.
environment is how you choose an API host. 'sandbox' points every
element you mount at the sandbox API, 'production' at the production API.
There is no hostname for you to type, and no hostname to change when you go
live — you change one word.
Omitting environment gives you production, not sandbox. That is
deliberate: a page that names no environment is a live page, and quietly
routing it to sandbox would take real bookings into an environment nobody
fulfils. If you meant sandbox, say sandbox.
init() is safe to call again. An SPA re-running its bootstrap on client-side
navigation will not accumulate duplicate handlers or leak the previous session’s
refresh timer — the previous state is discarded first.
Keeping the session alive
onRefreshToken is the only bridge between the expiring token in the browser and
your backend, which holds the credentials. It fires from two triggers — an
element seeing a 401, and a proactive timer 60 seconds before expires_at —
and concurrent triggers collapse into a single call rather than each starting
their own.
Return the whole { session_token, expires_at } object, not a bare string.
A string still works, but the SDK then has no new expiry to schedule against,
so the proactive timer stops after that one refresh and the session only ever
recovers from a 401 afterwards. Returning the object — which is simply the
/v1/sdk/sessions response your backend already has in hand — keeps it
refreshing indefinitely.
Listen for tokenRefreshFailed and surface it; a failed refresh leaves the
elements holding a stale token. See Events.
The runtime API
sdk.js assigns window.DriveCars with five methods. Full signatures and
behaviour are on the pages they belong to.
| Method | What it does |
|---|---|
init(options) | Configure once. Required: session, onRefreshToken. |
mount(tag, element, props?) | Apply that configuration to one element. |
on(event, handler) | Subscribe. Returns an unsubscribe function. See Events. |
refreshSession(result) | Apply a token you obtained some other way. |
confirmPartnerPayment(reference, paymentReference) | Partner-as-MoR only. See Payments. |
init(options)
| Option | Type | Notes |
|---|---|---|
session | object | Required. The full /v1/sdk/sessions response. |
onRefreshToken | () => Promise<string | { session_token, expires_at }> | Required. Calls your backend. Return the object form. |
authProvider | 'drivecars' | 'partner' | Defaults to 'drivecars' — this table. 'partner' is a different shape entirely: no session, no required onRefreshToken, and clientId + accessToken instead. See Your own users. |
environment | 'production' | 'staging' | 'sandbox' | Selects the API host applied to elements as base-url. Defaults to production. staging and sandbox are the same deployment. |
lang | string | en, fr, ar, es. Drives copy and RTL. |
currency | object | { symbol, rateToPrimary, code?, symbolPosition? } — not a bare code. See Components. |
theme | object | Partial theme. See Theming. |
onEvent | (event, payload) => void | Catch-all; fires for every SDK event. |
baseUrl | string | Escape hatch, not part of a normal integration. See below. |
baseUrl — the escape hatch
baseUrl points the elements at an arbitrary host and beats environment
when both are given. It exists for local development against an API on
localhost, for a self-hosted deployment, and for tests. A normal partner
integration never sets it: use environment, which cannot point at a host
that does not exist.
An unrecognised environment — 'prod', say — is not used as a hostname.
The SDK falls back to production and logs a console.warn naming the three
values it accepts.
Both options exist as element attributes too — environment and base-url,
with the same precedence — for the standalone path where there is no init()
to read them. An element with neither, on a page with no init(), holds
its first API calls rather than issuing them at a guessed host; see
An element that knows no host waits.
mount(tag, element, props?)
tag is one of 'search' | 'cart' | 'checkout' | 'payment' | 'hello', and
element must be the matching <drivecars-*> element. Mounting 'search' onto
a <div> is refused with a console error.
props may carry reference (a label disambiguating multiple checkout
elements), theme (a per-mount partial theme, the highest-precedence tier), and
any other key, forwarded as an attribute — still attribute-wins.
Entitlement is checked at mount. Mounting a component not in your session’s
partner.enabled_components is refused: no attributes are set, a console error
explains why, and a mount:rejected event fires. hello is exempt.
This is a guard rail for your own integration, not a security boundary — it tells you when your page and your DriveCars configuration have drifted apart. What you are entitled to sell is settled in your agreement with us, not by the browser.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| The element renders nothing, no console error | The loader could not fetch the sibling bundle. Check the Network tab for a 404 on search.js. Almost always a rearranged or copied path. |
| A result card is not clickable | results-url is not set. The card has no href to navigate to. |
| The switcher is missing, or opens on the wrong product | Check enabled_products in your session, and that the element existed before init() ran. |
Refusing to mount <drivecars-…> in the console | The component is not in your enabled_components. Ask us to enable it. |
window.DriveCars is undefined | sdk.js was not loaded. The loader does not inject it. |
| Everything 401s after ~15 minutes | onRefreshToken is missing, throwing, or returning the wrong shape. Listen for tokenRefreshFailed. |
409 auth_provider_not_partner from the exchange | You passed authProvider: 'partner' but your account is not switched to it. See Your own users. |
Elements stay empty and carry data-dc-session-pending | The partner-auth exchange has not resolved. If the marker is gone and there is still no token, it failed — read the console line. |
| The script is blocked with an integrity error | You took an SRI hash from /v1/. See Pinning. |
403 origin_not_allowed when minting a session | The origin you sent is not on your allowlist. It must match the browser origin exactly, scheme included. |
Uppercase tags (<drivecars-Search>) render empty | Custom-element tags are lowercase. The loader’s tag map is keyed lowercase and never matches otherwise. |