Backprompter

Backprompter Browser SDK

Add Backprompter AI agents to any web app. One script gives you end-user auth, agent calls, voice, conversation history, artifacts, custom data, and vector search, all scoped to your app.

Overview

The SDK ships as a hosted bundle. There is nothing to build or install from source. Include the script, set your App Key, sign a user in, then call an agent.

Calling from a server? For Node.js backends, use the Backprompter Server SDK instead.

Get your App Key

Your App Key identifies your app and tenant. Find it in the Backprompter dashboard. Open your app, then the Deploy modal. It also appears on the API Reference page.

Install

Include the bundle from the CDN. No build step is required.

<script src="https://backprompter-sdk.pages.dev/backprompter-full.min.js"></script>

<!-- Optional: only if your agents return math in artifacts -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />

Two bundles are published.

BundleContains
backprompter-core.min.js Auth, run, and voice.
backprompter-full.min.js Core, plus file reading and artifact rendering with Markdown and math. Use this one unless you need the smallest bundle.

The SDK is exposed as the global Backprompter.

Quick start

<script src="https://backprompter-sdk.pages.dev/backprompter-full.min.js"></script>
<script>
  Backprompter.setAppKey('YOUR_APP_KEY');        // 1. identify your app
  await Backprompter.signInAnonymously();        // 2. sign the end user in
  const r = await Backprompter.run({ userPrompt: 'Hello! What can you do?' });
  console.log(r.response);                       // 3. call an agent
</script>

Configuration

Backprompter.setAppKey('YOUR_APP_KEY');    // required
Backprompter.setBackendUrl('https://…');   // only if you self-host the backend
Backprompter.setEnvironment('production'); // or 'local', shorthand for setBackendUrl
Backprompter.getBackprompterURL();         // the backend URL currently in use

Authentication

Use whichever methods you enabled for your app.

await Backprompter.signIn(email, password);        // email and password
await Backprompter.signUp(email, password);        // self-service registration
await Backprompter.signInAnonymously();            // guest
await Backprompter.signInWithGoogle();             // Google OAuth popup
await Backprompter.signInWithSecret('your-code');  // shared secret code
await Backprompter.signOut();

const user = await Backprompter.currentUser();     // null when signed out

await Backprompter.onAuthChange((user) => {
  console.log(user ? `Signed in as ${user.email}` : 'Signed out');
});

signUp works only when self sign-up is enabled for the app. If you turned it off, create accounts from the dashboard under Users, then User Authentication. That either emails the person a link to choose their own password, or gives you a generated one to pass on.

Use your own login

You do not have to use Backprompter auth. If your app already has a login, keep it. Your backend mints a session token, and the browser signs in with that token. Every SDK function then works as normal, including saveData, search, file handling, and voice.

On your backend, using the Server SDK:

// behind your own session check
app.post('/api/bp-token', requireMyAuth, async (req, res) => {
  const { customToken } = await bp.createUserToken({
    externalId: req.user.id,     // your own user id
    email: req.user.email        // a label for your dashboard
  });
  res.json({ customToken });
});

In the browser, after your own login succeeds:

Backprompter.setAppKey('YOUR_APP_KEY');
await Backprompter.signInWithCustomToken(customToken);

await Backprompter.saveData('preferences', { theme: 'dark' });
await Backprompter.run({ agentId: 'analyst', userPrompt: 'Hello' });

signInWithExternalAuth wraps the fetch for you.

await Backprompter.signInWithExternalAuth(
  () => fetch('/api/bp-token', { credentials: 'include' }).then((r) => r.json())
);
Two rules keep this safe Your API key stays on your server. Only the token reaches the browser. A token is valid for one user of one app. It grants that person's own data and nothing else. It cannot reach developer functions, and it cannot reach another app.

Backprompter derives a storage id from your App Key and your external id. The same external id always returns to the same data. You cannot choose the id.

The session survives a page reload, because the auth library keeps a refresh token in the browser. Your own logout must therefore call Backprompter.signOut().

Password setup and reset

Accounts created from the dashboard, and end users who ask to reset, arrive on a link carrying a one-time code.

https://your-page.example.com/?mode=resetPassword&oobCode=…&tenantId=…

The hosted chat handles this already. You need the code below only if you point the app at your own page, under Users, User Authentication, then Use a custom password setup page.

const info = Backprompter.readPasswordSetup();   // null if this is not such a link
if (info) {
  // Verify first. An expired or used link should say so before
  // somebody types a password.
  const email = await Backprompter.verifyPasswordSetup(info.oobCode, info.tenantId);

  // …show your form, then:
  await Backprompter.completePasswordSetup(info.oobCode, newPassword, info.tenantId);
  await Backprompter.signIn(email, newPassword);
}

For a "forgot my password" link of your own:

await Backprompter.requestPasswordReset('person@example.com');

This always resolves the same way, and it never reveals whether the address has an account. The email carries your app name and points at whichever page you configured.

Why the link carries tenantId These calls set the tenant themselves, because the auth library does not remember one across page loads.

Run an agent

