REST API
Authenticate and interact with UserHero resources via the REST API
REST API
Use the UserHero REST API to manage feedback, projects, widgets, and webhooks programmatically.
Authentication
All endpoints require a Bearer token. Create an API key from Account > API Keys in the dashboard.
curl https://userhero.co/api/v1/feedback \
-H "Authorization: Bearer sk_live_your_key"API keys are scoped to your user account. You must be an owner or admin of the workspace, and the workspace must have API Access enabled in Settings > Integrations.
Base URL
https://userhero.co/api/v1Rate Limiting
Requests are limited to 100 per minute per API key. Rate limit headers are included in every response:
| Header | Description |
|---|---|
X-RateLimit-Remaining | Requests left in current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Error Format
All errors follow a consistent format:
{
"error": {
"code": "not_found",
"message": "Feedback item not found"
}
}| Code | HTTP Status | Description |
|---|---|---|
unauthorized | 401 | Missing or invalid API key |
forbidden | 403 | Insufficient permissions or API access disabled |
not_found | 404 | Resource not found |
bad_request | 400 | Invalid request body or parameters |
conflict | 409 | Duplicate idempotency key |
rate_limited | 429 | Too many requests |
Idempotency
POST requests support idempotency via the Idempotency-Key header. If you retry a request with the same key within 24 hours, the original response is returned.
curl -X POST https://userhero.co/api/v1/feedback \
-H "Authorization: Bearer sk_live_your_key" \
-H "Idempotency-Key: unique-request-id" \
-H "Content-Type: application/json" \
-d '{"workspaceId": "ws_123", "projectId": "proj_456", "message": "Bug report"}'Feedback
List Feedback
GET /api/v1/feedback?workspaceId={id}Query parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
status | No | Filter by status (new, in-progress, resolved, closed) |
category | No | Filter by category |
projectId | No | Filter by project |
userEmail | No | Filter by user email (exact match) |
page | No | Page number (default 1) |
pageSize | No | Results per page (default 20, max 100) |
Response fields:
| Field | Type | Description |
|---|---|---|
id | string | Feedback ID |
workspaceId | string | Workspace ID |
projectId | string | Project ID |
widgetId | string | Widget ID or "api" |
message | string | Feedback text |
category | string | null | Feedback category |
rating | number | null | Rating value |
status | string | Current status |
tags | string[] | Tags array |
priority | string | null | Priority level (low, medium, high, urgent) |
assigneeId | string | null | User ID of the assigned agent |
assigneeName | string | null | Display name of the assigned agent |
assigneeEmail | string | null | Email of the assigned agent |
watcherIds | string[] | User IDs subscribed to updates |
dueDate | string | null | SLA due date (ISO 8601) |
slaBreached | boolean | Whether the SLA deadline has been exceeded |
userEmail | string | null | Email of the user who submitted |
screenshotUrl | string | null | Screenshot image URL |
attachmentUrl | string | null | Attachment image URL |
audioUrl | string | null | Audio recording URL |
videoUrl | string | null | Video recording URL |
context | object | Contextual metadata |
votes | number | Vote count |
isPublic | boolean | Whether publicly visible |
createdAt | string | ISO 8601 timestamp |
updatedAt | string | ISO 8601 timestamp |
Upload Media
POST /api/v1/feedback/upload?workspaceId={id}Upload a file to use as screenshotUrl, attachmentUrl, audioUrl, or videoUrl when creating feedback — or as a conversation attachment for comments and notes.
Content-Type: multipart/form-data
| Field | Required | Description |
|---|---|---|
file | Yes | Image, audio, or video file (see supported types below) |
projectId | No | Project ID for storage organization (feedback media) |
feedbackId | No | Feedback ID — when provided, stores the file as a conversation attachment and returns a ready-to-use attachment object |
Supported file types:
| Category | MIME Types | Max Size |
|---|---|---|
| Image | image/png, image/jpeg, image/webp | 5 MB |
| Audio | audio/webm, audio/mp4, audio/mpeg, audio/ogg, audio/wav | 10 MB |
| Video | video/webm, video/mp4 | 50 MB |
Response (without feedbackId):
{
"data": {
"url": "https://storage.googleapis.com/...",
"contentType": "image/png",
"size": 102400
}
}Response (with feedbackId):
Returns an attachment object you can pass directly to the comments or notes endpoints:
{
"data": {
"id": "att_1712345678_abc123",
"url": "https://storage.googleapis.com/...",
"type": "image",
"fileName": "screenshot.png",
"fileSize": 102400,
"mimeType": "image/png"
}
}Create Feedback
POST /api/v1/feedbackBody parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
projectId | Yes | Project ID |
message | Yes | Feedback text |
category | No | Feedback category |
rating | No | Rating value |
tags | No | Array of tag strings |
userEmail | No | Email of the submitting user |
screenshotUrl | No | URL of a screenshot image (use the Upload endpoint) |
attachmentUrl | No | URL of an attachment image (use the Upload endpoint) |
audioUrl | No | URL of an audio recording (use the Upload endpoint) |
videoUrl | No | URL of a video recording (use the Upload endpoint) |
priority | No | Priority level (low, medium, high, urgent). If the project has SLA rules, a dueDate is auto-computed. |
context | No | Object with contextual metadata (e.g. page URL, browser) |
customMetadata | No | Object with arbitrary key-value data |
widgetId | No | Widget ID (defaults to "api") |
{
"workspaceId": "ws_123",
"projectId": "proj_456",
"message": "Something is broken",
"category": "bug",
"userEmail": "user@example.com",
"screenshotUrl": "https://storage.googleapis.com/...",
"context": { "page": "/settings", "browser": "Chrome" }
}Get Feedback
GET /api/v1/feedback/{id}?workspaceId={id}Returns the full feedback item including customerComments and internalNotes arrays with any attachments.
Update Feedback
PATCH /api/v1/feedback/{id}Body parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
status | No | new, in_progress, resolved, closed |
category | No | Feedback category |
tags | No | Array of tag strings |
priority | No | low, medium, high, urgent |
assigneeId | No | User ID to assign (must be a workspace member) |
dueDate | No | ISO 8601 date string for SLA deadline |
isPublic | No | Whether publicly visible |
{
"workspaceId": "ws_123",
"status": "resolved",
"priority": "high",
"assigneeId": "user_abc",
"tags": ["critical"],
"isPublic": true
}Delete Feedback
DELETE /api/v1/feedback/{id}?workspaceId={id}List Comments
GET /api/v1/feedback/{id}/commentsReturns the customer conversation thread for a feedback item.
Response:
{
"data": [
{
"id": "cc_1712345678_abc123",
"message": "We're looking into this issue.",
"authorType": "agent",
"authorName": "Jane Smith",
"attachments": [],
"createdAt": "2026-04-06T10:30:00.000Z"
},
{
"id": "cc_1712345999_def456",
"message": "Thanks! Here's a screenshot.",
"authorType": "customer",
"authorName": "John Doe",
"attachments": [
{
"id": "att_1712345999_xyz",
"url": "https://storage.googleapis.com/...",
"type": "image",
"fileName": "screenshot.png",
"fileSize": 102400,
"mimeType": "image/png"
}
],
"createdAt": "2026-04-06T11:00:00.000Z"
}
]
}Add Comment
POST /api/v1/feedback/{id}/commentsAdd a comment to the customer conversation thread. Use authorType to specify whether the comment is from an agent or a customer (end-user).
Body parameters:
| Parameter | Required | Description |
|---|---|---|
message | Conditional | Comment text (max 2000 chars). Required if no attachments. |
authorType | No | "agent" (default) or "customer" |
authorName | Conditional | Required when authorType is "customer" |
authorEmail | Conditional | Required when authorType is "customer" |
attachments | No | Array of attachment objects (max 3). See Upload Media with feedbackId to get attachment objects. |
Agent reply:
{
"message": "We've fixed this in the latest release."
}Customer reply (on behalf of end-user):
{
"message": "The issue is still happening.",
"authorType": "customer",
"authorName": "John Doe",
"authorEmail": "john@example.com"
}With attachments:
{
"message": "Here's what I see",
"authorType": "customer",
"authorName": "John Doe",
"authorEmail": "john@example.com",
"attachments": [
{
"id": "att_1712345678_abc123",
"url": "https://storage.googleapis.com/...",
"type": "image",
"fileName": "screenshot.png",
"fileSize": 102400,
"mimeType": "image/png"
}
]
}Customer comments must be enabled on the feedback item. Use the dashboard or create feedback with a project that has customer comments enabled by default.
List Internal Notes
GET /api/v1/feedback/{id}/notesReturns internal notes for a feedback item. Notes are only visible to workspace members, never to customers.
Response:
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"content": "Escalated to engineering team.",
"authorName": "Jane Smith",
"authorEmail": "jane@company.com",
"attachments": [],
"createdAt": "2026-04-06T10:30:00.000Z"
}
]
}Add Internal Note
POST /api/v1/feedback/{id}/notesBody parameters:
| Parameter | Required | Description |
|---|---|---|
content | Conditional | Note text (max 5000 chars). Required if no attachments. |
contentHtml | No | Rich text HTML version of the note |
mentions | No | Array of user IDs mentioned in the note. Mentioned users are automatically added as watchers. |
attachments | No | Array of attachment objects (max 3) |
{
"content": "Customer confirmed this is reproducible on Chrome 120.",
"contentHtml": "<p>Customer confirmed this is reproducible on Chrome 120.</p>",
"mentions": ["user_abc"]
}Projects
List Projects
GET /api/v1/projects?workspaceId={id}Create Project
POST /api/v1/projects{
"workspaceId": "ws_123",
"name": "My App",
"domain": "app.example.com"
}Get Project
GET /api/v1/projects/{id}?workspaceId={id}Update Project
PATCH /api/v1/projects/{id}Body parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
name | No | Project display name |
domain | No | Associated domain |
slaRules | No | Array of SLA rules. Each rule: { priority, hours } where priority is low/medium/high/urgent and hours is the response deadline in hours. |
{
"workspaceId": "ws_123",
"name": "Updated Name",
"domain": "new.example.com",
"slaRules": [
{ "priority": "urgent", "hours": 4 },
{ "priority": "high", "hours": 24 }
]
}Delete Project
DELETE /api/v1/projects/{id}?workspaceId={id}Widgets
List Widgets
GET /api/v1/widgets?workspaceId={id}Optional projectId query parameter to filter by project.
Create Widget
POST /api/v1/widgets{
"workspaceId": "ws_123",
"projectId": "proj_456",
"name": "Feedback Button",
"config": { "type": "modal" },
"styling": { "primaryColor": "#6366f1" }
}Get Widget
GET /api/v1/widgets/{id}?workspaceId={id}Update Widget
PATCH /api/v1/widgets/{id}{
"workspaceId": "ws_123",
"name": "Updated Widget",
"enabled": true,
"config": { "type": "popover" }
}Delete Widget
DELETE /api/v1/widgets/{id}?workspaceId={id}Workspace
Get Workspace
GET /api/v1/workspace/{id}Get Usage
GET /api/v1/workspace/{id}/usageReturns plan limits and current usage counts.
Incoming Webhooks
List Webhooks
GET /api/v1/webhooks?workspaceId={id}Create Webhook
POST /api/v1/webhooks{
"workspaceId": "ws_123",
"name": "Zapier Integration",
"projectId": "proj_456"
}The response includes a secret field (format whsec_...) that is only shown once. Store it securely.
Delete Webhook
DELETE /api/v1/webhooks/{id}?workspaceId={id}Sending Data to a Webhook
# Compute HMAC-SHA256 signature
SIGNATURE=$(echo -n '{"message":"Bug report"}' | openssl dgst -sha256 -hmac "whsec_your_secret" | awk '{print $2}')
curl -X POST https://userhero.co/api/hooks/{slug} \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: sha256=$SIGNATURE" \
-d '{"message": "Bug report"}'Payload fields:
| Field | Required | Description |
|---|---|---|
message | Yes | Feedback text |
projectId | No | Target project (required if webhook has no default) |
category | No | Feedback category |
metadata | No | Arbitrary JSON metadata |
Customers
Manage customer profiles. Customer scopes: customers:read, customers:write, customers:delete.
List Customers
GET /api/v1/customers?workspaceId={id}Query parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
search | No | Search by name, email, or phone |
channel | No | Filter by channel (e.g. widget, email, api) |
tag | No | Filter by tag |
sort | No | Sort field (lastSeenAt, firstSeenAt, feedbackCount, name). Default: lastSeenAt |
order | No | Sort order (asc, desc). Default: desc |
page | No | Page number (default 1) |
pageSize | No | Results per page (default 20, max 100) |
Response fields:
| Field | Type | Description |
|---|---|---|
id | string | Customer ID |
workspaceId | string | Workspace ID |
name | string | null | Customer name |
primaryEmail | string | null | Primary email address |
primaryPhone | string | null | Primary phone number |
emails | string[] | All known email addresses |
phones | string[] | All known phone numbers |
socialHandles | object[] | Social media handles |
company | string | null | Company name |
tags | string[] | Tags array |
feedbackCount | number | Total feedback items from this customer |
channels | string[] | Channels through which the customer interacted |
lastSeenAt | string | null | ISO 8601 timestamp |
firstSeenAt | string | null | ISO 8601 timestamp |
createdAt | string | ISO 8601 timestamp |
updatedAt | string | ISO 8601 timestamp |
Create Customer
POST /api/v1/customersSupports idempotency via the Idempotency-Key header. Returns 409 Conflict if a customer with the same email or phone already exists in the workspace.
Body parameters:
| Parameter | Required | Description |
|---|---|---|
workspaceId | Yes | Workspace ID |
name | No | Customer name |
primaryEmail | No | Primary email address |
primaryPhone | No | Primary phone number |
emails | No | Array of email addresses |
phones | No | Array of phone numbers |
company | No | Company name |
tags | No | Array of tag strings |
notes | No | Free-text notes |
{
"workspaceId": "ws_123",
"name": "Jane Smith",
"primaryEmail": "jane@example.com",
"company": "Acme Inc",
"tags": ["enterprise", "beta"]
}Get Customer
GET /api/v1/customers/{id}Returns the full customer profile including notes, customFields, timezone, language, and mergedCustomerIds.
Update Customer
PATCH /api/v1/customers/{id}Body parameters:
| Parameter | Required | Description |
|---|---|---|
name | No | Customer name |
primaryEmail | No | Primary email |
primaryPhone | No | Primary phone |
emails | No | Replace all email addresses |
phones | No | Replace all phone numbers |
company | No | Company name |
tags | No | Replace all tags |
notes | No | Free-text notes |
customFields | No | Arbitrary key-value object |
socialHandles | No | Array of {platform, handle, accountId?} |
timezone | No | IANA timezone string |
language | No | Language code |
{
"name": "Jane Smith-Rogers",
"tags": ["enterprise", "beta", "vip"],
"customFields": { "plan": "pro", "accountManager": "alex" }
}Delete Customer
DELETE /api/v1/customers/{id}Requires customers:delete scope. Unlinks all feedback associated with this customer before deleting.
Merge Customers
POST /api/v1/customers/{targetId}/mergeMerges a source customer into the target customer. Combines emails, phones, social handles, and channels. Sums feedback counts and re-links all feedback from the source to the target. The source customer is deleted after the merge.
Requires customers:delete scope.
Body parameters:
| Parameter | Required | Description |
|---|---|---|
sourceCustomerId | Yes | ID of the customer to merge into the target |
{
"sourceCustomerId": "cust_old_duplicate"
}Response:
{
"data": {
"id": "cust_target",
"mergedSourceId": "cust_old_duplicate",
"mergedFeedbackCount": 5
}
}Code Examples
JavaScript (Node.js)
const API_KEY = 'sk_live_your_key';
const BASE = 'https://userhero.co/api/v1';
// List feedback
const res = await fetch(`${BASE}/feedback?workspaceId=ws_123`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
const { data } = await res.json();
// List feedback by user email
const userRes = await fetch(`${BASE}/feedback?workspaceId=ws_123&userEmail=user@example.com`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
// Upload an image
const form = new FormData();
form.append('file', fs.createReadStream('screenshot.png'));
const uploadRes = await fetch(`${BASE}/feedback/upload?workspaceId=ws_123`, {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}` },
body: form,
});
const { data: { url } } = await uploadRes.json();
// Upload an audio file
const audioForm = new FormData();
audioForm.append('file', fs.createReadStream('recording.webm'));
const audioUpload = await fetch(`${BASE}/feedback/upload?workspaceId=ws_123`, {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}` },
body: audioForm,
});
const { data: { url: audioUrl } } = await audioUpload.json();
// Create feedback with attachment and audio
await fetch(`${BASE}/feedback`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
workspaceId: 'ws_123',
projectId: 'proj_456',
message: 'Feature request from API',
screenshotUrl: url,
audioUrl: audioUrl,
}),
});
// Upload a conversation attachment
const attForm = new FormData();
attForm.append('file', fs.createReadStream('screenshot.png'));
attForm.append('feedbackId', 'feedback_id');
const attRes = await fetch(`${BASE}/feedback/upload?workspaceId=ws_123`, {
method: 'POST',
headers: { Authorization: `Bearer ${API_KEY}` },
body: attForm,
});
const attachment = (await attRes.json()).data;
// Add agent reply with attachment
await fetch(`${BASE}/feedback/feedback_id/comments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'Here is the fix',
attachments: [attachment],
}),
});
// Add customer reply on behalf of end-user
await fetch(`${BASE}/feedback/feedback_id/comments`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: 'The issue is still happening',
authorType: 'customer',
authorName: 'John Doe',
authorEmail: 'john@example.com',
}),
});
// Add internal note
await fetch(`${BASE}/feedback/feedback_id/notes`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: 'Escalated to engineering',
}),
});Python
import requests
API_KEY = "sk_live_your_key"
BASE = "https://userhero.co/api/v1"
headers = {"Authorization": f"Bearer {API_KEY}"}
# List feedback
res = requests.get(f"{BASE}/feedback", headers=headers, params={"workspaceId": "ws_123"})
feedback = res.json()["data"]
# List feedback by user email
res = requests.get(f"{BASE}/feedback", headers=headers, params={
"workspaceId": "ws_123",
"userEmail": "user@example.com",
})
# Upload an image
with open("screenshot.png", "rb") as f:
upload_res = requests.post(
f"{BASE}/feedback/upload",
headers=headers,
params={"workspaceId": "ws_123"},
files={"file": ("screenshot.png", f, "image/png")},
)
url = upload_res.json()["data"]["url"]
# Upload an audio file
with open("recording.webm", "rb") as f:
audio_res = requests.post(
f"{BASE}/feedback/upload",
headers=headers,
params={"workspaceId": "ws_123"},
files={"file": ("recording.webm", f, "audio/webm")},
)
audio_url = audio_res.json()["data"]["url"]
# Create feedback with attachment and audio
requests.post(f"{BASE}/feedback", headers=headers, json={
"workspaceId": "ws_123",
"projectId": "proj_456",
"message": "Bug from CI pipeline",
"screenshotUrl": url,
"audioUrl": audio_url,
})
# Upload a conversation attachment
with open("screenshot.png", "rb") as f:
att_res = requests.post(
f"{BASE}/feedback/upload",
headers=headers,
params={"workspaceId": "ws_123"},
data={"feedbackId": "feedback_id"},
files={"file": ("screenshot.png", f, "image/png")},
)
attachment = att_res.json()["data"]
# Add agent reply with attachment
requests.post(f"{BASE}/feedback/feedback_id/comments", headers=headers, json={
"message": "Here is the fix",
"attachments": [attachment],
})
# Add customer reply on behalf of end-user
requests.post(f"{BASE}/feedback/feedback_id/comments", headers=headers, json={
"message": "The issue is still happening",
"authorType": "customer",
"authorName": "John Doe",
"authorEmail": "john@example.com",
})
# Add internal note
requests.post(f"{BASE}/feedback/feedback_id/notes", headers=headers, json={
"content": "Escalated to engineering",
})cURL
# List feedback by user email
curl "https://userhero.co/api/v1/feedback?workspaceId=ws_123&userEmail=user@example.com" \
-H "Authorization: Bearer sk_live_your_key"
# Upload an image
curl -X POST "https://userhero.co/api/v1/feedback/upload?workspaceId=ws_123" \
-H "Authorization: Bearer sk_live_your_key" \
-F "file=@screenshot.png"
# Create feedback with screenshot
curl -X POST "https://userhero.co/api/v1/feedback" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"workspaceId": "ws_123", "projectId": "proj_456", "message": "Bug report", "screenshotUrl": "https://storage.googleapis.com/..."}'
# Upload conversation attachment
curl -X POST "https://userhero.co/api/v1/feedback/upload?workspaceId=ws_123" \
-H "Authorization: Bearer sk_live_your_key" \
-F "file=@screenshot.png" \
-F "feedbackId=feedback_id"
# Add agent reply
curl -X POST "https://userhero.co/api/v1/feedback/feedback_id/comments" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"message": "We are looking into this."}'
# Add customer reply on behalf of end-user
curl -X POST "https://userhero.co/api/v1/feedback/feedback_id/comments" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"message": "Still broken", "authorType": "customer", "authorName": "John Doe", "authorEmail": "john@example.com"}'
# List comments
curl "https://userhero.co/api/v1/feedback/feedback_id/comments" \
-H "Authorization: Bearer sk_live_your_key"
# Add internal note
curl -X POST "https://userhero.co/api/v1/feedback/feedback_id/notes" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-d '{"content": "Escalated to engineering"}'
# List projects
curl "https://userhero.co/api/v1/projects?workspaceId=ws_123" \
-H "Authorization: Bearer sk_live_your_key"
# Create project with idempotency
curl -X POST "https://userhero.co/api/v1/projects" \
-H "Authorization: Bearer sk_live_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-proj-001" \
-d '{"workspaceId": "ws_123", "name": "New Project"}'