Virbe Documentation

Integration Scenarios

Common patterns for integrating external applications with an active Virbe kiosk or web widget using the API.

Building an Operator Control Panel

The example used throughout is an operator control panel – an external app that finds the active conversation on a kiosk or web widget and lets a human send messages or take over in real time.

Architecture Overview

The typical integration flow looks like this:

  1. A kiosk or web widget starts a conversation via its connected profile.
  2. Your external application polls the API by profileId to discover the active conversation.
  3. Once found, your application sends messages or actions to that conversation using the Messages API.

All integration calls are server-side only. Use the x-virbe-api-key header for authentication – see Authentication.

Step 1 – Know Your Profile ID

Each kiosk or web widget is connected through a profile. You can find your profile ID in the dashboard under Profiles → General – it appears in the browser URL:

https://hub.virbe.app/dashboard/profiles/PROFILE_ID/edit/general

You will use this profile ID to filter conversations and identify sessions from a specific touchpoint.

Step 2 – Discover the Active Conversation

Poll the List Conversations endpoint with your profileId to find the currently active session in a single API call:

curl -X GET \
  "https://my-assistant.virbe.app/api/v1/conversations-history?profileId=PROFILE_ID&sortBy=CreatedAtDesc&pageSizeAndNumber[size]=1&pageSizeAndNumber[number]=1" \
  -H "x-virbe-api-key: YOUR_API_KEY"

The first conversation in the response is the most recent session for that profile. Check its state – if it is still active (not closed), that is your target. Note the id from the response.

For a kiosk with a single concurrent user, the most recent conversation for that profile is almost always the active session.

Step 3 – Send Messages to the Active Conversation

Once you have the conversation id, use the Create Message endpoint to inject text or trigger named actions:

Sending a text message

curl -X POST \
  "https://my-assistant.virbe.app/api/v1/conversations/CONVERSATION_ID/messages/add" \
  -H "x-virbe-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "senderId": "operator-panel",
    "action": {
      "text": { "text": "The operator says hello!" }
    }
  }'

Triggering a named action

curl -X POST \
  "https://my-assistant.virbe.app/api/v1/conversations/CONVERSATION_ID/messages/add" \
  -H "x-virbe-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "senderId": "operator-panel",
    "action": {
      "customAction": { "name": "welcome" }
    }
  }'

Step 4 – Enable Human Handover

When an operator takes over a conversation, you should mark it as a human handover. This signals to the system that a human is now handling the conversation, which can affect conversation flow behavior and analytics.

PATCH /api/v1/conversations/:id/human-handover

curl -X PATCH \
  "https://my-assistant.virbe.app/api/v1/conversations/CONVERSATION_ID/human-handover" \
  -H "x-virbe-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true
  }'

Set "enabled": false to return the conversation back to the virtual being.

This endpoint requires an API key with the ConversationActions scope.

Polling for Active Sessions

Since the API is REST-based, your application should poll at a regular interval to detect new conversations. A simple polling loop works well:

  1. Every few seconds, call GET /api/v1/conversations-history?profileId=PROFILE_ID&sortBy=CreatedAtDesc&pageSizeAndNumber[size]=1&pageSizeAndNumber[number]=1.
  2. Compare the returned conversation id to the one you already have.
  3. If it changed, update your local state – a new session has started.

Do not poll more frequently than once every 2–3 seconds to avoid rate limiting.

Example: React Operator Panel

Below is a minimal React component that polls for the active conversation and lets an operator send text messages and named actions:

import { useState, useEffect, useCallback } from "react";

const API_BASE = "https://my-assistant.virbe.app/api/v1";
const API_KEY = "YOUR_API_KEY"; // Keep this server-side in production!
const PROFILE_ID = "YOUR_PROFILE_ID";

