Web SDKYour own users

Signing in your own users

The default integration assumes your backend mints the session: it holds your client credentials, calls POST /v1/sdk/sessions, and hands the browser the result. That is the right shape when you have a backend willing to add a route.

If your users are already signed in on your own site, there is a shorter path. Your page hands the SDK the access token your user already holds, the browser trades it for a DriveCars session, and we find out who that user is by asking you. Your backend never enters the picture.

DriveCars.init({
  authProvider: 'partner',
  clientId: 'dc_ci_...',            // public. It is in your page source.
  accessToken: yourUsersAccessToken, // your own token, from your own auth
  environment: 'sandbox',
  lang: 'en',
});

No session. No onRefreshToken. Those are the two things the default path needs a backend for, and this path needs neither.

What you have to build

One endpoint: token introspection. We POST it the access token your page just handed us, and it tells us who that token belongs to.

POST https://auth.yoursite.com/introspect
Content-Type: application/json
Accept: application/json
Authorization: Bearer <the secret you gave us, if you configured one>
 
{ "token": "<your end user's access token>" }

Answer 200 with:

{
  "active": true,
  "sub": "usr_123",
  "email": "ada@example.com",
  "name": "Ada Lovelace"
}

That is RFC 7662 (OAuth 2.0 Token Introspection), on purpose. If you already run an OAuth server you almost certainly have this endpoint already. If you do not, the shape above is the whole specification.

FieldRequiredNotes
subYesA stable identifier for the user. user_id and id are accepted as aliases. A non-empty string, or a finite number (we stringify it).
activeNoAn explicit false is a rejection, even at HTTP 200 — that is RFC 7662’s design. Omit the field entirely and we judge on sub alone.
emailNoEchoed back to the SDK so you can render “signed in as”.
nameNoAs above. Trimmed to 320 characters.

email and name are genuinely optional. If your privacy posture says the subject and nothing else, the integration still works completely.

An object, array or boolean in sub is refused rather than stringified. It has to be, and it is worth knowing why: "[object Object]" is a perfectly stable string, and accepting it would collapse every user you have onto one DriveCars customer.

We call it server-side

The request comes from our servers, not from your user’s browser. That means:

  • No CORS. You do not need Access-Control-Allow-Origin on it, and it does not need to be reachable from a browser at all.
  • You can authenticate it. Give us a bearer secret and we present it as Authorization: Bearer … on every call. Optional — plenty of introspection endpoints are fine unauthenticated behind a token requirement — but available.
  • It must be reachable from the public internet. See what your URL must satisfy.

The budget

Five seconds, wall clock. Not an idle timeout — a total deadline, so an endpoint trickling bytes will not hold it open. A browser is blocked on this call while your user waits for a search box to appear, which is why it is half the ten seconds we allow a webhook delivery.

We buffer at most 64 KB of your response and never follow a redirect. A 3xx is a failure, exactly like a 500.

client_id is public. client_secret never is.

Your client_id belongs in your page source. It is public by design, in the same way a card processor’s publishable key is, and putting it there is not a leak.

Your client_secret must never reach a browser under any circumstances. It is your server credential; anyone who reads it can transact as you.

The reason the exchange is safe without a secret is worth stating plainly, because “an endpoint that takes no authentication” sounds alarming until you see what is actually holding it up:

The client_id grants nothing on its own. All it does is select whose auth server we go and ask. The exchange still fails unless your introspection endpoint vouches for the end-user token presented alongside it. Someone who scrapes a client_id out of your page has a name, not a credential — they would still need a real, live access token from your own auth system, which is the same thing they would need to impersonate that user on your own site.

Three more properties back that up:

Origin bindingA web exchange must arrive from an origin on your allowlist. A scraped client_id replayed from an attacker’s page is 403.
A rate limit on your client_id20 requests per minute. It bounds both brute-forcing your token space and using us to hammer your auth server.
An explicit opt-inThe exchange only works when your partner account has auth_provider = partner. A partner who never asked for this gets a 409, never a guest session.

Do not try to “harden” this by putting a client_secret in the page. A browser cannot hold one, and the moment you do, the secret is public and the whole credential is worthless.

What happens when things fail

Every failure lands on tokenRefreshFailed with an SdkExchangeError carrying both status and code, and the SDK logs a line naming what you should do. The four are deliberately distinguishable, because they call for four different actions:

StatuscodeWhat it meansWhat to do
401partner_auth_rejectedYour auth server did not accept the token. Their session with you has expired.Have the user sign in again on your site, then call init() with the new accessToken.
502partner_auth_unavailableWe could not reach your auth server, or it answered something we could not read.Do not sign the user out. This is not their fault and a retry may work.
409auth_provider_not_partnerYour partner account is not switched to partner auth.Ask us to enable it, or use POST /v1/sdk/sessions from your backend.
404unknown_clientNo partner matches that client_id.Check the clientId you passed to init().

