Widget
The embeddable feedback widget lets your users submit feedback, bug reports, and questions directly from your app. It renders in a Shadow DOM for full style isolation and is under 30KB gzipped.
Script tag integration
Add a single script tag before </body>. Replace the API key with yours from the dashboard.
<script src="https://userdispatch.com/widget.js" data-api-key="pk_your-api-key" defer ></script>
The widget renders a floating feedback button. Users can submit feedback, bug reports (with screenshots via drag-and-drop), and questions.
Alternative: link-based integration
If you prefer not to use the floating widget, you can embed a simple link or button on your site that opens a hosted feedback page:
<a href="https://userdispatch.com/submit?key=pk_your-api-key"> Give Feedback </a>
This is useful for email signatures, documentation footers, or anywhere you want a lightweight feedback entry point without loading the widget script.
Framework guides
Use the Script component in your root layout:
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src="https://userdispatch.com/widget.js"
data-api-key="pk_your-api-key"
strategy="afterInteractive"
/>
</body>
</html>
);
}Configuration options
Configure the widget via data attributes on the script tag:
| Attribute | Required | Default | Description |
|---|---|---|---|
| data-api-key | Yes | — | Your app's API key (starts with pk_) |
| data-position | No | "br" | Trigger button position: "br" (bottom-right), "bl", "tr", "tl" |
| data-trigger-label | No | "Feedback" | Text on the trigger button |
| data-collect-email | No | "true" | Show email field. Set to "false" to hide. |
| data-collect-name | No | "false" | Show name field. Set to "true" to enable. |
| data-enable-logs | No | "false" | Enable built-in console log collector. Set to "true" to enable. Requires app-level log upload setting. |
| data-api-url | No | (auto-detected) | Override the API base URL. Detected automatically from the script src. |
JavaScript API
For programmatic control, use the global UserDispatch object:
// Initialize with custom config
const widget = UserDispatch.init({
apiKey: "pk_your-api-key",
position: "br",
triggerLabel: "Send Feedback",
darkMode: true,
types: ["general", "support", "bug"],
collectEmail: true,
collectName: false,
metadata: { plan: "pro", version: "2.1.0" },
userContext: { id: "user_123", name: "Jane" },
onSubmit: (result) => console.log("Submitted:", result),
onError: (error) => console.error("Error:", error),
});
// Later: remove the widget
widget.destroy();Programmatic submission (no UI)
await UserDispatch.submit({
apiKey: "pk_your-api-key",
type: "bug",
email: "user@example.com",
subject: "Page crash on checkout",
message: "The page freezes when clicking Pay",
metadata: { page: "/checkout", cartId: "abc123" },
});SDK (@userdispatch/sdk)
The TypeScript SDK provides a clean API for server-side or programmatic use:
npm install @userdispatch/sdk
import { UserDispatch } from "@userdispatch/sdk";
const ud = new UserDispatch({ apiKey: "pk_your-api-key" });
// Submit feedback
await ud.submit({
type: "feedback",
message: "Love the new dashboard!",
rating: 5,
email: "user@example.com",
});
// Submit an automated log
await ud.submitLog({
message: "Unhandled promise rejection in /api/checkout",
metadata: {
stack: "Error: timeout at fetch.js:42",
severity: "error",
},
});Submission types
| Widget type | DB type | Description |
|---|---|---|
| general | feedback | General feedback with optional star rating |
| support | question | Support questions |
| bug | bug_report | Bug reports with subject line and file attachments |
| — | automated_log | Automated logs via SDK (no widget equivalent) |
Allowed origins
Each app can restrict which origins may send feedback. Set the list under App settings → Allowed origins, or with the allowed_origins field of the apps MCP tool and the admin API. An empty list accepts every origin. Once the list has an entry, a request whose Origin header is not on it is refused with 403 Origin not allowed.
An origin is scheme://host[:port]. Paths, trailing slashes and upper case are normalized away, and wildcards are not supported.
Native apps and web views
A web view sends its own origin, not your website's. If the widget runs inside a native shell, add that shell's origin as well as your website:
| Shell | Origin to allow |
|---|---|
| Capacitor, iOS | capacitor://localhost |
| Capacitor, Android | https://localhost (the default androidScheme) |
| Ionic or Cordova, iOS | ionic://localhost |
| Cordova, Android | http://localhost (older builds: http://localhost:8080) |
| React Native WebView | The origin of the page it loads, usually your https:// site. A WebView given inline HTML sends null: load a real URL instead. |
| Electron | A page loaded from file:// sends null and cannot be allowed. Serve it from a custom scheme or https://localhost. |
If your Capacitor config changes server.iosScheme, server.androidScheme or server.hostname, allow the resulting origin instead (for example capacitor://myapp). Native HTTP clients, including React Native's own fetch, send no Origin header and are refused whenever a list exists: submit from a server, or leave the list empty.
Diagnosing a 403
- The widget shows the refused origin in its error message, and the 403 response body carries
{ "code": "origin_not_allowed", "origin": "..." }. - The app's settings page lists origins refused in the last 7 days with an Allow this origin button. Counts are a lower bound.
- No origin shown? The request carried no
Originheader, which happens with server-side calls. Leave the list empty, or send submissions from an allowed browser origin.
Ramble needs an https origin
Turning Ramble on adds the Ramble pilot origin to the list when it is missing, which makes the list a restriction. If the list is empty today, the dashboard asks you to confirm once before it does that, so add your native shells first. While Ramble is on, the list must keep that origin; other origins and schemes can sit alongside it. Turning Ramble off removes the origin again, but only if Ramble added it. Origins you entered yourself are never removed.
File uploads
Users can attach files by dragging and dropping anywhere on the widget modal. Supported formats include images (PNG, JPG, GIF, WebP), documents (PDF), and text files. Files are attached to the submission and accessible from the dashboard.
Browser metadata
Every submission automatically captures:
- User agent string
- Viewport dimensions
- Page URL
- Operating system
- Screen resolution
- Custom metadata (via
data-*attributes or JS API)