Own Chat UI

Design the chat yourself and let BitPalm answer behind it. Your interface sends what the user writes, BitPalm answers with everything the agent can do: its knowledge, specialists, tools, lead capture and appointments. Conversations and leads show up in your dashboard as usual, under the channel App.

Use this when the widget doesn't fit, for example in a mobile app with its own design. If the widget only needs your colors or your own button, theming and an own chat button are less work.

What you need

  • The Pro plan or higher.
  • An app key from the dashboard: open the agent, go to Install and find Own chat interface. It starts with bp_pk_.

The app key may go into your app or website. It only allows chatting with this one agent, like the widget does. Never put a secret API key (bp_live_) into an app.

React

npm install @bitpalm/ai-agents
import { useState } from "react";
import { useBitPalmChat } from "@bitpalm/ai-agents/react";
 
export function Chat() {
  const { messages, send, status, agent } = useBitPalmChat({ key: "bp_pk_…", locale: "de" });
  const [text, setText] = useState("");
 
  return (
    <div>
      {agent ? <p>{agent.welcomeMessage}</p> : null}
      {messages.map((message, index) => (
        <p key={index} className={message.role}>{message.text}</p>
      ))}
      <form
        onSubmit={(event) => {
          event.preventDefault();
          void send(text);
          setText("");
        }}
      >
        <input value={text} onChange={(event) => setText(event.target.value)} placeholder={agent?.inputPlaceholder} />
        <button type="submit" disabled={status === "streaming"}>Send</button>
      </form>
      {agent?.branding.required ? <a href={agent.branding.url}>{agent.branding.text}</a> : null}
    </div>
  );
}

The hook keeps the messages, streams the answer into the last one, loads the user's last conversation on start and remembers it across app restarts. The random visitor id it creates stays on the device. Don't replace it with a user id, see visitorId below. It also returns error, handoff and teamTyping for when your team takes over, refresh() to load the conversation again, reset() for a new conversation and client for the rest of the API below.

Capacitor, Ionic and other web views

The same code runs in apps built with Capacitor or Ionic, on iOS and Android. The API allows every origin, so capacitor://localhost works without setup. The visitor id is kept in localStorage of the app. With CapacitorHttp switched on, requests go through the native layer and the answer arrives at once instead of streaming.

Any JavaScript

import { createChatClient } from "@bitpalm/ai-agents/headless";
 
const chat = createChatClient({ key: "bp_pk_…", locale: "de" });
 
const agent = await chat.agent();              // name, greeting, quick links, branding
const earlier = await chat.history();          // the user's last conversation, if any
 
const result = await chat.send("Do you ship to Austria?", {
  onDelta: (piece, answer) => render(answer),  // the answer as it grows
  context: { cart: { total: 49 } },            // facts for this answer, at most 4 KB
});
MethodWhat it does
agent({ url? })Name, logo, color, greeting, input placeholder, quick links, photo uploads and the branding rule
send(text, options?)Sends a message, resolves with { text, conversationId, cards?, handoff? }
history()The messages of the current or last conversation, each with an id
updates()New replies of your team since the last call, plus teamTyping and handoff
identify({ name, email, … })Tells BitPalm who the logged-in user is
upload(file)A photo for the next message, when the agent takes photos. Pass the returned id as fileIds to send
feedback(index, "up" | "down")Rates the agent's answer number index, the first answer is 0
reset()Starts a new conversation

Errors are BitPalmApiError with a code like invalid_key, plan_required or rate_limited.

In React Native the answer arrives at once instead of streaming, because its fetch can't stream. Everything else works the same.

"Powered by BitPalm"

On Pro your chat UI has to show "Powered by BitPalm", linked to agents.bitpalm.ai. agent().branding tells you whether it is needed and gives you text and link. On Business it is up to you: the switch in the dashboard decides.

REST API

For native apps without JavaScript. Every request sends the app key as Authorization: Bearer bp_pk_…. Base URL: https://agents.bitpalm.ai.

