Quick start
Copy your snippet from Settings -> Install in the console and paste it before the closing </body> tag on every page. That is it, the chat bubble appears in the corner.
<script src="https://wedget.app/widget.js"
data-app-id="icw_xxxxxxxxxxxxxxxxxxxxxxxx"
defer></script>
Your data-app-id is unique to your workspace. The widget loads in an isolated iframe, so it never clashes with your site's CSS or JavaScript.
Position the widget
By default the widget sits in the bottom-right corner. Move it to the left, or place it at a custom distance from the edges, with simple data attributes, no code required.
Left or right
<script src="https://wedget.app/widget.js"
data-app-id="icw_..."
data-position="left" // "left" or "right" (default)
defer></script>
Custom placement
Nudge the widget away from the corner, for example to clear a cookie banner or a floating cart, with data-offset-x (distance from the side edge) and data-offset-y (distance from the bottom), in pixels. They apply to both the button and the open panel.
<script src="https://wedget.app/widget.js"
data-app-id="icw_..."
data-position="right"
data-offset-x="28" // 28px in from the right edge
data-offset-y="96" // 96px up from the bottom
defer></script>
Use your own button
Prefer to open the chat from a link in your nav, a "Contact us" button, or anything else? Turn off our floating bubble in Settings -> Launcher ("Use my own button"), then call the API from any element. You can also set the button size there (Small / Medium / Large).
<button onclick="SoftbetChat('open')">Chat with us</button>
<!-- or toggle open/closed from a nav link -->
<a href="#" onclick="SoftbetChat('toggle'); return false">Support</a>
JavaScript API
Once widget.js has loaded, a global SoftbetChat(command, arg) function is available. A legacy pre-loader stub may queue display commands, but it cannot return a real security Promise. Call reset and verified identify only after widget.js has installed the real API, require a thenable return value, and await it.
| Command | What it does |
|---|---|
SoftbetChat('open') | Open the chat panel. |
SoftbetChat('close') | Close the panel. |
SoftbetChat('toggle') | Open if closed, close if open. |
SoftbetChat('showNewMessage', 'Hi!') | Open and start a new conversation, optionally prefilled. |
SoftbetChat('showArticle', 'slug') | Open the help center to a specific article. |
await SoftbetChat('identify', {...}) | Safely establish or update a verified customer. Verified identity returns a Promise; see below. |
await SoftbetChat('reset') | Revoke the current widget session, clear its visitor token and cached identity/conversation state, close the connection, and hide the widget. |
Reset is an authentication boundary
await SoftbetChat('reset') resolves with { ok: true } only after Wedget has successfully called POST /v1/chat/widget/reset with the current widget token, the server has returned { ok: true, data: { reset: true } }, and the iframe has cleared its visitor token and in-memory conversation state. The loader hides immediately, removes identity-bearing script attributes and discards identity commands queued before reset. The reset endpoint atomically rotates the durable visitor token, increments the session generation and closes connections for the old generation. It is Cache-Control: no-store, accepts no request body and issues no replacement token. Host applications should call it through SoftbetChat, not directly.
function resetUnavailable() {
const error = new Error('Wedget reset API is not ready');
error.code = 'reset_failed';
return error;
}
try {
if (typeof SoftbetChat !== 'function') throw resetUnavailable();
const reset = SoftbetChat('reset');
if (!reset || typeof reset.then !== 'function') throw resetUnavailable();
await reset;
// Safe to reload/remount with a different load-time identity.
} catch (error) {
// Stable codes: "reset_failed" or "reset_timeout".
reportSecurityEvent('wedget_reset_failed', { code: error.code });
// Do not identify another account in this widget instance.
}
A rejected reset throws an Error whose stable code is reset_failed (the reset could not be completed) or reset_timeout (no completion acknowledgement arrived within 8 seconds). The widget remains hidden on either failure. Treat all other error fields and message text as diagnostic, not as a programmatic contract.
Concurrent reset calls share the in-flight reset. A duplicate reset after confirmed success and before a new iframe exists resolves locally with { ok: true }. Commands issued after reset begins are held for the next fresh session and replayed only after revocation succeeds; they are discarded on reset failure. After a successful reset, the next non-reset command may create a fresh anonymous messenger. For an account change, Vrulo should finish the switch and reload/remount with the next account's load-time verified identity.
Identify your customers
Use a verified identity for every authenticated customer so agents see the correct account and the customer can access only activity attached to that account. An unverified name or email is contact metadata only; it must never grant access to account history.
import crypto from 'node:crypto';
const identitySecret = process.env.WEDGET_IDENTITY_SECRET;
if (!identitySecret || identitySecret.length < 32) {
throw new Error('WEDGET_IDENTITY_SECRET is missing or too short');
}
app.get('/api/support/wedget-identity', requireAuthenticatedSession, (req, res) => {
// Derive this only from the authenticated server session.
const userId = String(req.auth.user.id);
const userHash = crypto
.createHmac('sha256', identitySecret)
.update(userId, 'utf8')
.digest('hex');
res.set({
'Cache-Control': 'private, no-store, max-age=0',
'Pragma': 'no-cache',
'Vary': 'Cookie, Authorization'
});
res.json({
user_id: userId,
user_hash: userHash,
name: req.auth.user.username,
email: req.auth.user.email
});
});
WEDGET_IDENTITY_SECRET must contain the existing Vrulo workspace identity secret. A Wedget workspace administrator provisions it to Vrulo through a protected handoff directly into the production server secret manager; it is not displayed by Settings -> Install and must not be exposed through a reveal endpoint or pasted into documentation. Make it available only to the Vrulo server process. It is distinct from the Wedget Customer-Data API key and from every Vrulo session/JWT secret.
Load-time verified identity (recommended)
Fetch the no-store identity document with same-origin credentials first, then add widget.js with data-user-id and data-user-hash. This lets bootstrap resolve the verified account before it considers an anonymous visitor token. Use the exact same stable, internal user-id string as the Customer-Data API's external_id.
const response = await fetch('/api/support/wedget-identity', {
credentials: 'same-origin',
cache: 'no-store',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) throw new Error('Authenticated Wedget identity unavailable');
const identity = await response.json();
const widget = document.createElement('script');
widget.src = 'https://wedget.app/widget.js';
widget.dataset.appId = 'icw_YOUR_PUBLIC_APP_ID'; // public, not a secret
widget.dataset.userId = identity.user_id;
widget.dataset.userHash = identity.user_hash;
widget.defer = true;
document.body.appendChild(widget);
If identity verification fails, Wedget does not link the visitor to the requested account. Do not fall back to granting authenticated record access by matching name, email, username or wallet address. Use a stable immutable account id, not an email address or wallet address, as user_id.
Runtime verified identity
For a completed login inside an already-running single-page application, call the real loaded API with a complete verified identity and await it. If the current widget is anonymous or belongs to another account, the loader hides it, awaits reset, then bootstraps with the new HMAC before applying optional profile fields. If it already belongs to the same verified account, only the profile update is sent.
function identityUnavailable() {
const error = new Error('Wedget verified identity API is not ready');
error.code = 'identity_failed';
return error;
}
async function applyVerifiedWedgetIdentity(identity) {
if (typeof SoftbetChat !== 'function') throw identityUnavailable();
const operation = SoftbetChat('identify', identity);
if (!operation || typeof operation.then !== 'function') {
throw identityUnavailable(); // a legacy queue stub is not success
}
return await operation; // { ok: true, verified: true }
}
Concurrent calls for the same verified id/hash share the in-flight Promise. A different concurrent identity rejects with identity_busy. The verified operation can reject with identity_invalid, identity_busy, identity_failed, identity_timeout after 10 seconds, or a propagated reset_failed/reset_timeout. A name/email-only call remains fire-and-forget unverified metadata and grants no account or record access.
| Identity response | Meaning |
|---|---|
401 identity_invalid | The id/hash pair is incomplete, malformed or invalid. No identity/profile fallback occurs. |
503 identity_not_configured | The workspace identity secret is missing or too short. Keep verified identity disabled until configuration is corrected. |
403 host_origin_unattested | Verified bootstrap did not carry the messenger's browser-attested parent-origin marker. Signed identity is rejected. |
409 identity_bootstrap_required | A verified identity was sent directly to an anonymous visitor. Establish it through identity-first bootstrap so anonymous history is never attached to an account. |
409 identity_switch_requires_reset | The current widget belongs to another external id, or that identity belongs to another visitor. Reset before continuing. |
Vrulo security integration
Vrulo must treat chat identity as part of its authentication lifecycle. The invariant is simple: one loaded widget instance belongs to at most one authenticated Vrulo account. A second identity is never applied until the previous Wedget session has been successfully reset.
Allowed origins
Set a non-empty, exact origin allowlist for the Vrulo workspace in Wedget. Include only origins that actually embed this workspace, for example:
https://vrulo.com
https://www.vrulo.com
An origin is the exact parent/embed site scheme + hostname + optional port, with no path or trailing slash. The loader sets an origin-only iframe referrer policy. The messenger cross-checks the browser's parent-origin signals and sends host_origin plus host_origin_attested: true during verified bootstrap; the backend requires both and enforces the exact allowlist. The attested flag is client-consistency metadata, not cryptographic proof: the HMAC and explicit origin allowlist remain the trust boundaries. Anonymous clients omit the flag rather than sending false.
If the browser can provide only an unattested fragment fallback, Wedget suppresses user_id/user_hash and permits anonymous bootstrap only.
Do not use * and do not leave the production list empty. Keep https://affiliate.vrulo.com excluded unless its separate authentication lifecycle implements the same identity/reset contract. Put localhost and development domains in a separate non-production workspace with separate keys and secrets.
Authentication transitions
| Vrulo transition | Required Wedget behavior |
|---|---|
| Existing authenticated session at page load | Obtain the HMAC from the no-store server endpoint and pass verified identity at widget load time. |
| Password login with 2FA | Do not identify after the password step. Identify only after the server has accepted the second factor and issued the final authenticated session. |
| Wallet login | Do not trust a browser-provided address. Identify only after the server verifies the signed nonce, binds it to a Vrulo account, and issues the authenticated session. |
| Logout | Await SoftbetChat('reset'), then complete Vrulo logout. Vrulo logout must still complete if reset rejects; keep Wedget hidden and blocked from re-identification. |
| Account switch | Reset and await success before ending account A and mounting or identifying account B. If reset rejects, abort the in-place switch and never identify B in that widget instance. |
| Session expiry/revocation or authenticated API 401 | Immediately hide/reset Wedget, clear Vrulo auth state, and route to a signed-out page that does not remount the widget until reset recovery succeeds. |
| Stale tab or user-id mismatch | If the current server session's stable user id differs from the identity used to mount Wedget, reset first. Never overwrite the old identity with identify. |
async function logoutVrulo() {
let chatReset = false;
try {
if (typeof SoftbetChat !== 'function') {
const error = new Error('Wedget is unavailable');
error.code = 'reset_failed';
throw error;
}
const reset = SoftbetChat('reset');
if (!reset || typeof reset.then !== 'function') {
const error = new Error('Wedget reset API is not ready');
error.code = 'reset_failed';
throw error;
}
await reset;
chatReset = true;
} catch (error) {
blockWedgetUntilResetRecovery();
reportSecurityEvent('wedget_reset_failed', { code: error.code });
} finally {
// Never keep the Vrulo account logged in because a support service failed.
await fetch('/api/auth/logout', {
method: 'POST', credentials: 'same-origin'
});
}
location.assign(chatReset ? '/signed-out' : '/signed-out?support-reset=required');
}
The Vrulo logout request must retain Vrulo's normal CSRF protection. The recovery marker and telemetry must contain no user id, email, HMAC, API key, chat token or session token. Coordinate logout across tabs so each loaded iframe resets. A signed-out or recovery page must not show cached conversation UI.
Widget-token transport
The hosted messenger owns the short-lived widget token. It authenticates WebSockets in the first frame, not in the URL: { type: "auth", token: "..." }. The server verifies the token signature and current session generation before sending ready. Do not copy widget tokens into Vrulo URLs, query strings, browser logs or analytics. After reset, replay of the old token is rejected with 401 widget_session_revoked and old WebSockets close with the session-reset signal.
Vrulo customer and activity sync
Keep profile and financial data server-to-server. On account creation/profile change, call POST /v1/chat/ingest/identify. On trade, deposit and withdrawal creation or status change, call POST /v1/chat/ingest/records. For existing accounts, Vrulo may backfill only the recent records required for support. Re-posting the same kind + reference updates that record.
const wedgetApiKey = process.env.WEDGET_API_KEY;
if (!wedgetApiKey) throw new Error('WEDGET_API_KEY is required');
async function wedgetIngest(path, body) {
const response = await fetch(`https://api.wedget.app/v1/chat/ingest/${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${wedgetApiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) throw new Error(`Wedget ingest failed: ${response.status}`);
}
const externalId = String(user.id); // same value signed by the HMAC
await wedgetIngest('identify', {
external_id: externalId,
username: user.username,
email: user.email
});
await wedgetIngest('records', {
external_id: externalId,
records: [
{ kind: 'trade', reference: String(trade.id), title: 'Trade', status: trade.status, amount: trade.amount, currency: trade.currency, occurred_at: trade.createdAt },
{ kind: 'deposit', reference: String(deposit.id), title: 'Deposit', status: deposit.status, amount: deposit.amount, currency: deposit.currency, occurred_at: deposit.createdAt },
{ kind: 'withdrawal', reference: String(withdrawal.id), title: 'Withdrawal', status: withdrawal.status, amount: withdrawal.amount, currency: withdrawal.currency, occurred_at: withdrawal.createdAt }
]
});
Do not expose a browser API that accepts an arbitrary session id, email, username or external_id to retrieve another account's activity. Vrulo resolves the current user from its HttpOnly authenticated session; Wedget then returns records only for the workspace and visitor ids signed into the widget session token.
Shopify
Connect your store in Settings -> Integrations -> Shopify and approve the one-click install. Wedget then:
- syncs your product catalog so the AI can recommend products by name and show product cards in chat;
- lets the AI look up order status for a customer;
- can issue a discount code during a conversation.
The widget is the same one-line snippet, the storefront integration runs through the approved Shopify app, no theme editing needed.
WooCommerce
No plugin needed. Add this to your child theme functions.php to load verified identity at bootstrap for logged-in customers and push orders to Wedget. Do not also add a second static widget snippet. Provision the identity secret server-side and create an API key in Settings -> Developers.
// 1) Render one widget script, with load-time identity when logged in.
add_action('wp_footer', function () {
$app_id = getenv('WEDGET_APP_ID'); // public workspace id
if (!$app_id) return;
$identity_attrs = '';
if (is_user_logged_in()) {
$u = wp_get_current_user();
$identity_secret = getenv('WEDGET_IDENTITY_SECRET');
if (!$identity_secret || strlen($identity_secret) < 32) return;
$hash = hash_hmac('sha256', (string) $u->ID, $identity_secret);
$identity_attrs = '" data-user-id="' . esc_attr((string) $u->ID) .
'" data-user-hash="' . esc_attr($hash) . '"';
}
echo '<script src="https://wedget.app/widget.js" data-app-id="' .
esc_attr($app_id) . $identity_attrs . '" defer></script>';
});
// 2) Push each order to the customer's Wedget activity timeline
add_action('woocommerce_order_status_changed', function ($order_id) {
$o = wc_get_order($order_id);
wp_remote_post('https://api.wedget.app/v1/chat/ingest/records', [
'headers' => ['Authorization' => 'Bearer ' . getenv('WEDGET_API_KEY'), 'Content-Type' => 'application/json'],
'body' => json_encode([
'external_id' => (string) $o->get_customer_id(),
'records' => [[
'kind' => 'order', 'reference' => (string) $order_id,
'title' => 'Order #' . $order_id,
'amount' => (float) $o->get_total(), 'currency' => $o->get_currency(),
'status' => $o->get_status(),
]],
]),
]);
});
The external_id ties the order to the same customer established at bootstrap (use the identical WordPress user-id string in both places). Wire await SoftbetChat('reset') into WooCommerce/WordPress logout and account switching as described above.
Customer-Data API
A server-to-server REST API to stream your customers' identity and activity (trades, orders, deposits, withdrawals and payments) into Wedget. Agents see a live timeline in the conversation panel, and a verified customer can see the activity attached to their own account in the widget.
Authentication
Create a key in Settings -> Developers. You will see it once, store it as a server-side secret. Send it as a Bearer token. Keys are scoped per workspace, never expose one in the browser.
Authorization: Bearer wgk_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Base URL: https://api.wedget.app
| Endpoint | Scope | What it does |
|---|---|---|
POST /v1/chat/ingest/identify | identify | Create or update a customer profile by external_id. |
POST /v1/chat/ingest/records | records | Append or update activity (trades/orders/deposits/withdrawals/payments). |
POST /v1/chat/ingest/products | products | Upsert a product catalog for AI grounding + cards. |
DELETE /v1/chat/ingest/records | records | Remove one record by reference. |
Identify a customer
external_id is your own stable, immutable internal user id and the key everything links to. It must exactly match the string used for verified widget HMAC identity. Everything except external_id is optional; send only what support needs. attributes are merged, so you can enrich the profile over time.
curl https://api.wedget.app/v1/chat/ingest/identify \
-H "Authorization: Bearer wgk_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "USER_STABLE_ID",
"name": "ACCOUNT_DISPLAY_NAME",
"email": "ACCOUNT_EMAIL",
"username": "ACCOUNT_USERNAME",
"attributes": { "support_tier": "ACCOUNT_SUPPORT_TIER" }
}'
Push activity records
Each record needs a kind (trade, order, deposit, withdrawal, payment or custom) and a reference unique within that kind, posting the same reference again updates it (so you can stream status changes). Amounts are major units (19.99) or pass amount_cents.
await fetch('https://api.wedget.app/v1/chat/ingest/records', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.WEDGET_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
external_id: 'USER_STABLE_ID',
records: [
{ kind: 'trade', reference: 'TRADE_REFERENCE', title: 'Trade', amount: 25, currency: 'USD', status: 'settled' },
{ kind: 'deposit', reference: 'DEPOSIT_REFERENCE', amount: 50, currency: 'USD', status: 'completed' },
{ kind: 'withdrawal', reference: 'WITHDRAWAL_REFERENCE', amount: 20, currency: 'USD', status: 'pending' }
]
})
});
| Field | Type | Notes |
|---|---|---|
kind | string | trade, order, deposit, withdrawal, payment, custom. Required. |
reference | string | Your id for this item. Unique per kind. Required. Re-post to update. |
title | string | Shown to agents + the customer. |
amount / amount_cents | number | Major units, or integer cents. |
currency | string | ISO code, e.g. USD. |
status | string | Free text, color-coded (paid/completed = green, pending = amber, failed/refunded = red). |
url | string | Deep link agents can open. |
occurred_at | ISO date | Defaults to now. |
Session-scoped activity reads
The hosted messenger—not Vrulo page code—uses GET /v1/chat/widget/records with its short-lived widget token. The endpoint accepts no external_id, username, email or Vrulo session id and returns at most the 20 most recent records for the workspace + visitor ids signed into that token. Username and email come from the verified profile linked by the same stable id; they are not lookup keys.
Push products
curl https://api.wedget.app/v1/chat/ingest/products \
-H "Authorization: Bearer wgk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{ "products": [
{ "external_id": "sku1", "title": "Pro Plan", "price": 199, "currency": "USD",
"url": "https://merchant.example.invalid/pro", "image_url": "https://merchant.example.invalid/pro.png" }
] }'
What the widget can receive
Beyond the basics, you can stream rich customer context to Wedget so your agents (and the AI) always know who they are talking to. All of it is optional, send only what is useful.
| Data | How | Where it shows |
|---|---|---|
| Name, email, username | identify | Agent console + AI |
| Profile picture, address, phone | identify attributes | Agent console |
| Custom attributes (plan, tier, LTV...) | identify attributes | Agent console + AI |
| Products | Shopify or the Data API | AI + product cards |
| Orders + status | Shopify or the Data API | Agent console + AI |
| Trades / deposits / withdrawals / payments + status | Customer Data API | Agent console + AI + widget |