const result = await Backprompter.run({
  userPrompt: 'Summarize my last order.',
  // agentId: 'MyAgent',      // omit to auto-route to the best agent
  conversationHistory: [
    // { role: 'user', content: '…' }, { role: 'assistant', content: '…' },
  ],
  profileString: '',          // optional user-profile context
  // fileInputs: [],          // attachments, see below
  // urlInputs: [],           // URLs to attach as context
  // contextLimit: 120000,    // characters of history before auto-summarization
  // maxTokens: 1000,
  // ragCharBudget: 20000,
});

result.response;      // the agent's text reply
result.agentId;       // which agent handled it
result.artifactType;  // 'chat' | 'markdown' | 'html' | 'charts' | 'javascript'

Attachments

Inside fileInputs, the key you use decides how the attachment is sent.

KeySent as
url, dataUrl, base64image
file_url, file_datadocument

Backprompter infers the real type wherever it can. It reads an explicit mimeType, the data-URL prefix, the file extension, or the leading bytes of base64. It falls back to the key only when the type cannot be determined. A PDF passed as { url: '…/report.pdf' } is therefore still treated as a document. Pass mimeType when you have it and neither guess is available.

await Backprompter.run({
  userPrompt: 'What are the Q3 totals?',
  fileInputs: [
    { file_url: 'https://example.com/q3.pdf', filename: 'q3.pdf' },  // document
    { url: 'https://example.com/chart.png' },                        // image
  ],
});

Document support varies by model. OpenAI models accept spreadsheets. Anthropic models accept PDF only, and they reject other types outright. Choose a model that handles what you send.

Image links written in the prompt text are attached automatically, up to four per message. A pasted https://…/photo.jpg therefore reaches a vision model without appearing in fileInputs. Other links stay as text for the agent web-fetch tool, which returns page text and cannot see images.

Letting the browser read local files

runWithFiles takes File objects straight from an <input type="file">. It extracts text from PDFs and Office documents in the browser, and it sends images as attachments.

const result = await Backprompter.runWithFiles({
  userPrompt: 'Summarize these.',
  promptFiles: Array.from(document.getElementById('files').files),
});

This requires the full bundle.

Voice

const ws = await Backprompter.connectVoice({
  appId: 'YOUR_APP_ID',
  conversationHistory: [],
  onMessage: (msg) => {
    if (msg?.type === 'response.audio_transcript.delta') console.log(msg.delta || '');
  },
  onError: (err) => console.error(err),
  onClose: () => console.log('closed'),
});
// ws.send(JSON.stringify({ type: 'cancel_response' }));

Rendering agent artifacts

const html = Backprompter.renderArtifactHtml(result.response, { markdown: true, math: true });

Backprompter.renderArtifactElement(document.getElementById('pane'), result.response, {
  markdown: true, math: true,
});

Showing sources

When an agent answers from your indexed documents, the run returns the passages it was given. This adds a Show sources button under that answer; it does nothing when the answer used no retrieval, so it is safe to call for every message.

const extra = typeof result.additional_response === 'string'
  ? JSON.parse(result.additional_response)
  : (result.additional_response || {});

Backprompter.attachSourcesButton(bubbleEl, extra.rag?.docs, result.response);

Clicking it fills #artifactPaneContent with each passage: which document it came from, where in it, and the text with the indexing noise removed. One cheap model call then marks the sentences the answer rests on — it points at sentences by number and never writes any text, so what is shown is always exactly what your document says. That call is billed like any other agent call, which is why it happens on click.

Backprompter.showSources(docs, answerText);   // open the panel yourself
Backprompter.getSources(docs);                // [{ name, location, collection, text }]
await Backprompter.markSourceSentences({ answer, passages });

In a chat deployed from the builder, tick Show retrieved sources in the deploy dialog instead — no code needed.

Conversation history

await Backprompter.saveConversation(conversationId, messages, { title: 'My chat' });
const convo = await Backprompter.getConversation(conversationId); // .messages, .metadata
const list  = await Backprompter.listConversations();             // .conversations[]
await Backprompter.deleteConversation(conversationId);

Artifacts

Artifacts are stored with vector embeddings, so search can find them.

await Backprompter.storeArtifacts([
  { name: 'notes.md', content: '# Notes…', summary: 'Project notes', sourceType: 'text' },
]);
const arts = await Backprompter.listArtifacts();    // .artifacts[]
const art  = await Backprompter.getArtifact(docId); // .content, .name, …
await Backprompter.deleteArtifact(docId);

Custom data

Schemaless JSON documents with no embeddings. Use these for preferences and profiles.

await Backprompter.saveData('preferences', { theme: 'dark', language: 'en' });
const { data } = await Backprompter.getData('preferences');
await Backprompter.deleteData('preferences');
const result = await Backprompter.search('notes from last week', {
  searchIn: ['conversations', 'artifacts'],
  k: 5,
});

If you charge your end users, payment goes to your own Stripe account.

const { active } = await Backprompter.getEntitlements();
if (!active) {
  const { url } = await Backprompter.createCheckoutSession({
    successUrl: location.href + '?paid=1',
    cancelUrl: location.href,
  });
  location.href = url;
}

Backprompter gates a run only when you configure Stripe on the app. If you collect money yourself, leave the payment settings empty. No gate then applies to anyone.