A 403 is also possible: origin_not_allowed (the browser origin is not on your allowlist) or partner_inactive.

The 401/502 split is the one that matters. Collapsing them into “auth failed” means a DNS blip on your side logs all your customers out. They are two different sentences in the SDK’s console output for exactly that reason.

Your auth server being down does not break the widget

The customer sees an element that has not signed in — not a broken page and not a stack trace. During the exchange every mounted element carries data-dc-session-pending, which you can style as a skeleton; when the exchange fails the marker is removed, no token is written, and the element sits there having made no request it would only have got a 401 for.

Nothing from your auth server ever reaches the customer’s browser. Whatever your endpoint said — a stack trace, an internal hostname, another user’s record — is logged on our side and becomes exactly 401 partner_auth_rejected with our own wording. That is not a courtesy to us; it is what stops your internals leaking through our widget.

Refresh

There is no onRefreshToken to write. When the session token nears expiry (or an element sees a 401), the SDK re-exchanges the same accessToken — one round trip through your introspection endpoint, no backend.

Once your auth server rejects that token with a 401, the SDK latches: it stops re-exchanging until the next init(). A dead token would otherwise produce one failed exchange per mounted element per 401, which is a burst of traffic at your auth server for a session that cannot be recovered. Calling init() again with a fresh accessToken — which is what you do when your user signs back in — clears the latch.

You may still pass onRefreshToken on this path, and it wins if you do. That is worth it only if you have a backend that can mint a session directly: POST /v1/sdk/sessions is cheaper than a round trip through your own auth server.

The customer we resolve

The exchange echoes back the customer it resolved, so you can render “signed in as” without a second call:

{
  "session_token": "eyJhbGciOi...",
  "expires_at": "2026-09-10T12:15:00.000Z",
  "partner": { "slug": "acme", "...": "..." },
  "customer": {
    "ref": "acme:usr_123",
    "email": "ada@example.com",
    "name": "Ada Lovelace"
  }
}

customer.ref is namespaced with your partner slug, not the bare sub you returned. Two partners can both call a user 1, and an unprefixed reference would silently merge their customers the day a second partner turned this on.

email and name are only ever what you told us about your own user. Nothing is derived, and nothing comes from our side.

What your introspection URL must satisfy

We make an authenticated outbound request to an address you choose, from inside our network. The same guard that protects our webhook delivery applies here, and it will refuse a URL both when ops saves it and again on every call — a hostname that resolved to a public address the day it was configured can resolve somewhere else tomorrow.

Rule
HTTPS on port 443http:// and other ports are refused.
A hostname, not an IP literalhttps://203.0.113.10/introspect is refused.
No credentials in the URLhttps://user:pass@host/… is refused. Use the bearer secret.
It must resolve to a public addressPrivate (RFC 1918), loopback, link-local, CGNAT, multicast and IPv6 equivalents are all refused — including IPv4-mapped and NAT64 spellings of them.
Not localhost, *.internal, *.amazonaws.com or *.drivecars.aiRefused by hostname, trailing dot included.
Every resolved address must passA hostname with one public and one private A record is refused outright.
Redirects are never followedA 3xx is a failed introspection. If your endpoint moves, tell us the new URL.

The connection is pinned to the exact address that validation resolved, so the hostname is never resolved a second time between the check and the connect.

Turning it on

auth_provider is a setting on your partner account, not something you pass at runtime. Ask us to switch it, and give us:

  1. Your introspection URL.
  2. The bearer secret we should present to it, if it needs one.

Until it is switched, the exchange answers 409 auth_provider_not_partner — deliberately, rather than quietly issuing a guest session to anyone holding a client_id.

init() on this path

OptionTypeNotes
authProvider'partner'Required to take this path. Omitted (or 'drivecars') is the default backend-minted integration.
clientIdstringRequired. Your public client id.
accessTokenstringRequired. Your own end user’s token. Opaque to us — it is forwarded to your introspection endpoint and nowhere else.
sessionNot accepted. There is no session yet; the exchange produces it.
onRefreshTokencallbackOptional here. Wins over re-exchange when given.
environment, lang, currency, theme, onEventExactly as on the default path.

init() stays synchronous. It starts the exchange and returns; elements mount immediately, tokenless and marked pending, and pick up the token when it lands. Mount your elements in the DOM before init(), exactly as on the default path — that ordering is what lets the SDK sweep them.

Calling init() without clientId or accessToken on this path logs a console error naming the missing field and starts no exchange, leaving whatever was already configured intact.