API Reference
Webhook Payloads
Webhook event formats and payload reference
Webhook Payloads
Reference documentation for webhook event payloads.
Event Types
| Event | Description |
|---|---|
feedback.created | New feedback submitted |
feedback.updated | Feedback status changed |
Common Structure
All webhook payloads share this structure:
{
"event": "string",
"timestamp": "ISO 8601 datetime",
"data": { ... }
}feedback.created
Sent when new feedback is submitted.
{
"event": "feedback.created",
"timestamp": "2026-01-07T15:30:00.000Z",
"data": {
"id": "fb_abc123xyz",
"projectId": "proj_xyz789",
"widgetId": "w_default",
"widgetName": "Main Feedback Widget",
"template": "floating",
"message": "The export button doesn't work when I select more than 100 items. I've tried multiple times with different browsers.",
"category": "bug",
"rating": 2,
"email": "user@example.com",
"status": "new",
"screenshotUrl": "https://storage.userhero.co/screenshots/fb_abc123xyz.png",
"context": {
"pageUrl": "/dashboard/reports",
"fullUrl": "https://app.example.com/dashboard/reports?view=list",
"referrer": "https://app.example.com/dashboard",
"browser": "Chrome",
"browserVersion": "120.0.6099.129",
"os": "macOS",
"osVersion": "14.2",
"deviceType": "desktop",
"viewport": {
"width": 1920,
"height": 1080
},
"timezone": "America/New_York",
"locale": "en-US",
"country": "US"
},
"metadata": {
"userId": "user_123",
"plan": "pro",
"company": "Acme Inc",
"accountId": "acc_456",
"role": "admin"
},
"createdAt": "2026-01-07T15:30:00.000Z"
}
}Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique feedback ID |
projectId | string | Project ID |
widgetId | string | Widget ID |
widgetName | string | Widget display name |
template | string | Widget template (floating, bug-report, rating) |
message | string | User's feedback text |
category | string | null | Selected category |
rating | number | null | Rating score (1-5 or 1-10) |
email | string | null | User's email if provided |
status | string | Feedback status (new, in-progress, resolved, archived) |
screenshotUrl | string | null | Screenshot URL if captured |
context | object | Automatically collected context |
metadata | object | null | Custom metadata from setMetadata() |
createdAt | string | ISO 8601 timestamp |
Context Object
| Field | Type | Description |
|---|---|---|
pageUrl | string | Page path |
fullUrl | string | Complete URL (if enabled) |
referrer | string | Previous page URL |
browser | string | Browser name |
browserVersion | string | Browser version |
os | string | Operating system |
osVersion | string | OS version |
deviceType | string | desktop, mobile, tablet |
viewport.width | number | Screen width |
viewport.height | number | Screen height |
timezone | string | IANA timezone |
locale | string | Language locale |
country | string | 2-letter country code |
feedback.updated
Sent when feedback status is changed.
{
"event": "feedback.updated",
"timestamp": "2026-01-07T16:45:00.000Z",
"data": {
"id": "fb_abc123xyz",
"projectId": "proj_xyz789",
"previousStatus": "new",
"newStatus": "in-progress",
"updatedBy": {
"id": "user_team123",
"email": "team@example.com",
"name": "John Smith"
},
"updatedAt": "2026-01-07T16:45:00.000Z"
}
}Signature Verification
Webhooks include a signature header for verification:
X-UserHero-Signature: sha256=abc123def456...Verification Code
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage
app.post('/webhook', (req, res) => {
const signature = req.headers['x-userhero-signature'];
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
const { event, data } = req.body;
// ...
res.status(200).json({ received: true });
});Response Requirements
Your endpoint should:
- Return
2xxstatus code - Respond within 30 seconds
- Return quickly (process async if needed)
Retry Policy
Failed deliveries are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 24 hours |
After 6 failures, webhook is marked as failing.
Testing
Use the test webhook feature in your dashboard:
- Go to Integrations → Webhooks
- Click "Send Test Event"
- Select event type
- Verify your endpoint receives it
Best Practices
- Verify signatures: Always verify the signature in production
- Respond fast: Return 200 immediately, process async
- Idempotency: Handle duplicate events gracefully
- Logging: Log webhook payloads for debugging
- Error handling: Return 5xx for retryable errors, 4xx for permanent failures