visitorId holds the conversation together: only the same visitor id can continue a conversation, read it or rate it. So treat it like a password. Use a random id per device, like a UUID, and keep it in the app. 8 to 120 letters, digits, dots, colons, dashes or underscores.

Never use a plain user id or an email as visitorId. The app key is public, so anyone who guesses the id could read that user's chats. If a logged-in user should see the same conversation on all devices, let your server compute the id from the user id and a secret that never leaves your server:

// On your server, never in the app
const visitorId = crypto.createHmac("sha256", process.env.CHAT_ID_SECRET).update(userId).digest("hex");

Your app gets the id from your server after login and passes it on: createChatClient({ key, visitorId }) or useBitPalmChat({ key, visitorId }).

POST /api/v1/chat

{
  "message": "Do you ship to Austria?",
  "visitorId": "3f1c2b8e-9d7a-4c1e-8f00-1234567890ab",
  "conversationId": "…",
  "context": { "cart": { "total": 49 } },
  "fileIds": ["…"],
  "stream": true
}

Leave out conversationId for a new conversation. The answer streams as server-sent events, one JSON object per event:

data: {"type":"delta","text":"Yes, "}
 
data: {"type":"delta","text":"we ship to Austria."}
 
data: {"type":"replace","text":"Yes, we ship to Austria. It takes 3 to 5 days."}
 
data: {"type":"done","conversationId":"c0a8…","cards":[…]}

delta adds text, replace replaces everything shown so far, done ends the answer with the conversation id to send next time, error means the answer failed. Lines starting with : only keep the connection open. done carries "handoff": true when your team has the conversation, see below. With "stream": false you get one JSON answer: { "reply", "conversationId", "cards"?, "handoff"? }.

More endpoints

EndpointWhat it does
GET /api/v1/agent?locale=deName, greeting, placeholder, quick links, photo uploads and branding. url=/pricing picks the quick links of a page
GET /api/v1/chat/history?visitorId=…{ conversationId, handoff, messages } of the visitor's last conversation of the past week, or of conversationId
GET /api/v1/chat/updates?visitorId=…&conversationId=…&after=…{ handoff, teamTyping, messages }: replies of your team after after, the createdAt of the last one you have
POST /api/v1/identify{ visitorId, name?, email?, phone?, company?, externalUserId? }
POST /api/v1/chat/uploadMultipart with file, visitorId and optionally conversationId. Returns { id, url }
POST /api/v1/chat/feedback{ visitorId, conversationId, messageIndex, feedback: "up" | "down" }

Errors always look like { "error": { "code": "…", "message": "…" } }.

CodeStatusMeaning
missing_key, invalid_key401No app key, or one that was revoked
plan_required403The agent's owner needs the Pro plan or higher
agent_inactive, agent_paused503The agent is switched off or beyond the plan's agent limit
rate_limited429Too many messages, wait for Retry-After seconds
bad_request400Something is missing or invalid, the message says what

What the widget does that your UI takes over

  • Showing the answer. Text may contain Markdown. cards are things like appointment slots or products with a title, an image, fields and a button.
  • Quick links come with agent(). Show them as buttons: message is sent as the user's message, url opens a page.
  • Proactive messages (triggers) only run in the widget for now.

When your team takes over

The agent can hand a conversation over to your team, and your team can take one over in the dashboard. From then on the agent stays silent in that conversation until your team closes it.

  • The agent's last answer and every later done carry "handoff": true. Messages sent meanwhile get no answer from the agent, so done comes without text.
  • The hook then shows your team's replies as they are written. It asks every 3 seconds, adds them to messages with fromHuman: true and stops once your team closes the conversation. It pauses while the app is in the background. live: false switches this off.
  • teamTyping is true while someone of your team types.
const { messages, handoff, teamTyping } = useBitPalmChat({ key: "bp_pk_…" });
 
// Below the messages
{handoff ? <p>{teamTyping ? "Typing…" : "Our team will get back to you here."}</p> : null}

Without React, call client.updates() every few seconds while client.handoff is true. It returns the new replies of your team since the last call, teamTyping and handoff. Native apps ask GET /api/v1/chat/updates and send the createdAt of the last team reply they have as after.