Verb

Docs

Connecting your signed-in users

Being logged into your product proves nothing to Verb. It cannot see your session cookie, so every visitor starts anonymous and read-only, including one who is signed in. Nothing that writes will work until your own backend mints a short-lived signed token and hands it to the widget.

Why it works this way

This is the step it is easiest to miss and the one that matters most. Skip it and reads work perfectly, while every write is refused with an honest "you need to sign in" to somebody who already is. If that is what you are seeing, you are on the right page.

The alternative would be Verb holding a credential of its own that can act on behalf of your users, which is exactly the thing you should not want. Instead your backend, which already knows who is logged in, says so in a token only you can sign, and Verb takes your word for it and nothing more.

The upshot is that the assistant can never do more than the person talking to it. Your API makes the same authorisation decision it already makes for every other request.

1. Mint a token on your server

Sign with the key from Sites, your site, Connect your users. It is an HMAC-SHA256 over a small JSON blob, which every backend language does in its standard library. Nothing here is framework-specific, and your dashboard has the same snippet for Next.js, Express, Fastify, Rails, Django and Laravel.

your server, wherever you already know who is logged in

javascript
// sign with the key from Sites, your site, Connect your users
const claims = { sub: user.id, exp: Math.floor(Date.now() / 1000) + 900 };
const body = base64url(JSON.stringify(claims));
const sig  = hmacSha256(body, VERB_SIGNING_KEY);
return Response.json({ token: `${body}.${sig}` });

sub is your own user id, whatever that is in your system. exp is a unix timestamp: keep it short, fifteen minutes is plenty, because the widget asks again when it needs to.

Both halves are base64url without padding, so - and _ rather than + and /, and no trailing =. Standard base64 is the one mistake that fails every token while looking entirely correct in a log.

2. Hand it to the widget

anywhere after the script tag, on load

javascript
const getSessionToken = () =>
  fetch("/api/verb-token", { credentials: "same-origin", cache: "no-store" })
    .then(r => r.json())
    .then(({ token }) => token);

// Sign in now, and let the widget ask again when the token gets old.
window.Verb.configure({ getSessionToken });
getSessionToken().then(token => token && window.Verb.identify(token));

Give it `getSessionToken`, not just one token. Tokens are short-lived on purpose, because one is a bearer credential for one person. A page left open outlives its token, and without a way to ask for another the visitor's next question is refused. With it, the widget fetches a new one before the old one expires and nobody notices.

If your API is on a different origin from the page, that fetch needs the full URL and credentials: "include". A relative path there quietly returns your own HTML, and the widget stays anonymous with nothing in the console to say why.

Return null when nobody is signed in. The widget drops the old token and carries on anonymously, which is a working state, rather than sending the previous person's identity on a shared machine.

identify still works on its own and is worth calling on load, so the panel is signed in from the first frame rather than from the first question.

3. Call reset() when they sign out

wherever your app signs somebody out

javascript
window.Verb.reset();

This forgets the conversation, the saved transcript and the cached token, so the next person at that machine starts from nothing. On an admin panel where a manager and an assistant share a desk, that matters more than it sounds: the conversation is on screen, and it can contain customer names and figures somebody read out of your product.

You get most of this for free. When identify() or getSessionToken returns a token for a different person, the widget clears the conversation itself. It compares the token's sub, so a refreshed token for the same person is not a change and your conversation survives it. Signing in after browsing anonymously is not a change either, because that is the same person continuing.

reset() is for the case with no next user to notice: they sign out, and nobody signs in.

Checking it worked

Ask the assistant to do something that writes. If it does it, you are done. If it says you need to sign in, one of three things is true:

  • identify() was never called, or ran before the script tag had loaded.
  • The token expired, because exp is in the past or in milliseconds rather than seconds.
  • The signature does not match, usually a different key than the one on that site.

Your dashboard's Logs tab shows whether any conversation on the site has ever carried a signed-in user, which separates "wired up, nobody has used it yet" from "never wired up at all". Those look identical in config and completely different in the log.

If every page is behind your login

Some products, an admin panel especially, have no anonymous visitors at all. Turn on Hide the assistant until someone signs in on the site, and the launcher stays hidden until a token arrives rather than offering an assistant that will refuse everything.

It offers less and grants nothing: the gates behind it are unchanged, and your API still decides what that person may do.

Stuck on something this does not cover? Write to us and you reach the person who built it.