UserHero Docs
API Reference

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/v1

Rate Limiting

Requests are limited to 100 per minute per API key. Rate limit headers are included in every response:

HeaderDescription
X-RateLimit-RemainingRequests left in current window
X-RateLimit-ResetUnix timestamp when the window resets

Error Format

All errors follow a consistent format:

{
  "error": {
    "code": "not_found",
    "message": "Feedback item not found"
  }
}
CodeHTTP StatusDescription
unauthorized401Missing or invalid API key
forbidden403Insufficient permissions or API access disabled
not_found404Resource not found
bad_request400Invalid request body or parameters
conflict409Duplicate idempotency key
rate_limited429Too 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:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
statusNoFilter by status (new, in-progress, resolved, closed)
categoryNoFilter by category
projectIdNoFilter by project
userEmailNoFilter by user email (exact match)
pageNoPage number (default 1)
pageSizeNoResults per page (default 20, max 100)

Response fields:

FieldTypeDescription
idstringFeedback ID
workspaceIdstringWorkspace ID
projectIdstringProject ID
widgetIdstringWidget ID or "api"
messagestringFeedback text
categorystring | nullFeedback category
ratingnumber | nullRating value
statusstringCurrent status
tagsstring[]Tags array
prioritystring | nullPriority level (low, medium, high, urgent)
assigneeIdstring | nullUser ID of the assigned agent
assigneeNamestring | nullDisplay name of the assigned agent
assigneeEmailstring | nullEmail of the assigned agent
watcherIdsstring[]User IDs subscribed to updates
dueDatestring | nullSLA due date (ISO 8601)
slaBreachedbooleanWhether the SLA deadline has been exceeded
userEmailstring | nullEmail of the user who submitted
screenshotUrlstring | nullScreenshot image URL
attachmentUrlstring | nullAttachment image URL
audioUrlstring | nullAudio recording URL
videoUrlstring | nullVideo recording URL
contextobjectContextual metadata
votesnumberVote count
isPublicbooleanWhether publicly visible
createdAtstringISO 8601 timestamp
updatedAtstringISO 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

FieldRequiredDescription
fileYesImage, audio, or video file (see supported types below)
projectIdNoProject ID for storage organization (feedback media)
feedbackIdNoFeedback ID — when provided, stores the file as a conversation attachment and returns a ready-to-use attachment object

Supported file types:

CategoryMIME TypesMax Size
Imageimage/png, image/jpeg, image/webp5 MB
Audioaudio/webm, audio/mp4, audio/mpeg, audio/ogg, audio/wav10 MB
Videovideo/webm, video/mp450 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/feedback

Body parameters:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
projectIdYesProject ID
messageYesFeedback text
categoryNoFeedback category
ratingNoRating value
tagsNoArray of tag strings
userEmailNoEmail of the submitting user
screenshotUrlNoURL of a screenshot image (use the Upload endpoint)
attachmentUrlNoURL of an attachment image (use the Upload endpoint)
audioUrlNoURL of an audio recording (use the Upload endpoint)
videoUrlNoURL of a video recording (use the Upload endpoint)
priorityNoPriority level (low, medium, high, urgent). If the project has SLA rules, a dueDate is auto-computed.
contextNoObject with contextual metadata (e.g. page URL, browser)
customMetadataNoObject with arbitrary key-value data
widgetIdNoWidget 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:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
statusNonew, in_progress, resolved, closed
categoryNoFeedback category
tagsNoArray of tag strings
priorityNolow, medium, high, urgent
assigneeIdNoUser ID to assign (must be a workspace member)
dueDateNoISO 8601 date string for SLA deadline
isPublicNoWhether 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}/comments

Returns 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}/comments

Add 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:

ParameterRequiredDescription
messageConditionalComment text (max 2000 chars). Required if no attachments.
authorTypeNo"agent" (default) or "customer"
authorNameConditionalRequired when authorType is "customer"
authorEmailConditionalRequired when authorType is "customer"
attachmentsNoArray 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}/notes

Returns 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}/notes

Body parameters:

ParameterRequiredDescription
contentConditionalNote text (max 5000 chars). Required if no attachments.
contentHtmlNoRich text HTML version of the note
mentionsNoArray of user IDs mentioned in the note. Mentioned users are automatically added as watchers.
attachmentsNoArray 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:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
nameNoProject display name
domainNoAssociated domain
slaRulesNoArray 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}/usage

Returns 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:

FieldRequiredDescription
messageYesFeedback text
projectIdNoTarget project (required if webhook has no default)
categoryNoFeedback category
metadataNoArbitrary JSON metadata

Customers

Manage customer profiles. Customer scopes: customers:read, customers:write, customers:delete.

List Customers

GET /api/v1/customers?workspaceId={id}

Query parameters:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
searchNoSearch by name, email, or phone
channelNoFilter by channel (e.g. widget, email, api)
tagNoFilter by tag
sortNoSort field (lastSeenAt, firstSeenAt, feedbackCount, name). Default: lastSeenAt
orderNoSort order (asc, desc). Default: desc
pageNoPage number (default 1)
pageSizeNoResults per page (default 20, max 100)

Response fields:

FieldTypeDescription
idstringCustomer ID
workspaceIdstringWorkspace ID
namestring | nullCustomer name
primaryEmailstring | nullPrimary email address
primaryPhonestring | nullPrimary phone number
emailsstring[]All known email addresses
phonesstring[]All known phone numbers
socialHandlesobject[]Social media handles
companystring | nullCompany name
tagsstring[]Tags array
feedbackCountnumberTotal feedback items from this customer
channelsstring[]Channels through which the customer interacted
lastSeenAtstring | nullISO 8601 timestamp
firstSeenAtstring | nullISO 8601 timestamp
createdAtstringISO 8601 timestamp
updatedAtstringISO 8601 timestamp

Create Customer

POST /api/v1/customers

Supports 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:

ParameterRequiredDescription
workspaceIdYesWorkspace ID
nameNoCustomer name
primaryEmailNoPrimary email address
primaryPhoneNoPrimary phone number
emailsNoArray of email addresses
phonesNoArray of phone numbers
companyNoCompany name
tagsNoArray of tag strings
notesNoFree-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:

ParameterRequiredDescription
nameNoCustomer name
primaryEmailNoPrimary email
primaryPhoneNoPrimary phone
emailsNoReplace all email addresses
phonesNoReplace all phone numbers
companyNoCompany name
tagsNoReplace all tags
notesNoFree-text notes
customFieldsNoArbitrary key-value object
socialHandlesNoArray of {platform, handle, accountId?}
timezoneNoIANA timezone string
languageNoLanguage 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}/merge

Merges 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:

ParameterRequiredDescription
sourceCustomerIdYesID 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"}'

On this page