Developer documentation

Secure support context,
through every session.

Install Wedget, verify logged-in customers from your server, reset chat identity on every authentication boundary, and safely stream customer activity to your support team.

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.

HTML
<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.

Authenticated Vrulo pages: this static snippet is suitable for anonymous visitors only. For a logged-in customer, obtain a server-signed identity first and mount the widget with load-time data-user-id and data-user-hash as described under Identify customers.

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

HTML
<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.

HTML
<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>
Tip: position works the same whether you use our button or your own. With a custom button, the offsets control where the open chat panel appears.

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).

HTML
<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>
When the custom button is on, our floating launcher is hidden and the page stays fully clickable; the chat only appears when you call SoftbetChat('open').

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.

CommandWhat 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.

JavaScript
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.

Conservative revocation: the session generation belongs to the verified visitor, so one successful reset invalidates that visitor's active widget tokens and WebSockets across tabs and devices. It does not delete server-side conversations or activity; the same account can recover its history later with a new valid verified identity. Vrulo must still broadcast logout so every local tab hides and clears its iframe.
Mandatory: await reset on logout, account switch, server-session expiry or revocation, and before replacing one authenticated identity with another. If it rejects, complete the Vrulo application logout, keep Wedget disabled, record the failure without personal data, and do not identify the next user until a reset succeeds.

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.

Node.js (Vrulo server)
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.

JavaScript (Vrulo authenticated shell)
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);
Never put secrets in the browser. The browser may receive the public app id, stable user id and the HMAC produced for that user. It must never receive WEDGET_IDENTITY_SECRET, WEDGET_API_KEY, a database credential, or another user's/session's identity payload. The identity endpoint must accept no browser-supplied user id, username, email or session token; it derives all fields from the current server-authenticated session.

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.

JavaScript
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 responseMeaning
401 identity_invalidThe id/hash pair is incomplete, malformed or invalid. No identity/profile fallback occurs.
503 identity_not_configuredThe workspace identity secret is missing or too short. Keep verified identity disabled until configuration is corrected.
403 host_origin_unattestedVerified bootstrap did not carry the messenger's browser-attested parent-origin marker. Signed identity is rejected.
409 identity_bootstrap_requiredA 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_resetThe 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:

Allowed origins
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 transitionRequired Wedget behavior
Existing authenticated session at page loadObtain the HMAC from the no-store server endpoint and pass verified identity at widget load time.
Password login with 2FADo not identify after the password step. Identify only after the server has accepted the second factor and issued the final authenticated session.
Wallet loginDo 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.
LogoutAwait SoftbetChat('reset'), then complete Vrulo logout. Vrulo logout must still complete if reset rejects; keep Wedget hidden and blocked from re-identification.
Account switchReset 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 401Immediately 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 mismatchIf 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.
JavaScript (logout boundary)
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.

Node.js (Vrulo server only)
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.

PHP (functions.php)
// 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.

HTTP
Authorization: Bearer wgk_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Base URL: https://api.wedget.app
EndpointScopeWhat it does
POST /v1/chat/ingest/identifyidentifyCreate or update a customer profile by external_id.
POST /v1/chat/ingest/recordsrecordsAppend or update activity (trades/orders/deposits/withdrawals/payments).
POST /v1/chat/ingest/productsproductsUpsert a product catalog for AI grounding + cards.
DELETE /v1/chat/ingest/recordsrecordsRemove 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
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.

Node.js (server)
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' }
    ]
  })
});
FieldTypeNotes
kindstringtrade, order, deposit, withdrawal, payment, custom. Required.
referencestringYour id for this item. Unique per kind. Required. Re-post to update.
titlestringShown to agents + the customer.
amount / amount_centsnumberMajor units, or integer cents.
currencystringISO code, e.g. USD.
statusstringFree text, color-coded (paid/completed = green, pending = amber, failed/refunded = red).
urlstringDeep link agents can open.
occurred_atISO dateDefaults 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.

Do not proxy arbitrary lookups. Vrulo may query its own database for the user derived from its HttpOnly authenticated session and then ingest that user's recent trades, deposits and withdrawals. Never let browser input select which user's profile or activity to retrieve.

Push products

cURL
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" }
  ] }'
Privacy + safety. Keys are workspace-scoped and a customer only ever sees their own activity (matched on the verified widget session). Push only the fields you want agents and the AI to use, all of them are optional.

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.

DataHowWhere it shows
Name, email, usernameidentifyAgent console + AI
Profile picture, address, phoneidentify attributesAgent console
Custom attributes (plan, tier, LTV...)identify attributesAgent console + AI
ProductsShopify or the Data APIAI + product cards
Orders + statusShopify or the Data APIAgent console + AI
Trades / deposits / withdrawals / payments + statusCustomer Data APIAgent console + AI + widget
Customer Data API. A server-to-server REST API (per-workspace API keys) to push trades, orders, deposits, withdrawals, payments and a full customer profile, so agents see a live activity timeline and the AI can answer "what is the status of my last deposit?". See the Customer-Data API reference and create a key in Settings -> Developers.