Back to Docs
Feedback Collection
Collect customer feedback from your app using widgets, forms, or direct API calls.
New: narrated feedback
Instead of a form, let people record themselves using your app and talking through what they see. One call to sp.feedback.record() with the SDK. We record the mic only, never the screen, and work out which element "this button" meant. Read the guide →
import { useState } from 'react';
interface FeedbackWidgetProps {
apiKey: string;
userEmail?: string;
userName?: string;
}
export function FeedbackWidget({ apiKey, userEmail, userName }: FeedbackWidgetProps) {
const [isOpen, setIsOpen] = useState(false);
const [content, setContent] = useState('');
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus('sending');
try {
const res = await fetch('https://simpleproduct.dev/api/v1/feedback', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Feedback',
content,
email: userEmail,
name: userName,
source: 'react-widget',
metadata: { page: window.location.pathname },
}),
});
if (res.ok) {
setStatus('sent');
setTimeout(() => {
setIsOpen(false);
setStatus('idle');
setContent('');
}, 2000);
} else {
setStatus('error');
}
} catch {
setStatus('error');
}
};
if (!isOpen) {
return (
<button
onClick={() => setIsOpen(true)}
style={{
position: 'fixed',
bottom: '20px',
right: '20px',
padding: '12px 24px',
borderRadius: '8px',
border: 'none',
background: '#000',
color: '#fff',
cursor: 'pointer',
}}
>
Feedback
</button>
);
}
return (
<div style={{
position: 'fixed',
bottom: '20px',
right: '20px',
width: '320px',
padding: '20px',
borderRadius: '12px',
background: '#fff',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
<h3 style={{ margin: 0 }}>Send Feedback</h3>
<button onClick={() => setIsOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>✕</button>
</div>
{status === 'sent' ? (
<p>Thanks for your feedback!</p>
) : (
<form onSubmit={handleSubmit}>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="What's on your mind?"
required
style={{ width: '100%', minHeight: '100px', marginBottom: '12px', padding: '8px', borderRadius: '6px', border: '1px solid #ddd' }}
/>
<button
type="submit"
disabled={status === 'sending'}
style={{ width: '100%', padding: '10px', borderRadius: '6px', border: 'none', background: '#000', color: '#fff', cursor: 'pointer' }}
>
{status === 'sending' ? 'Sending...' : 'Send'}
</button>
{status === 'error' && <p style={{ color: 'red', marginTop: '8px' }}>Something went wrong. Please try again.</p>}
</form>
)}
</div>
);
}
// Usage in your app
function App() {
return (
<>
{/* Your app content */}
<FeedbackWidget
apiKey="YOUR_API_KEY"
userEmail={currentUser?.email}
userName={currentUser?.name}
/>
</>
);
}API Reference
POST /api/v1/feedback
Headers
Authorization: Bearer YOUR_API_KEY(required)Content-Type: application/json
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Feedback title/subject |
| content | string | Yes | The feedback message |
| string | No | Customer email (creates/links customer record) | |
| name | string | No | Customer name |
| externalId | string | No | Your user ID for identity matching |
| source | string | No | Where feedback came from (e.g., "widget", "nps") |
| metadata | object | No | Any custom JSON data |
Response
{
"data": {
"id": "abc123",
"title": "Feedback",
"content": "...",
"person": { "id": "...", "name": "John", "email": "john@example.com" },
"createdAt": 1234567890
}
}
Tips
- •Link feedback to customers: Include
emailto automatically create/link customer records - •Track sources: Use the
sourcefield to see where feedback comes from - •Add context: Use
metadatato include page URL, user agent, feature flags, etc.