JavaScript API
Start, open and close the widget from your own code, identify visitors and react to events.
Once widget.js has loaded, the widget is controlled through window.Pinwren. With the npm package, import
the same functions instead — import { init, open, on } from "@pinwren/widget" — they queue calls until the
widget has loaded. Every method is wrapped: a
bad argument or an internal error is logged as [pinwren] … and never thrown into your code.
Methods
| Method | What it does |
|---|---|
Pinwren.init(options) | Start the widget. Needed when the script tag has no data-project, or with the npm package. A second call is ignored — one widget per page. |
Pinwren.open(mode?) | "comment" starts comment mode, "bug" opens a bug report, "panel" opens the sidebar, "page" opens a comment on the whole page (no pin); no argument opens the launcher's actions. Calls made before the widget is ready are kept and run once it is. |
Pinwren.close() | Close whatever is open: menu, comment mode, card, panel or bug report. |
Pinwren.on(event, handler) | Subscribe to an event. Returns a function that unsubscribes. |
Pinwren.identify(user, options?) | Prefill the reporter's name and email; { id } plus { signature } makes a verified reporter — see Identity. |
Pinwren.destroy() | Full teardown: removes the DOM, every listener, the console/network wrappers and timers. init() can start it again. |
Pinwren.setPageScope(scope) | Group pins by a URL pattern — see page scope. |
Pinwren.setMetadata(object) | Context sent with every bug report and comment from now on — see metadata. null clears it. Put release in it to say which version of your site the report came from (the same as data-release). |
Pinwren.show() / Pinwren.hide() | Show or hide the whole widget, overriding the project's visibility mode until the other one is called. |
open("comment") and open("bug") do nothing when that mode is turned off for the project or the project is
read-only, and open() does nothing while the visibility rules hide the widget.
init options
Prop
Type
Pinwren.init({ projectKey: "pk_…", endpoint: "https://feedback.example.com", locale: "de" });Metadata
Pinwren.setMetadata() attaches your own context to everything the visitor sends — the plan they are on, an
account id, the feature flags in effect — so the team doesn't have to ask. It shows on the item's Details in the
dashboard and in the API (feedback.metadata).
Pinwren.setMetadata({ plan: "pro", accountId: "acc_42", flags: ["new-checkout"] });It takes a plain JSON object of at most 14 KB (the whole metadata of a report is capped at 16 KB; the widget's
own page timings use the rest). Anything else — an array, a class instance, an object with cycles, or too much
data — is refused with a [pinwren] console warning and the previous value stays. Call it again whenever the
context changes. Like identify, it may be called before the widget has loaded. With React, pass the
metadata prop of <Pinwren> (compared by value) and visible to show or hide it.
To link a report to a monitoring tool, put its ids under observability; the dashboard turns them into
“Open in …” buttons, and the widget fills in Sentry, PostHog, LogRocket and Datadog by itself. See
Session and trace links.
Events
| Event | Handler receives | Fires when |
|---|---|---|
ready | nothing | The configuration is loaded and the pins are placed. A handler added later still runs once. |
open | { mode } | The widget goes from idle to open. mode is what opened first: "menu", "comment", "bug", "panel", "composer" or "thread". |
close | nothing | Nothing is open any more. |
submit | { id, number, kind } | The server stored a comment or bug report — including a queued report sent later. |
error | { message } | Sending, replying, resolving or loading a part of the widget failed. message is the text shown to the visitor. |
const off = Pinwren.on("submit", ({ id, number, kind }) => {
analytics.track("feedback_sent", { id, number, kind });
});
Pinwren.on("error", ({ message }) => console.warn("Pinwren:", message));
// later
off();Handlers run inside a try/catch: an exception in your handler is logged and doesn't affect the widget or
other handlers. open and close fire once per session of use — moving from the menu to comment mode doesn't
emit a close in between.
Calling the API before the script loads
With async, widget.js may load after your own code. Queue calls on a stub and the widget replays them, in
order, as soon as it loads:
<script>
window.Pinwren = window.Pinwren || { q: [] };
window.Pinwren.q.push(["identify", { name: "Ada Lovelace", email: "ada@example.com" }]);
window.Pinwren.q.push(["on", "submit", (e) => console.log("Feedback #" + e.number)]);
</script>
<script src="https://feedback.example.com/widget.js" data-project="pk_…" async></script>Each entry is [method, ...arguments]. Unknown methods are skipped.
Your own launcher
Hide the built-in launcher (Hide the launcher on the project's Widget tab, launcher.hidden) and open the widget from your
UI:
<button type="button" onclick="Pinwren.open('comment')">Comment on this page</button>
<button type="button" onclick="Pinwren.open('bug')">Report a problem</button>The keyboard shortcuts (C, B) keep working unless you turn them off too.
Single-page apps
Nothing to wire up: the widget watches the URL and reloads the pins of the new page on navigation. If your
framework replaces the whole document body, the widget survives — it is attached to <html>, not <body>.
Call Pinwren.destroy() when you want it gone, for example on a route where feedback makes no sense.
The npm package ships TypeScript types for this API (PinwrenApi, PinwrenOptions, PinwrenUser, the
event payloads), and the React component wraps it — see Install.