Triggers & JavaScript API

The floating button is the default, not the only option. You can open the panel from your own UI, from a keyboard shortcut, or at a moment that matters in your product, and attach who the person is before they write a word.

Open it from your own button

Put data-voicebox-trigger on any element. No JavaScript, and it works on elements added to the page later, since the listener is delegated from document.

anywhere in your markup
<button data-voicebox-trigger>Send feedback</button>

<!-- Works on links, menu items, anything clickable -->
<a href="#" data-voicebox-trigger>Report a problem</a>

Turn off the floating button under Widget → Trigger when you are supplying your own, otherwise you will have two ways in.

Clicks during page load are not lost

The listener is attached the moment the script executes, before the widget has finished loading its configuration. A click in that window is remembered and the panel opens as soon as it is ready, so your button is never inert.

The JavaScript API

A single global function, available as soon as the script runs.

CallDoes
Voicebox("open")Opens the panel. Queues if still loading.
Voicebox("close")Closes the panel.
Voicebox("identify", {…})Attaches traits to every submission from this visitor.

A keyboard shortcut

app.js
document.addEventListener("keydown", (e) => {
  // ⌘/ or Ctrl+/ opens feedback from anywhere
  if ((e.metaKey || e.ctrlKey) && e.key === "/") {
    e.preventDefault();
    Voicebox("open");
  }
});

Esc closes the panel already, you do not need to wire that up.

Ask at the right moment

The best feedback arrives just after something notable happened. Open the panel then, rather than hoping people find the button later.

app.js
// After a task that took real effort
async function onExportFinished() {
  Voicebox("identify", { lastAction: "export", exportRows: 12400 });
  Voicebox("open");
}

// After someone hits an error, while it's fresh
function onPaymentFailed(code) {
  Voicebox("identify", { errorCode: code, screen: "checkout" });
  Voicebox("open");
}

Do this sparingly

An unprompted panel is an interruption. Once, after a meaningful moment, reads as attentive. On every page load it reads as a pop-up and people learn to dismiss it without reading.

Attaching who they are

Call identify once you know who the visitor is, usually right after your own auth resolves. Traits are held for the session and attached to anything they submit, so feedback arrives with the account already on it and you can reply without asking who they are.

app.js
Voicebox("identify", {
  userId: "usr_8123",
  plan: "pro",
  company: "Acme",
  signedUpAt: "2026-02-14",
  seats: 25,
});

Calls merge rather than replace, so you can add traits as you learn them. Up to 30 keys and 4KB per submission. Go over and the extra traits are trimmed, never the message: an over-filled identify call should not cost you what somebody took the time to write.

Traits never reach the model

Only the message text, the chosen type, and the rating are sent for analysis. Identity traits and the optional email address are stored on the record and shown to you, and are deliberately excluded from what goes to the AI provider. See Security & privacy.

Collecting without the widget

A mobile app, a CLI, a support inbox, a Discord bot: anything that can make an HTTP request can file feedback, and it lands in the same inbox and the same themes as widget submissions.

POST /api/ingest
curl -X POST https://www.usevoicebox.dev/api/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "key": "pk_live_your_key",
    "body": "The CSV export drops the last row.",
    "type": "ISSUE",
    "rating": 2,
    "email": "sam@acme.com",
    "metadata": { "source": "ios-app", "version": "3.4.1" }
  }'

Full request and response shape is in the API reference. Note that server-to-server calls have no Origin header, so if you have set a domain allowlist you will need to send from the browser or leave the allowlist empty for that project.