#Sessions and channels
#On the web
Render the token onto the widget's script tag. Signed-out visitors simply get no token, and the agent answers with its public tools.
<script src="https://cdn.vatio.ai/v1/widget.js"
data-workspace="acme"
data-token="vatpub_..."
<%= "data-visitor-token=\"#{visitor_jwt}\"".html_safe if current_user %>></script>
To sign someone in without reloading the page:
window.VatioWidget.identify(newToken); // null to sign out
#Signing in mid-conversation
A visitor asks something, the agent tells them to sign in, they do, and they
come back. The conversation survives. Whether they signed in through
identify() or by navigating away and reloading the page with a token, the
chat they already had is carried over and now has a name on it — so the agent
can answer the question that prompted the login without them retyping it.
| Before | After | What happens |
|---|---|---|
| anonymous | signed in | Same conversation, now identified |
| user A | user A, newer token | Same conversation, token refreshed |
| user A | user B | New conversation |
| signed in | signed out | New conversation |
The last two are the shared-laptop case: a conversation that belonged to one
sub is never handed to another, so signing in as someone else looks exactly
like arriving for the first time. Vatio enforces that server-side — the page
cannot opt out of it.
Refreshing is also how a web session stays alive: render a fresh token on each
page load and a long-running conversation keeps working past the first token's
exp.
#On WhatsApp and Instagram
A WhatsApp or Instagram chat has no page to render a token onto, so the
contact is sent to one. Add sign_in_url to auth::
auth:
public_key: identity.pub
sign_in_url: https://acme.com/vatio/sign-in
When a private tool runs for a contact who is not signed in, Vatio sends them a message with a Sign in button instead. That message is the agent's whole reply for the turn, and the inbox shows it with the button under it:
- The button opens
sign_in_url?code=.... The code works once, for 10 minutes. If they write again without having signed in, the next private tool sends a new button. - Your page signs them in the way your site already does. If they are already signed in on that phone, there is nothing to type.
- It redirects to
https://vatio.ai/connect/<code>?token=<jwt>, with the same JWT you put on the widget. - Vatio verifies it, and the conversation picks up where it left off: the agent runs the tool it was waiting on.
With vatio-sdk the page is one redirect:
redirect_to Vatio::Identity.sign_in_redirect_url(
code: params[:code],
subject: current_user.id,
claims: { name: current_user.name, phone_number: current_user.phone }
), allow_other_host: true
Without it, sign the JWT as you do for the web and redirect to
https://vatio.ai/connect/${code}?token=${jwt}. Always to that host, never to
one taken from the request.
That redirect has to be a full page load. When the contact was not signed
in, your login sends them back to this page and this page on to vatio.ai. If
your login form submits with fetch (Turbo, the Rails default, does), the
browser cannot follow a redirect to another site from it: the login page just
sits there and the conversation never hears about it. In Rails, answer Turbo's
request with a reload (vatio-sdk
shows how); in a single-page app, send the browser to this page with
window.location.assign, not a router push.
The token stays with the contact, so later conversations start signed in until
its exp, and then the button goes out again. Give it a longer exp than a web
token (the gem uses a week): the web gets a fresh one on every page load,
WhatsApp only when the contact signs in.
Confirming the number. A link can be forwarded: someone could ask for a
button on their own WhatsApp and send it to one of your users, whose account
would then answer on the wrong phone. So Vatio shows what is being connected
(•••• 5678 on WhatsApp, @handle on Instagram) and asks the person to
confirm. On WhatsApp, put a phone_number claim in the token and, when it
matches the WhatsApp number, that step is skipped. Instagram has no number to
match, so there it is always asked.
Your API sees nothing new: a private tool sends the same $auth.token, and you
verify it the same way.