Build on ReasonVoice
The assistant can answer from your documents on its own. This page is for the rest: telling it who the visitor is, letting it look things up in your system, putting the chat queue inside your own portal, and receiving every chat as it happens. Nothing here needs a ReasonVoice account for your users or your staff.
Who the visitor is
Your server signs a short-lived token saying who is on the page. The assistant trusts that and nothing else — never a name typed into the chat.
Create the signing key on the widget’s Embed page, under “Signed-in visitors”. It is shown once. Sign a token per page load and pass it to the widget:
// your server
import jwt from 'jsonwebtoken'
const token = jwt.sign(
{
sub: String(user.id), // required, a string: their id in your system
wid: 'widget_…', // required: this widget
email: user.email, // optional
name: user.name, // optional
role: 'admin', // optional, yours to define
audience: 'employee', // optional, see below
phone: user.phone, // optional: staff may call it from the chat
},
process.env.REASONVOICE_IDENTITY_SECRET, // rvid_…
{ algorithm: 'HS256', expiresIn: '1h' }
)
// your page
reva('init', { apiKey: 'reva_…', widgetId: 'widget_…', identityToken: token })submust be a string. A numeric one is refused.- A token lasts at most 24 hours; an hour is plenty. We allow 60 seconds of clock drift.
- One key per widget, so a token is only good for the widget it names.
- A widget can be set to signed-in visitors only, and then every message needs a live token. When one expires mid-chat the widget is asked for a fresh one; the chat carries on.
- Only the same person can re-attach to a chat: a second token for someone else is refused.
Different answers for different users
One assistant, one knowledge base, but an accountant and an employee asking the same question get the page written for them.
Put an audience claim in the token — any short word of your own: employee, accountant, small-business. Then tag knowledge sources with the audiences they are for, on the assistant’s Knowledge tab. A tagged source is only ever used for a visitor whose token names that audience; untagged sources are for everyone.
Visitors who are not signed in, and people on the phone, see everything untagged and public, exactly as before. Answers are never shared across audiences, not even from cache.
Answering “what’s mine?” from your system
An action is one named question the assistant may ask your API — the last payslip, the next delivery, the balance.
Add actions on the assistant’s Tools step. Each one declares what the assistant may choose (a period, a reference) — never who it is for. We add the verified identity ourselves, so “show me Priya’s pay” cannot work.
On your server
POST https://api.yourcompany.com/reasonvoice/actions
X-ReasonVoice-Timestamp: 1758560000
X-ReasonVoice-Signature: sha256=<hex>
Content-Type: application/json
{
"action": "pay_summary",
"arguments": { "period": "2026-09" },
"identity": { "user_id": "4821", "email": "…", "role": "…", "audience": "employee" },
"session_id": "…", "channel": "chat",
"timestamp": 1758560000, "nonce": "…"
}Your endpoint must:
- check the signature:
sha256=HMAC_SHA256(secret, "<timestamp>.<raw body>"), over the raw bytes, before parsing; - reject a timestamp more than five minutes old, and any
nonceseen before; - scope the answer to
identity.user_idonly, and refuse whenidentityis null; - treat
argumentsas untrusted filters a language model chose.
Each action gets its own signing secret, shown once; or you can sign them all with one secret for the business. Replies are capped at 4,000 characters, there are no retries, timeouts are 0.5–15s, and a visitor may trigger 30 lookups a minute (600 per business).
Or in your own page
An action can run in the page instead, using the session the visitor already has. We hold no credentials and your own authorisation applies unchanged:
reva('init', {
apiKey: 'reva_…', widgetId: 'widget_…', identityToken: token,
actions: {
get_last_paystub: async () => {
const r = await fetch('/api/me/paystubs/latest') // your session, your rules
const p = await r.json()
return { period: p.period, net_pay: p.netPay, pay_date: p.payDate }
}
}
})Return the fewest fields that answer the question. Whatever comes back may be read aloud to the visitor and appears in the transcript; the reply body itself is never stored or logged by us.
Your staff, in your own portal
The queue, transcripts, take over, reply and close — inside your product. Your staff sign in to your portal as usual and need no ReasonVoice account.
Create the staff key on the Inbox page (owner only). It is secret: it can read every chat, so it stays on your server and never reaches a browser.
<div id="reva-staff" style="height:640px"></div>
<script src="https://www.reasonvoice.ai/reva-staff-panel.js"></script>
<script>
RevaStaffPanel.init({
container: document.getElementById('reva-staff'),
// your server signs: { sub: String(staffId), bid: '<business id>', name, iat, exp }
// optional: wids: ['widget_…'] — only those widgets' chats
getToken: async () => (await fetch('/your-api/reva-staff-token')).text(),
})
</script>- Staff tokens last at most one hour; the panel asks for a fresh one when it expires.
widslimits a person to certain widgets — sales staff to the public site, support staff to the portal. Any other chat is invisible to them.- Signing someone out takes effect on their next request, including open connections.
- Serve the hosting page with
Content-Security-Policy: frame-ancestors 'self'. - The same chats appear in the ReasonVoice Inbox and the mobile app; a claim anywhere shows everywhere.
Chats as they happen
Mirror conversations into your own tickets, alert your team, or build your own reporting.
Subscribe in the dashboard under Channels → Webhooks, where you also get the signing secret, pause or replace it, and see how recent deliveries went. Events:
chat_handoff_requested— the visitor asked for a person, or the assistant got stuckchat_human_joined— a staff member took itchat_returned_to_ai— handed backchat_message— each message while a person is involvedchat_missed— a visitor asked for a person, nobody came before the wait ran out, and the assistant took the chat back. Carrieswaited_secondsand why they were waiting: this is the one to alert your team on.chat_closed— closed by staff, or the visitor blockedchat_csat— the visitor rated the chat
POST <your url>
X-ReasonVoice-Signature: <hex HMAC-SHA256 of the raw body with your subscription secret>
{
"event": "chat_message",
"timestamp": "2026-09-24T09:15:02Z",
"data": {
"chat_id": "…", "widget_id": "widget_…", "seq": 42,
"occurred_at": "2026-09-24T09:15:02Z",
"message": { "id": "…", "role": "user", "content": "…", "timestamp": "…", "sender_name": "…" },
"visitor": { "user_id": "4821", "email": "…", "name": "…" } // null when not signed in
}
}visitorcarries only what your server signed about them — never anything typed into the chat.- Order a chat’s events by
seq: deliveries are in order, but your own retries may not be. - Public HTTPS only. No retries: up to 200 events queue per business, and a subscription switches itself off after 10 failures in a row. Make your receiver fast and idempotent.
History and retention
Chats are kept for as long as you choose, and you can take all of it with you.
- Retention: keep chats for 7, 30, 90, 180, 365 or 730 days, or for ever — Settings → Chat data. A nightly job removes the rest.
- Export:
GET /api/chat-data/export?start=…&end=…&format=jsonl|csvgives every chat in a date range with every message. Signed in as the owner. - From your own server:
GET /api/staff/webchats/history?days=30&limit=50&cursor=…with the same staff key as the panel. Newest first, each chat with its messages (staff notes markedinternal), paged by thecursorin the reply until it comes back null. A key limited to some widgets sees only those widgets' chats. - A visitor’s own data: preview, export or delete everything for one person, for data requests.
If you are mirroring chats into your own system, treat the webhooks as the record and store them as they arrive. Set retention deliberately: a short window is the safer choice once your copy is the one you rely on.
The rules we keep
Worth knowing before you design around it, because they don't bend.
- Who a chat is about comes from your signed token only. Not from the conversation, not from the model, not from a parameter. Argument names that look like identity are dropped.
- No sign-in, no account data. The assistant is told the visitor is not signed in, told which of its lookups cannot run, and told not to ask who they are. It says to sign in instead.
- Your endpoint is the authority. We pass the verified identity and take what you return; we don’t decide what a person may see.
- Keys are separated by what they can do. The widget key is public and locked to your websites; the sign-in key vouches for one person; the staff key reads every chat and never leaves your server.
Building something that doesn’t fit here? Tell us what you’re wiring up and we’ll tell you whether it exists, or add it.
Talk to us