Web SDKWeb SDK

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.

sandboxproduction
SDK CDNhttps://sdk.sandbox.drivecars.aihttps://sdk.drivecars.ai
APIyour sandbox base URLyour 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.

  1. POST /oauth/token with your client credentials, for an API access token. It needs the sdk:sessions scope. See Authentication.
  2. POST /v1/sdk/sessions with 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
  }
}
  • origin is required when channel is "web", and must be on the origin allowlist we configured for you. A mismatch is 403 origin_not_allowed; omitting it is 400 origin_required. The minted token is bound to that origin.
  • The token lives at most 15 minutes. ttl_seconds is 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_at is 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.

MethodWhat 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)

OptionTypeNotes
sessionobjectRequired. 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.
langstringen, fr, ar, es. Drives copy and RTL.
currencyobject{ symbol, rateToPrimary, code?, symbolPosition? }not a bare code. See Components.
themeobjectPartial theme. See Theming.
onEvent(event, payload) => voidCatch-all; fires for every SDK event.
baseUrlstringEscape 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

SymptomLikely cause
The element renders nothing, no console errorThe 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 clickableresults-url is not set. The card has no href to navigate to.
The switcher is missing, or opens on the wrong productCheck enabled_products in your session, and that the element existed before init() ran.
Refusing to mount <drivecars-…> in the consoleThe component is not in your enabled_components. Ask us to enable it.
window.DriveCars is undefinedsdk.js was not loaded. The loader does not inject it.
Everything 401s after ~15 minutesonRefreshToken is missing, throwing, or returning the wrong shape. Listen for tokenRefreshFailed.
409 auth_provider_not_partner from the exchangeYou passed authProvider: 'partner' but your account is not switched to it. See Your own users.
Elements stay empty and carry data-dc-session-pendingThe 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 errorYou took an SRI hash from /v1/. See Pinning.
403 origin_not_allowed when minting a sessionThe origin you sent is not on your allowlist. It must match the browser origin exactly, scheme included.
Uppercase tags (<drivecars-Search>) render emptyCustom-element tags are lowercase. The loader’s tag map is keyed lowercase and never matches otherwise.