Skip to main content
The SDK never sees your secret key. Instead, your page asks your server for a customer session, and your server creates one with doola for the user who is signed in to your product. The loader calls your route when the SDK mounts, again before each session expires, and each time the founder presses Try again after a failed session. Your route must:
  1. Read the customer from your own login, never from the request body.
  2. Answer 401 only when nobody is signed in. The loader treats your 401 as “your user signed out”.
  3. Create the session with doola using your dk_ secret key.
  4. Return only accessToken and expiresIn, with Cache-Control: no-store.

Next.js and web-standard runtimes

@doola/js/server builds the route for you. createSessionHandler takes a web-standard Request and returns a Response, so it is a Next.js route handler as written, and a one-line wrapper in Remix, Hono, Bun, Deno and Cloudflare Workers (where process.env needs the nodejs_compat flag).
app/doola-session/route.ts

Express, Fastify and your own routing

createCustomerSession returns the status and body to send:

Any other language

Call Create a customer session with your secret key as the whole Authorization header, with no Bearer prefix:
Response from doola
Then keep the rules the helper applies for you:
  • Turn doola’s 401 into a 502. It means your key or account is wrong. Passed through, the loader would read it as “your user signed out” and send a signed-in customer to your login page.
  • Pass other errors through with their status, and send a 409 with doola’s error.code as { code }. doola answers 409 for more than one reason, and the loader tells them apart by the code.
  • Answer 502 when doola cannot be reached or answers with something that is not a session.
  • Unwrap the envelope. Send the browser only accessToken and expiresIn from payload, never the whole body.
  • Send Cache-Control: no-store on every answer, as for any token.

What to send

The founder confirms their legal name, phone and country in the first step of the wizard anyway, so sending them is optional.

The session

  • It lasts 10 minutes (expiresIn: 600). The loader asks your route for a new one at 80% of that, so a mounted SDK renews about every eight minutes, and only while it is mounted.
  • It acts as that one customer. A cs_test_ or cs_live_ token can read and submit only that customer’s formation through the SDK. It cannot call the Partner API.
  • It needs no storage. Your route mints a fresh session on every call. Never cache one, and never send one in a URL.

Errors from doola

On a renewal, each of these arrives as renewal_failed, except E_RESOURCE_CONFLICT and E_CUSTOMER_REVOKED, which keep their own types. The current session keeps working until it expires. For the three 409s, the iframe tells the founder what happened (Customer experience), but only when your route forwards the code. A route that answers a 409 with its status alone gets email_in_use for all three on the first session, and the founder sees “We could not start your session”. For mint_failed, the iframe shows “We could not start your session” with Try again, which calls your route again, up to three times, and then points the founder to your team. E_RESOURCE_CONFLICT usually means your user ids and emails disagree: send the same externalCustomerId for a person every time.

Protect the route

  • Authenticate it like any other route that acts as your user, and apply your CSRF protection.
  • Accept POST only, so a link or an image tag cannot mint a session.
  • Never let the browser choose the customer. The email and id come from your session, not the request.
  • Send only an email your app has verified for the signed-in user, and always send externalCustomerId. doola resolves the customer from them, and the code your route forwards on a 409 says whether that email is already a doola customer.
Last modified on October 8, 2026