# Usero developer docs Usero turns user feedback into shipped code. This file contains every page of the developer docs (https://usero.io/docs) as one markdown document. Per-page index: https://usero.io/llms.txt --- # Send your first feedback in 5 minutes One POST. No SDK, no API key. Works from any language, any platform. ## Step 1: Get your clientId Your clientId identifies your project. It starts with `client_`. - Logged in? Every snippet on this page already shows your real clientId. - No account yet? [Sign up free](/signup) and your clientId is on the first screen. Until then, snippets show the placeholder `YOUR_CLIENT_ID`, swap it for your own before sending. More detail: [Find your clientId](/docs/find-your-client-id). ## Step 2: Send a POST ```bash title=curl curl -X POST https://usero.io/api/feedback \ -H "Content-Type: application/json" \ -d '{ "clientId": "YOUR_CLIENT_ID", "rating": 4, "comment": "First feedback from the quickstart" }' ``` ```javascript title=JavaScript const res = await fetch('https://usero.io/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: 'YOUR_CLIENT_ID', rating: 4, comment: 'First feedback from the quickstart', }), }) console.log(await res.json()) // { success: true, feedbackId: "..." } ``` ```swift title=Swift var request = URLRequest(url: URL(string: "https://usero.io/api/feedback")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: [ "clientId": "YOUR_CLIENT_ID", "rating": 4, "comment": "First feedback from my Mac app", ]) let (data, _) = try await URLSession.shared.data(for: request) print(String(data: data, encoding: .utf8)!) // {"success":true,"feedbackId":"..."} ``` ```python title=Python import requests res = requests.post( "https://usero.io/api/feedback", json={ "clientId": "YOUR_CLIENT_ID", "rating": 4, "comment": "First feedback from the quickstart", }, ) print(res.json()) # {'success': True, 'feedbackId': '...'} ``` A successful response is: ```json { "success": true, "feedbackId": "abc123" } ``` `rating` is 1 to 4 (1 Terrible, 2 Bad, 3 Good, 4 Amazing). Every submission needs a `rating`, a non-empty `comment`, or both. Full field list: [POST /api/feedback reference](/docs/api/feedback). > [!WARNING] **Omit `environment` for your default environment.** Do not send a placeholder like `"no-env"` or `"default"`. The > dashboard treats an absent environment as the default; a literal placeholder string creates a separate environment and your > feedback will not appear in the default inbox. ## Step 3: See it land Open your [dashboard](/) and check the feedback inbox. Your POST shows up within a second or two. If you sent an `environment` value, switch the environment picker to that value to see it. ## Or let your coding agent do it Paste this prompt into Cursor, Claude Code, or any coding agent and it will build a feedback form wired to your account. ```text title=Agent prompt # Task: Add Feedback Collection to This Project Build a feedback UI that submits to Usero's API. The goal is to collect user satisfaction ratings and optional comments. ## What to build A feedback form or prompt with: - A 4-level rating (1 = Terrible, 2 = Bad, 3 = Good, 4 = Amazing) - An optional comment text field - A submit button that POSTs to the Usero API ## API Details POST https://usero.io/api/feedback Content-Type: application/json ### Request body { "clientId": "YOUR_CLIENT_ID", // required — your client ID "rating": 3, // required — 1 to 4 "comment": "User's feedback", // optional "environment": "production", // optional — helps filter by env in dashboard "pageUrl": "https://...", // optional — auto-detect from window.location "pageTitle": "...", // optional — auto-detect from document.title "userEmail": "user@example.com" // optional } ### Response 200: { "success": true, "feedbackId": "abc123" } ## Working example curl -X POST https://usero.io/api/feedback \ -H "Content-Type: application/json" \ -d '{ "clientId": "YOUR_CLIENT_ID", "rating": 3, "comment": "Love the new dashboard!", "pageUrl": "https://yourapp.com/page", "environment": "production" }' ## Tips - Auto-detect pageUrl and pageTitle from the browser so you get per-page feedback tracking. - Set environment to distinguish production feedback from staging/dev. - Show a success message after submission. Only clientId and rating are required. ``` ## Next steps - [POST /api/feedback reference](/docs/api/feedback): every field, response shape, and error code - [Screenshot uploads](/docs/api/screenshots): attach images to feedback - [Drop-in widget](/docs/widget): React, vanilla JS, or a script tag, no form to build - [Integrations](/docs/integrations): GitHub, Slack, email, WordPress, and more --- # API reference: POST /api/feedback Create one feedback item. This is the same endpoint the widget uses, and it is open to direct calls from any language. ## Request ```text title=Endpoint POST https://usero.io/api/feedback Content-Type: application/json ``` - **CORS:** `Access-Control-Allow-Origin: *`. Call it straight from browsers, mobile apps, desktop apps, or servers. - **Auth:** the `clientId` in the body identifies your project. No API key, no auth header. - **Abuse control:** if your client has a domain allowlist configured in Settings, requests from other origins get a `403`. New clients have no allowlist, so everything is accepted. ## Required fields `clientId` is always required. On top of that, every submission needs a `rating` (1 to 4), a non-empty `comment`, or both. Everything else is optional. ## Fields | Field | Type | Required | Description | | ----------------- | ------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clientId` | string | yes | Your project id, starts with `client_`. See [Find your clientId](/docs/find-your-client-id). | | `rating` | integer | one of rating / comment | 1 to 4: 1 Terrible, 2 Bad, 3 Good, 4 Amazing. | | `comment` | string | one of rating / comment | Free-text feedback. Must be non-empty if no rating is sent. | | `userEmail` | string | no | The submitter's email, must be a valid address. Lets you reply and groups feedback by person in the dashboard. An empty string is treated as absent. | | `pageUrl` | string | no | URL the feedback was given on. If omitted, the server falls back to the `Referer` header. | | `pageTitle` | string | no | Title of the page the feedback was given on. | | `referrer` | string | no | The page the user arrived from. | | `environment` | string | no | Tags the feedback with an environment (for example `staging`). See the warning below before sending this. | | `screenshots` | array | no | Screenshot objects returned by [POST /api/screenshots](/docs/api/screenshots). Each item: `fileName` (string), `url` (string), `fileSize` (number), `mimeType` (string), `width` (number, optional), `height` (number, optional). | | `metadata` | object | no | Arbitrary JSON attached to the feedback, 10KB max when serialized. Good place for app version, OS, build number, or feature flags. | | `sessionReplayId` | string | no | 1 to 128 characters. Links the feedback to a session replay recorded by the [session replay plugin](/docs/widget/session-replay). | | `replayOffsetMs` | integer | no | Non-negative millisecond offset into the linked replay at which the feedback was given. | > [!WARNING] **Omit `environment` for your default environment.** Do not send a placeholder like `"no-env"` or `"default"`. The > dashboard treats an absent environment as the default; a literal placeholder string creates a separate environment and your > feedback will not appear in the default inbox. ## Examples Minimal submission: ```bash title=curl curl -X POST https://usero.io/api/feedback \ -H "Content-Type: application/json" \ -d '{"clientId": "YOUR_CLIENT_ID", "rating": 3}' ``` ```javascript title=JavaScript await fetch('https://usero.io/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: 'YOUR_CLIENT_ID', rating: 3 }), }) ``` ```swift title=Swift var request = URLRequest(url: URL(string: "https://usero.io/api/feedback")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: [ "clientId": "YOUR_CLIENT_ID", "rating": 3, ]) let (data, _) = try await URLSession.shared.data(for: request) ``` ```python title=Python import requests requests.post( "https://usero.io/api/feedback", json={"clientId": "YOUR_CLIENT_ID", "rating": 3}, ) ``` Full-field submission: ```bash title=curl curl -X POST https://usero.io/api/feedback \ -H "Content-Type: application/json" \ -d '{ "clientId": "YOUR_CLIENT_ID", "rating": 2, "comment": "Export to CSV times out on large projects", "userEmail": "jamie@example.com", "pageUrl": "https://yourapp.com/projects/42/export", "pageTitle": "Export project", "referrer": "https://yourapp.com/projects/42", "environment": "staging", "metadata": { "appVersion": "2.4.1", "os": "macOS 15.2", "build": 1842 } }' ``` ```javascript title=JavaScript await fetch('https://usero.io/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: 'YOUR_CLIENT_ID', rating: 2, comment: 'Export to CSV times out on large projects', userEmail: 'jamie@example.com', pageUrl: window.location.href, pageTitle: document.title, referrer: document.referrer, environment: 'staging', metadata: { appVersion: '2.4.1', os: navigator.platform, build: 1842 }, }), }) ``` ```swift title=Swift var request = URLRequest(url: URL(string: "https://usero.io/api/feedback")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONSerialization.data(withJSONObject: [ "clientId": "YOUR_CLIENT_ID", "rating": 2, "comment": "Export to CSV times out on large projects", "userEmail": "jamie@example.com", "environment": "staging", "metadata": [ "appVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "unknown", "os": ProcessInfo.processInfo.operatingSystemVersionString, ], ]) let (data, _) = try await URLSession.shared.data(for: request) ``` ```python title=Python import requests requests.post( "https://usero.io/api/feedback", json={ "clientId": "YOUR_CLIENT_ID", "rating": 2, "comment": "Export to CSV times out on large projects", "userEmail": "jamie@example.com", "environment": "staging", "metadata": {"appVersion": "2.4.1", "build": 1842}, }, ) ``` For native apps, `metadata` is the place for app version, OS version, and build number, so every report arrives with the context you need to reproduce it. ## Response Success: ```json { "success": true, "feedbackId": "abc123" } ``` ## Errors | Status | Body | Meaning and fix | | ------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | 400 | `{"error": "Invalid data provided", "issues": {"rating": ["..."]}}` | Validation failed. `issues` names each failing field. The most common cause: neither `rating` nor a non-empty `comment` was sent. | | 400 | `{"error": "Metadata too large (max 10KB)"}` | Serialized `metadata` exceeds 10KB. Trim it. | | 403 | `{"error": "Domain not allowed"}` | Your client has a domain allowlist and this request's origin is not on it. Add the origin in Settings, or clear the allowlist. | | 500 | `{"error": "Internal server error"}` | Something broke on our side. Safe to retry. | ## Related - [Screenshot uploads](/docs/api/screenshots): two-step flow to attach images - [Session replay plugin](/docs/widget/session-replay): where `sessionReplayId` comes from - [Find your clientId](/docs/find-your-client-id) --- # API reference: screenshot uploads Attaching an image to feedback is a two-step flow: upload the file here first, then include the returned screenshot object in the `screenshots` array of your [POST /api/feedback](/docs/api/feedback) call. ## Request ```text title=Endpoint POST https://usero.io/api/screenshots Content-Type: multipart/form-data ``` - **CORS:** `Access-Control-Allow-Origin: *`, callable directly from browsers and apps. - **Auth:** the `clientId` form field identifies your project. No API key. ### Form fields | Field | Type | Required | Description | | ------------ | ------ | -------- | --------------------------------------------------- | | `screenshot` | file | yes | The image. Any `image/*` MIME type, 10MB max. | | `clientId` | string | yes | Your project id. Must belong to an existing client. | One file per request. The widget allows up to 3 screenshots per feedback; if you need several, send several uploads. ## Examples ```bash title=curl curl -X POST https://usero.io/api/screenshots \ -F "screenshot=@./bug.png" \ -F "clientId=YOUR_CLIENT_ID" ``` ```javascript title=JavaScript const form = new FormData() form.append('screenshot', fileInput.files[0]) form.append('clientId', 'YOUR_CLIENT_ID') const res = await fetch('https://usero.io/api/screenshots', { method: 'POST', body: form, // do NOT set Content-Type yourself, the browser adds the boundary }) const { screenshot } = await res.json() ``` ```swift title=Swift let boundary = UUID().uuidString var request = URLRequest(url: URL(string: "https://usero.io/api/screenshots")!) request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var body = Data() body.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"clientId\"\r\n\r\nYOUR_CLIENT_ID\r\n".data(using: .utf8)!) body.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"screenshot\"; filename=\"bug.png\"\r\nContent-Type: image/png\r\n\r\n".data(using: .utf8)!) body.append(pngData) // your image bytes body.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = body let (data, _) = try await URLSession.shared.data(for: request) ``` ```python title=Python import requests res = requests.post( "https://usero.io/api/screenshots", files={"screenshot": open("bug.png", "rb")}, data={"clientId": "YOUR_CLIENT_ID"}, ) print(res.json()) ``` ## Response ```json { "success": true, "screenshot": { "fileName": "client_abc/1717740000000-x7k2p.png", "url": "https://usero.io/api/screenshots/client_abc/1717740000000-x7k2p.png", "fileSize": 48211, "width": 1280, "height": 800, "mimeType": "image/png" } } ``` ## Attach it to feedback Pass the whole `screenshot` object inside the `screenshots` array when you submit the feedback: ```bash title=curl curl -X POST https://usero.io/api/feedback \ -H "Content-Type: application/json" \ -d '{ "clientId": "YOUR_CLIENT_ID", "comment": "Button overlaps the footer, screenshot attached", "screenshots": [ { "fileName": "client_abc/1717740000000-x7k2p.png", "url": "https://usero.io/api/screenshots/client_abc/1717740000000-x7k2p.png", "fileSize": 48211, "width": 1280, "height": 800, "mimeType": "image/png" } ] }' ``` ```javascript title=JavaScript await fetch('https://usero.io/api/feedback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: 'YOUR_CLIENT_ID', comment: 'Button overlaps the footer, screenshot attached', screenshots: [screenshot], // the object from the upload response }), }) ``` ## Errors | Status | Body | Meaning and fix | | ------ | ------------------------------------------ | ------------------------------------------------------------------------------------- | | 400 | `{"error": "Screenshot file is required"}` | The `screenshot` form field is missing. | | 400 | `{"error": "Client ID is required"}` | The `clientId` form field is missing. | | 400 | `{"error": "File must be an image"}` | The MIME type is not `image/*`. | | 400 | `{"error": "File too large (max 10MB)"}` | The file exceeds 10MB. | | 403 | `{"error": "Domain not allowed"}` | Your client has a domain allowlist and this origin is not on it. | | 404 | `{"error": "Invalid client ID"}` | No client with that id exists. Check [Find your clientId](/docs/find-your-client-id). | | 500 | `{"error": "Failed to upload screenshot"}` | Something broke on our side. Safe to retry. | --- # Feedback widget (@usero/sdk) A drop-in feedback button for the web. Vanilla JS, React component, or a ` ``` `unpkg` and `jsDelivr` both serve the IIFE bundle automatically. > [!WARNING] **Omit the `environment` option for your default environment.** Do not pass a placeholder like `"no-env"` or > `"default"`. The dashboard treats an absent environment as the default; a placeholder string creates a separate environment and > your feedback will not appear in the default inbox. ## Identify your users (optional) The widget works fully anonymous. If you have a logged-in user, identifying them lights up "who is this person" on session replays and lets you filter feedback by email in the dashboard. React: pass the `user` prop (omit or pass `null` for logged-out visitors). The widget re-identifies automatically when the prop changes: ```tsx title=React ``` Vanilla: pass a `getUser` callback, called at session start: ```ts title=Vanilla initUseroFeedbackWidget({ clientId: 'YOUR_CLIENT_ID', getUser: () => (currentUser ? { id: currentUser.id, email: currentUser.email } : null), }) ``` ## Options | Option | Type | Default | Description | | ---------------------- | ------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `clientId` | `string` | required | Your Usero client id. See [Find your clientId](/docs/find-your-client-id). | | `position` | `'left' \| 'right'` | `'right'` | Which side of the viewport the button sits on. | | `theme` | `Partial` | auto | Override colors. By default the widget follows the OS color scheme (`prefers-color-scheme`), with dark as the fallback. Explicit values win; partial overrides merge on top of the detected base. | | `title` | `string` | `'Share Feedback'` | Panel header. | | `placeholder` | `string` | `'Tell us what you think... (optional)'` | Comment placeholder. | | `showEmailOption` | `boolean` | `true` | Show the "share my email" checkbox. | | `showScreenshotOption` | `boolean` | `true` | Show the screenshot upload button (up to 3 images, 10MB each). | | `environment` | `string` | undefined | Tag feedback with an environment. Omit for your default environment, see the warning above. | | `metadata` | `Record` | undefined | Arbitrary metadata attached to every submission. | | `baseUrl` | `string` | `'https://usero.io'` | Override the API host (self-hosted Usero). | | `plugins` | `UseroPlugin[]` | undefined | Opt-in plugins, for example [session replay](/docs/widget/session-replay). | | `getUser` | `() => User \| null` | undefined | Vanilla only. Returns the current logged-in user (or null for anonymous). React uses the `user` prop instead. | | `onSubmit` | `(data) => void` | undefined | Fires after a successful submission. | | `onError` | `(err: Error) => void` | undefined | Fires on init or submission error. | | `onOpen` / `onClose` | `() => void` | undefined | Fire when the panel opens or closes. | ## Content Security Policy If your app has a strict CSP, allow `https://usero.io` in `connect-src` (and `https://unpkg.com` in `script-src` if you use the script tag). The widget makes no other cross-origin requests. ## Next - [Session replay plugin](/docs/widget/session-replay): attach a recording of the last 30 seconds to each feedback - [POST /api/feedback](/docs/api/feedback): the API the widget submits to, if you would rather build your own UI --- # Session replay plugin An opt-in plugin for the [widget](/docs/widget) that attaches a rolling recording (the last 30 seconds by default) to each feedback submission. When a user reports a bug, you watch exactly what they did instead of asking for reproduction steps. Recordings use rrweb and are gzipped in the browser via the native CompressionStream API. In the dashboard, each feedback with a replay gets a "Watch session replay" link that opens the player at the moment the feedback was given. ## Install `rrweb` ships inside the plugin chunk, so `npm install @usero/sdk` is the only install step. The plugin lives in a subpath export; consumers who never import it pay zero rrweb bytes. ```ts title=Vanilla import { initUseroFeedbackWidget } from '@usero/sdk' import { sessionReplay } from '@usero/sdk/plugins/session-replay' initUseroFeedbackWidget({ clientId: 'YOUR_CLIENT_ID', plugins: [ sessionReplay({ bufferSeconds: 30, // Wait 3s of engagement before loading rrweb. If the user navigates // away first, rrweb is never fetched. startAfterMs: 3000, // Record 50% of sessions. Decided once at init. sampleRate: 0.5, }), ], }) ``` ```tsx title=React import { UseroFeedbackWidget } from '@usero/sdk/react' import { sessionReplay } from '@usero/sdk/plugins/session-replay' ; ``` Even when the plugin is imported, rrweb itself lazy-loads at runtime via dynamic `import()` the first time the engagement gate elapses, so opted-in consumers do not pay rrweb's bytes upfront either. ## Options and privacy defaults | Option | Default | What it does | | ------------------ | -------------------------------- | --------------------------------------------------------- | | `maskAllInputs` | `true` | Mask `` and `