export function OperatorPanel() {
  const [conversationId, setConversationId] = useState<string | null>(null);
  const [message, setMessage] = useState("");
  const [status, setStatus] = useState("Polling...");

  // Poll for the active conversation by profileId
  useEffect(() => {
    let active = true;

    async function poll() {
      try {
        const res = await fetch(
          `${API_BASE}/conversations-history?profileId=${PROFILE_ID}&sortBy=CreatedAtDesc&pageSizeAndNumber[size]=1&pageSizeAndNumber[number]=1`,
          { headers: { "x-virbe-api-key": API_KEY } }
        );
        const data = await res.json();
        const latest = data?.results?.[0];

        if (active && latest?.id) {
          setConversationId(latest.id);
          setStatus("Connected ✅");
        } else {
          if (active) setStatus("No active conversation");
        }
      } catch (err) {
        if (active) setStatus("Error polling");
      }
    }

    poll();
    const interval = setInterval(poll, 3000);
    return () => {
      active = false;
      clearInterval(interval);
    };
  }, []);

  // Send a text message
  const sendText = useCallback(
    async (text: string) => {
      if (!conversationId) return;
      await fetch(`${API_BASE}/conversations/${conversationId}/messages/add`, {
        method: "POST",
        headers: {
          "x-virbe-api-key": API_KEY,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          senderId: "operator-panel",
          action: { text: { text } },
        }),
      });
    },
    [conversationId]
  );

  // Send a named action
  const sendAction = useCallback(
    async (name: string) => {
      if (!conversationId) return;
      await fetch(`${API_BASE}/conversations/${conversationId}/messages/add`, {
        method: "POST",
        headers: {
          "x-virbe-api-key": API_KEY,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          senderId: "operator-panel",
          action: { customAction: { name } },
        }),
      });
    },
    [conversationId]
  );

  return (
    <div style={{ fontFamily: "sans-serif", maxWidth: 480, margin: "0 auto", padding: 24 }}>
      <h2>Operator Panel</h2>
      <p>
        Status: <strong>{status}</strong>
      </p>
      {conversationId && (
        <p style={{ fontSize: 12, color: "#666" }}>
          Conversation: <code>{conversationId}</code>
        </p>
      )}

      <h4>Quick Actions</h4>
      <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 16 }}>
        {["welcome", "info", "help", "goodbye"].map((action) => (
          <button key={action} onClick={() => sendAction(action)}>
            {action}
          </button>
        ))}
      </div>

      <h4>Send Text</h4>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          sendText(message);
          setMessage("");
        }}
      >
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Type a message..."
          style={{ width: "100%", padding: 8, marginBottom: 8 }}
        />
        <button type="submit" disabled={!conversationId}>
          Send
        </button>
      </form>
    </div>
  );
}

Example: Plain HTML Operator Panel

A standalone HTML file that works without any build tools:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Virbe Operator Panel</title>
  <style>
    body { font-family: sans-serif; max-width: 480px; margin: 40px auto; padding: 0 16px; }
    h4 { margin-bottom: 8px; }
    .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
    .actions button { padding: 8px 16px; cursor: pointer; }
    input[type="text"] { width: 100%; padding: 8px; box-sizing: border-box; margin-bottom: 8px; }
    .status { margin-bottom: 16px; }
    code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 12px; }
  </style>
</head>
<body>
  <h2>Virbe Operator Panel</h2>
  <div class="status">Status: <strong id="status">Polling...</strong></div>
  <div id="conv-info" style="display:none; font-size:12px; color:#666; margin-bottom:16px;">
    Conversation: <code id="conv-id"></code>
  </div>

  <h4>Quick Actions</h4>
  <div class="actions">
    <button onclick="sendAction('welcome')">welcome</button>
    <button onclick="sendAction('info')">info</button>
    <button onclick="sendAction('help')">help</button>
    <button onclick="sendAction('goodbye')">goodbye</button>
  </div>

  <h4>Send Text</h4>
  <form onsubmit="handleSubmit(event)">
    <input type="text" id="msg-input" placeholder="Type a message..." />
    <button type="submit" id="send-btn" disabled>Send</button>
  </form>

  <script>
    const API_BASE  = "https://my-assistant.virbe.app/api/v1";
    const API_KEY   = "YOUR_API_KEY";  // Keep server-side in production!
    const PROFILE_ID = "YOUR_PROFILE_ID";
    let conversationId = null;

    async function poll() {
      try {
        const res = await fetch(
          `${API_BASE}/conversations-history?profileId=${PROFILE_ID}&sortBy=CreatedAtDesc&pageSizeAndNumber[size]=1&pageSizeAndNumber[number]=1`,
          { headers: { "x-virbe-api-key": API_KEY } }
        );
        const data = await res.json();
        const latest = data?.results?.[0];
        if (latest?.id) {
          conversationId = latest.id;
          setStatus("Connected ✅");
          document.getElementById("conv-info").style.display = "block";
          document.getElementById("conv-id").textContent = conversationId;
          document.getElementById("send-btn").disabled = false;
        } else {
          setStatus("No active conversation");
        }
      } catch (e) {
        setStatus("Error polling");
      }
    }

    async function sendText(text) {
      if (!conversationId) return;
      await fetch(`${API_BASE}/conversations/${conversationId}/messages/add`, {
        method: "POST",
        headers: { "x-virbe-api-key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({ senderId: "operator-panel", action: { text: { text } } }),
      });
    }

    async function sendAction(name) {
      if (!conversationId) return;
      await fetch(`${API_BASE}/conversations/${conversationId}/messages/add`, {
        method: "POST",
        headers: { "x-virbe-api-key": API_KEY, "Content-Type": "application/json" },
        body: JSON.stringify({ senderId: "operator-panel", action: { customAction: { name } } }),
      });
    }

    function handleSubmit(e) {
      e.preventDefault();
      const input = document.getElementById("msg-input");
      if (input.value.trim()) {
        sendText(input.value.trim());
        input.value = "";
      }
    }

    function setStatus(msg) {
      document.getElementById("status").textContent = msg;
    }

    poll();
    setInterval(poll, 3000);
  </script>
</body>
</html>

These examples call the API directly from the browser for simplicity. In production, proxy all API calls through your own backend (e.g. a Netlify Function or Node.js server) to keep your API key secret.

On this page